diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 6fa25fed8..e30c7016e 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -5,6 +5,7 @@ "packages/access-api": {}, "packages/capabilities": {}, "packages/upload-api": {}, - "packages/upload-client": {} + "packages/upload-client": {}, + "packages/w3up-client": {} } } diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index fd0032bf3..b14a1bd56 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -3,5 +3,6 @@ "packages/access-api": "5.2.1", "packages/capabilities": "4.0.1", "packages/upload-api": "2.0.0", - "packages/upload-client": "8.1.0" + "packages/upload-client": "8.1.0", + "packages/w3up-client": "5.1.0" } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index daa80fd5b..842009d71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,8 @@ jobs: contains(fromJson(needs.release.outputs.paths_released), 'packages/access-client') || contains(fromJson(needs.release.outputs.paths_released), 'packages/capabilities') || contains(fromJson(needs.release.outputs.paths_released), 'packages/upload-client') || - contains(fromJson(needs.release.outputs.paths_released), 'packages/upload-api') + contains(fromJson(needs.release.outputs.paths_released), 'packages/upload-api') || + contains(fromJson(needs.release.outputs.paths_released), 'packages/w3up-client') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/w3up-client.yml b/.github/workflows/w3up-client.yml new file mode 100644 index 000000000..b816bcf4d --- /dev/null +++ b/.github/workflows/w3up-client.yml @@ -0,0 +1,38 @@ +name: w3up-client +on: + push: + branches: + - main + paths: + - 'packages/w3up-client/**' + - '.github/workflows/w3up-client.yml' + - 'pnpm-lock.yaml' + pull_request: + paths: + - 'packages/w3up-client/**' + - '.github/workflows/w3up-client.yml' + - 'pnpm-lock.yaml' +jobs: + test: + name: Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/w3up-client + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install + uses: pnpm/action-setup@v2.2.3 + with: + version: 7 + - name: Setup + uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + - run: pnpm --filter '@web3-storage/w3up-client...' install + - uses: ./packages/w3up-client/.github/actions/test + with: + w3up-client-dir: ./packages/w3up-client/ diff --git a/packages/access-api/package.json b/packages/access-api/package.json index 8cfe948a1..61dae28ce 100644 --- a/packages/access-api/package.json +++ b/packages/access-api/package.json @@ -28,6 +28,7 @@ "@web3-storage/access": "workspace:^", "@web3-storage/capabilities": "workspace:^", "@web3-storage/worker-utils": "0.4.3-dev", + "dotenv": "^16.0.3", "kysely": "^0.23.4", "kysely-d1": "^0.3.0", "multiformats": "^11.0.2", @@ -53,7 +54,6 @@ "@ucanto/client": "^5.1.0", "better-sqlite3": "8.0.1", "buffer": "^6.0.3", - "dotenv": "^16.0.3", "esbuild": "^0.17.2", "git-rev-sync": "^3.0.2", "hd-scripts": "^4.0.0", diff --git a/packages/upload-client/package.json b/packages/upload-client/package.json index 4a6901dcd..cdfb67e84 100644 --- a/packages/upload-client/package.json +++ b/packages/upload-client/package.json @@ -81,7 +81,7 @@ "@ucanto/server": "^6.1.0", "assert": "^2.0.0", "blockstore-core": "^3.0.0", - "c8": "^7.12.0", + "c8": "^7.13.0", "hd-scripts": "^4.0.0", "hundreds": "^0.0.9", "ipfs-unixfs-exporter": "^10.0.0", diff --git a/packages/w3up-client/.github/actions/test/action.yml b/packages/w3up-client/.github/actions/test/action.yml new file mode 100644 index 000000000..733c34321 --- /dev/null +++ b/packages/w3up-client/.github/actions/test/action.yml @@ -0,0 +1,18 @@ +name: Test +description: 'test' + +# install npm dependencies (e.g. via `npm` or monorepo `pnpm`) before invoking this action to run tests + +inputs: + w3up-client-dir: + description: 'path to directory of w3up-client package' + +runs: + using: "composite" + steps: + - run: npm run lint + shell: bash + working-directory: ${{ inputs.w3up-client-dir }} + - run: npm test + shell: bash + working-directory: ${{ inputs.w3up-client-dir }} diff --git a/packages/w3up-client/.gitignore b/packages/w3up-client/.gitignore new file mode 100644 index 000000000..b2ac53210 --- /dev/null +++ b/packages/w3up-client/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +coverage +docs +docs-generated \ No newline at end of file diff --git a/packages/w3up-client/CHANGELOG.md b/packages/w3up-client/CHANGELOG.md new file mode 100644 index 000000000..d76b04bb6 --- /dev/null +++ b/packages/w3up-client/CHANGELOG.md @@ -0,0 +1,97 @@ +# Changelog + +## [5.1.0](https://github.com/web3-storage/w3up-client/compare/v5.0.0...v5.1.0) (2023-03-24) + + +### Features + +* updated README instructions for MVP ([#85](https://github.com/web3-storage/w3up-client/issues/85)) ([0d0a038](https://github.com/web3-storage/w3up-client/commit/0d0a0389f0b6b29c843d5b28c3ea2d840d38120f)) + +## [5.0.0](https://github.com/web3-storage/w3up-client/compare/v4.3.0...v5.0.0) (2023-03-23) + + +### ⚠ BREAKING CHANGES + +* updated access client dep ([#89](https://github.com/web3-storage/w3up-client/issues/89)) + +### Features + +* add HAMT sharded directories support ([#87](https://github.com/web3-storage/w3up-client/issues/87)) ([a6673e9](https://github.com/web3-storage/w3up-client/commit/a6673e98f51dc1dc93e5e40ea752a5c10e46c159)) +* updated access client dep ([#89](https://github.com/web3-storage/w3up-client/issues/89)) ([35f3964](https://github.com/web3-storage/w3up-client/commit/35f39640a62eaf9a2e6a81632e32cf1851d640b4)) + +## [4.3.0](https://github.com/web3-storage/w3up-client/compare/v4.2.0...v4.3.0) (2023-03-21) + + +### Features + +* add authorize to client, register no longer needs email ([c9555d9](https://github.com/web3-storage/w3up-client/commit/c9555d92edb1ded9c7db81efcdd2c83331b52106)) +* expose connection id did and add email back to registerSpace ([ee1cf3a](https://github.com/web3-storage/w3up-client/commit/ee1cf3a30a79a98c21f2e897d6a26e443b41390f)) +* use new claimDelegations "use case" ([1659786](https://github.com/web3-storage/w3up-client/commit/1659786fd79da6292d3605c1ca09b80d94ac83ca)) + + +### Bug Fixes + +* back to 100% test coverage ([5ae7f1e](https://github.com/web3-storage/w3up-client/commit/5ae7f1e1d19f7eb95b13ed3bc491fc99e51296d0)) +* keep casting defaultProvider() ([7b8c859](https://github.com/web3-storage/w3up-client/commit/7b8c8594abd1665b0c0061ab2eef233d6a0b6cdf)) +* pass registerSpace default provider inferred from connection ([224f818](https://github.com/web3-storage/w3up-client/commit/224f818f45b3fa4778a659c4f95124c92534c354)) +* typos ([52c648a](https://github.com/web3-storage/w3up-client/commit/52c648a525466e1d6e0619ed4ab663164a9b6a9d)) +* typos ([52c648a](https://github.com/web3-storage/w3up-client/commit/52c648a525466e1d6e0619ed4ab663164a9b6a9d)) +* update package-lock ([6aa7c47](https://github.com/web3-storage/w3up-client/commit/6aa7c4785ae2bc49039c326e02d1fd042460b83d)) +* use released packages to green the build ([05881fc](https://github.com/web3-storage/w3up-client/commit/05881fce652a1bc964937bec9c0cdd20aa2204b2)) +* warnings about uploads being public/permanent ([187228a](https://github.com/web3-storage/w3up-client/commit/187228a828ff357f3d1083738673c6a502f2aff9)) + +## [4.2.0](https://github.com/web3-storage/w3up-client/compare/v4.1.0...v4.2.0) (2023-02-15) + + +### Features + +* update to latest dependencies ([f4da59e](https://github.com/web3-storage/w3up-client/commit/f4da59ec10d8f7e96857998d34672d8848652445)) + +## [4.1.0](https://github.com/web3-storage/w3up-client/compare/v4.0.1...v4.1.0) (2023-01-11) + + +### Features + +* add CAR upload method ([#72](https://github.com/web3-storage/w3up-client/issues/72)) ([8b31255](https://github.com/web3-storage/w3up-client/commit/8b31255521e6fd875fe043b9c09b347759ebf315)) + +## [4.0.1](https://github.com/web3-storage/w3up-client/compare/v4.0.0...v4.0.1) (2022-12-14) + + +### Bug Fixes + +* prod access service DID ([67a5d4c](https://github.com/web3-storage/w3up-client/commit/67a5d4c77eb054f5e0075137e77daed3337f35d2)) + +## [4.0.0](https://github.com/web3-storage/w3up-client/compare/v3.2.0...v4.0.0) (2022-12-14) + + +### ⚠ BREAKING CHANGES + +* The client has been re-written as a wrapper around [access-client](https://www.npmjs.com/package/@web3-storage/access) and [upload-client](https://www.npmjs.com/package/@web3-storage/upload-client) and the API has changed. + +Migration notes: + +* `client.account()` has been removed, use `client.currentSpace()` +* `client.exportDelegation()` has been removed, use `client.createDelegation()` and then call `export()` on the returned value and encode the returned blocks as a CAR file using the [`@ipld/car`](https://www.npmjs.com/package/@ipld/car) library. +* `client.identity()` has been removed, use `client.agent()` + `client.currentSpace()` + `client.delegations()` +* `client.importDelegation()` has been removed, use `client.addProof()` (for general delegations to your agent) or `client.addSpace()` (to add a proof and _also_ add the space to your list of spaces). +* `client.insights()` has been removed - this was never working +* `client.invoke()` has been removed +* `client.list()` has been removed, use `client.capability.upload.list()` +* `client.makeDelegation()` has been renamed and signature has changed, use `client.createDelegation()` +* `client.register()` has been removed, use `client.registerSpace()` +* `client.remove()` has been removed, use `client.capability.store.remove()` +* `client.removeUpload()` has been removed, use `client.capability.upload.remove()` +* `client.stat()` has been removed, use `client.capability.store.list()` +* `client.upload()` has been removed, use `client.capability.store.add()` +* `client.uploadAdd()` has been removed, use `client.capability.upload.add()` +* `client.whoami()` has been removed, use `client.capability.space.info()` + +### Features + +* consume upload and access client ([#58](https://github.com/web3-storage/w3up-client/issues/58)) ([7bd91d5](https://github.com/web3-storage/w3up-client/commit/7bd91d59da2b961a4227d9c062e74169e645a0bc)) + + +### Bug Fixes + +* release please package name ([3d00586](https://github.com/web3-storage/w3up-client/commit/3d0058658d4a0e819b2da49c7cea3d084b8bba1b)) +* remove pnpm reference from deploy-docs workflow ([6807f5d](https://github.com/web3-storage/w3up-client/commit/6807f5d91366e24ffe28d3177540549efbf808ca)) diff --git a/packages/w3up-client/LICENSE.md b/packages/w3up-client/LICENSE.md new file mode 100644 index 000000000..8b4860909 --- /dev/null +++ b/packages/w3up-client/LICENSE.md @@ -0,0 +1,232 @@ +The contents of this repository are Copyright (c) corresponding authors and +contributors, licensed under the `Permissive License Stack` meaning either of: + +- Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 + ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) + +- MIT Software License: https://opensource.org/licenses/MIT + ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) + +You may not use the contents of this repository except in compliance +with one of the listed Licenses. For an extended clarification of the +intent behind the choice of Licensing please refer to +https://protocol.ai/blog/announcing-the-permissive-license-stack/ + +Unless required by applicable law or agreed to in writing, software +distributed under the terms listed in this notice is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See each License for the specific language +governing permissions and limitations under that License. + + + +`SPDX-License-Identifier: Apache-2.0 OR MIT` + +Verbatim copies of both licenses are included below: + +
Apache-2.0 Software License + +``` + 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 +``` + +
+ +
MIT Software License + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +
diff --git a/packages/w3up-client/README.md b/packages/w3up-client/README.md new file mode 100644 index 000000000..eccd6f06e --- /dev/null +++ b/packages/w3up-client/README.md @@ -0,0 +1,760 @@ +

+

The main JavaScript client for the w3up platform by https://web3.storage

+

+ GitHub Workflow Status + + Twitter Follow + License: Apache-2.0 OR MIT +

+ +> ### ⚠️❗ w3up-client and the available hosted APIs are currently beta preview features +> Please read the beta Terms of Service ([web3.storage](https://console.web3.storage/terms), [NFT.Storage](https://console.nft.storage/terms)) for more details. +> +> Open an issue on the repo or reach out to the #web3-storage channel on [IPFS Discord](https://docs.ipfs.tech/community/chat/#discord) if you have any +questions! + +## About + +`@web3-storage/w3up-client` is a JavaScript library that provides a convenient interface to the w3up platform, a simple "on-ramp" to the content-addressed decentralized IPFS network. + +This library is the user-facing "porcelain" client for interacting with w3up services from JavaScript. It wraps the lower-level [`@web3-storage/access`][access-client-github] and [`@web3-storage/upload-client`][upload-client-github] client packages, which target individual w3up services. We recommend using `w3up-client` instead of using those "plumbing" packages directly, but you may find them useful if you need more context on w3up's architecture and internals. + +> ⚠️❗ __Public Data__ 🌎: All data uploaded to w3up is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using w3up. + +> ⚠️❗ __Permanent Data__ ♾️: Removing files from w3up will remove them from the file listing for your account, but that doesn’t prevent nodes on the decentralized storage network from retaining copies of the data indefinitely. Do not use w3up for data that may need to be permanently deleted in the future. + +- [Install](#install) +- [Usage](#usage) + - [Core concepts](#core-concepts) + - [Basic usage](#basic-usage) + - [Creating a client object](#creating-a-client-object) + - [Creating and registering Spaces](#creating-and-registering-spaces) + - [Uploading data](#uploading-data) + - [Alternate implementation options](#alternate-implementation-options) - _Coming soon!_ +- [API](#api) +- [Contributing](#contributing) +- [License](#license) + +## Install + +You can add the `@web3-storage/w3up-client` package to your JavaScript or TypeScript project with `npm`: + +```sh +npm install @web3-storage/w3up-client +``` + +## Usage + +[API Reference](#api) + +### Core concepts + +w3up services use [ucanto][ucanto], a Remote Procedure Call (RPC) framework built around [UCAN](https://ucan.xzy), or User Controlled Authorization Networks. UCANs are a powerful capability-based authorization system that allows fine-grained sharing of permissions through a process called _delegation_. See our [intro to UCAN blog post](https://blog.web3.storage/posts/intro-to-ucan) for an overview of UCAN. + +`w3up-client` and `ucanto` take care of the details of UCANs for you, but a few of the underlying terms and concepts may "bubble up" to the surface of the API, so we'll cover the basics here. We'll also go over some terms that are specific to w3up that you might not have encountered elsewhere. + +UCAN-based APIs are centered around _capabilities_, which are comprised of an _ability_ and a _resource_. Together, the ability and resource determine what action a client can perform and what objects in the system can be acted upon. When invoking a service method, a client will present a UCAN token that includes an ability and resource, along with _proofs_ that verify that they should be allowed to exercise the capability. + +To invoke a capability, the client must have a private signing key, which is managed by a component called an _Agent_. When you [create a client object](#creating-a-client-object) with `w3up-client`, an Agent is automatically created for you and used when making requests. The Agent's keys and metadata are securely stored and are loaded the next time you create a client. + +Each device or browser should create its own Agent, so that private keys are never shared across multiple devices. Instead of sharing keys, a user can delegate some or all of their capabilities from one Agent to another. + +When you upload data to w3up, your uploads are linked to a unique _Space_ acts as a "namespace" for the data you upload. Spaces are used to keep track of which uploads belong to which users, among other things. + +When invoking storage capabilities, the Space ID is the "resource" portion of the capability, while the ability is an action like `store/add` or `store/remove`. + +Both Agents and Spaces are identified using _DIDs_, or Decentralized Identity Documents. DIDs are a [W3C specification](https://www.w3.org/TR/did-core/) for verifiable identities in decentralized systems. There are several DID "methods," but the ones most commonly used by w3up are [`did:key`](https://w3c-ccg.github.io/did-method-key/), which includes a public key directly in the DID string. Agents and Spaces both use `did:key` URI strings as their primary identifiers. The other DID method used by w3up is [`did:web`](https://w3c-ccg.github.io/did-method-web/), which is used to identify the service providers. + +Agents and Spaces are both generated by `w3up-client` on the user's local machine. Before they can be used for storage, the user will need to [register the space](#creating-and-registering-spaces) by confirming their email address. Once registered, a Space can be used to [upload files and directories](#uploading-data). + +### Basic usage + +This section shows some of the basic operations available in the `w3up-client` package. See the [API reference docs][docs] or the source code of the [`w3up-cli` package][w3up-cli-github], which uses `w3up-client` throughout. + +Before data can be uploaded via the client, the client needs to have permissions to upload to the target service either by having permissions to a registered `Space`. This means either you (the developer) or your user needs to have a registered Space. + +Currently, `w3up-client` offers as defaults two beta services to register Spaces with and upload data to that the w3up core maintainers also run: +- [web3.storage](http://web3.storage) w3up beta: A developer storage platform for any data +- [NFT.Storage](http://NFT.Storage) w3up beta: A free service for archiving specifically off-chain NFT data + +However, `w3up-client` can be used for any service that complies to the w3up [specs](https://github.com/web3-storage/specs) and [protocol](https://github.com/web3-storage/w3protocol/). + +> By you or your users registering a w3up beta Space via email confirmation with either [NFT.Storage](http://NFT.Storage) or [web3.storage](http://web3.storage), you agree to the relevant w3up beta Terms of Service ([web3.storage](https://console.web3.storage/terms), [NFT.Storage](https://console.nft.storage/terms)). If you have an existing non-w3up beta account with NFT.Storage or web3.storage and register for the w3up beta version of the same product (NFT.Storage or web3.storage) using the same email, then at the end of the beta period, these accounts will be combined. Until the beta period is over and this migration occurs, uploads to w3up will not appear in your NFT.Storage or web3.storage account (and vice versa), even if you register with the same email (_coming soon!_). + +In terms of whether you or your user should register the Space (and more broadly how to integrate `w3up-client`), there are three general wasy to integrate. +- (Simplest) Client-server: You (the developer) own the Space and register it with the service of your choosing, and your user uploads to your backend infra before you upload it to the service +- (More complex) [Server-owned space with direct upload from end-user](#server-owned-space-with-direct-upload-from-end-user): You own the Space and register it with the service of your choosing, but you give a delegated UCAN token to your user to upload directly to the service +- (Most complex) [User-owned](#user-owned): Your user owns the Space and registers it (likely with the service you choose for them in your code, but not necessarily), and they use it to upload directly with the service (if you want to instrument visibility into what they’re uploading, you’ll have to write separate code in your app for it) + +The first and simplest of these options (client-server) is covered in-depth in this section, though the other two options are discussed further down in the README as well. + +#### Creating a client object + +The package provides a [static `create` function][docs-create] that returns a [`Client` object][docs-Client]. + +```js +import { create } from '@web3-storage/w3up-client' + +const client = await create() +``` + +By default, clients will be configured to use the production w3up service endpoints, and the client will create a new [`Agent`][access-docs-Agent] with a persistent `Store` if it can't find one locally to load. + +Agents are entities that control the private signing keys used to interact with the w3up service layer. You can access the client's `Agent` with the [`agent()` accessor method][docs-Client#agent]. + +`create` accepts an optional [`ClientFactoryOptions` object][docs-ClientFactoryOptions], which can be used to target a non-production instance of the w3up access and upload services, or to use a non-default persistent `Store`. See the [`@web3-storage/access` docs](https://web3-storage.github.io/w3protocol/modules/_web3_storage_access.html) for more about `Store` configuration. + +#### Authorizing your agent + +In order to store data with w3up, you'll need to authorize your agent. Currently you can only authorize your agent by confirming your email address. By confirming your email address w3up will attest that you are not a robot and are ok to upload data to the service! Hooray. + +Authorizing your agent allows you to claim spaces and other delegations that you created on a _different_ agent that is authorized to the _same_ email account. Authorization needs to happen only once per agent. + +```js +await client.authorize('zaphod@beeblebrox.galaxy') +``` + +Calling `authorize` will cause an email to be sent to the given address. Once a user clicks the confirmation link in the email, the `authorize` method will resolve. Make sure to check for errors, as `authorize` will fail if the email is not confirmed within the expiration timeout. + +Note: Alternatively, you can add a delegation for access to a space created by a different authorized agent, see the [`addSpace` client method](docs-client#addSpace). + +If this is not the first time you authorized an agent with your email, then you'll want to claim any spaces and delegations you have on your other agent(s): + +```js +await client.capability.access.claim() +``` + +#### Creating and registering Spaces + +Before you can upload data, you'll need to create a [`Space`][docs-Space] and register it with the service. A Space acts as a namespace for your uploads. Spaces are created using the [`createSpace` client method][docs-client#createSpace]: + +```js +const space = await client.createSpace('my-awesome-space') +``` + +The name parameter is optional. If provided, it will be stored in your client's local state store and can be used to provide a friendly name for user interfaces. + +After creating a `Space`, you'll need to register it with the w3up service before you can upload data. + +First, set the space as your "current" space using the [`setCurrentSpace` method][docs-Client#setCurrentSpace], passing in the DID of the `space` object you created above: + +```js +await client.setCurrentSpace(space.did()) +``` + +Next, call the [`registerSpace` method][docs-Client#registerSpace], passing in the _same_ email address you used to authorize your agent. You +can specify a storage provider for the space to use by passing a provider DID as the `provider` option: + +```js +try { + await client.registerSpace('zaphod@beeblebrox.galaxy', { provider: 'did:web:web3.storage' }) +} catch (err) { + console.error('registration failed: ', err) +} +``` + +By default, calling `registerSpace` registers the Space with web3.storage w3up. You can pass the optional `provider` param to register with NFT.Storage w3up instead. + +```js +try { + await client.registerSpace('zaphod@beeblebrox.galaxy', { provider: 'did:web:nft.storage' }) +} catch (err) { + console.error('registration failed: ', err) +} +``` + +#### Uploading data + +Once you've [authorized](#authorizing-your-agent), [created and registered a space](#creating-and-registering-spaces), you can upload files to the w3up platform. + +Call [`uploadFile`][docs-Client#uploadFile] to upload a single file, or [`uploadDirectory`][docs-Client#uploadDirectory] to upload multiple files. + +`uploadFile` expects a "Blob like" input, which can be a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) when running in a browser. On node.js, see the [`filesFromPath` library](https://github.com/web3-storage/files-from-path), which can load compatible objects from the local filesystem. + +`uploadDirectory` requires `File`-like objects instead of `Blob`s, as the file's `name` property is used to build the directory hierarchy. + +You can control the directory layout and create nested directory structures by using `/` delimited paths in your filenames: + +```js +const files = [ + new File(['some-file-content'], 'readme.md'), + new File(['import foo'], 'src/main.py'), + new File([someBinaryData], 'images/example.png'), +] + +const directoryCid = await client.storeDirectory(files) +``` + +In the example above, `directoryCid` resolves to an IPFS directory with the following layout: + +``` +. +├── images +│   └── example.png +├── readme.md +└── src + └── main.py +``` +### Alternate implementation options + +As discussed above, there are options outside the traditional client-server model that w3up supports. We how to use `w3up-client` to achieve these options in this section. + +#### Server-owned space with direct upload from end-user + +In this option, you (the developer) own your Space, but delegate permissions to your users to directly upload content to the service on your behalf. This isn’t completely “serverless” - you still need some infrastructure to create delegated UCAN tokens, but it’s minimal, and potentially saves a ton of bandwidth and overhead. + +> 🔜 _More detail coming soon!_ + +If you explore this option, contributions are welcome to these docs to help others in the future (and to reveal feature requests and bugs that we can patch to improve this implementation path)! Also feel free to chime into the discussion [here](https://github.com/web3-storage/w3up-client/discussions/92). + +#### User-owned + +In this option, your user owns their own Space. This option is the most web3-native (since your user owns their own identity, and thus their own data) and probably the most interesting one. It comes with a world of possibilities; for instance, instead of generating a new Space keypair for your user, you might look into using the existing keypair from their Metamask wallet or Apple Passkey. However, there are also likely edge cases that will appear early on for developers developing these types of apps that we haven’t had a chance to think much about yet, best-practices for various requirements (e.g., how much visibility do you want into user activity), and useful features that we could support. + +> 🔜 _More detail coming soon!_ + +If you explore this option, contributions are welcome to these docs to help others in the future (and to reveal feature requests and bugs that we can patch to improve this implementation path)! Also feel free to chime into the discussion [here](https://github.com/web3-storage/w3up-client/discussions/93). + +## API + +- [`create`](#create) +- `Client` + - [`uploadDirectory`](#uploaddirectory) + - [`uploadFile`](#uploadfile) + - [`uploadCAR`](#uploadcar) + - [`agent`](#agent) + - [`authorize`](#authorize) + - [`currentSpace`](#currentspace) + - [`setCurrentSpace`](#setcurrentspace) + - [`spaces`](#spaces) + - [`createSpace`](#createspace) + - [`registerSpace`](#registerSpace) + - [`addSpace`](#addSpace) + - [`proofs`](#proofs) + - [`addProof`](#addproof) + - [`delegations`](#delegations) + - [`createDelegation`](#createdelegation) + - [`capability.access.authorize`](#capabilityaccessauthorize) + - [`capability.access.claim`](#capabilityaccessclaim) + - [`capability.space.info`](#capabilityspaceinfo) + - [`capability.space.recover`](#capabilityspacerecover) + - [`capability.store.add`](#capabilitystoreadd) + - [`capability.store.list`](#capabilitystorelist) + - [`capability.store.remove`](#capabilitystoreremove) + - [`capability.upload.add`](#capabilityuploadadd) + - [`capability.upload.list`](#capabilityuploadlist) + - [`capability.upload.remove`](#capabilityuploadremove) +- [Types](#types) + - [`Capability`](#capability) + - [`CARMetadata`](#carmetadata) + - [`ClientFactoryOptions`](#clientfactoryoptions) + - [`Delegation`](#delegation) + - [`Driver`](#driver) + - [`ListResponse`](#listresponse) + - [`ServiceConf`](#serviceconf) + - [`ShardStoredCallback`](#shardstoredcallback) + - [`Space`](#space) + - [`StoreListResult`](#storelistresult) + - [`UploadListResult`](#uploadlistresult) + +--- + +### `create` + +```ts +function create (options?: ClientFactoryOptions): Promise +``` + +Create a new w3up client. + +If no backing store is passed one will be created that is appropriate for the environment. + +If the backing store is empty, a new signing key will be generated and persisted to the store. In the browser an unextractable RSA key will be generated by default. In other environments an Ed25519 key is generated. + +If the backing store already has data stored, it will be loaded and used. + +More information: [`ClientFactoryOptions`](#clientfactoryoptions) + +### `uploadDirectory` + +```ts +function uploadDirectory ( + files: File[], + options: { + retries?: number + signal?: AbortSignal + onShardStored?: ShardStoredCallback + shardSize?: number + concurrentRequests?: number + } = {} +): Promise +``` + +Uploads a directory of files to the service and returns the root data CID for the generated DAG. All files are added to a container directory, with paths in file names preserved. + +More information: [`ShardStoredCallback`](#shardstoredcallback) + +### `uploadFile` + +```ts +function uploadFile ( + file: Blob, + options: { + retries?: number + signal?: AbortSignal + onShardStored?: ShardStoredCallback + shardSize?: number + concurrentRequests?: number + } = {} +): Promise +``` + +Uploads a file to the service and returns the root data CID for the generated DAG. + +More information: [`ShardStoredCallback`](#shardstoredcallback) + +### `uploadCAR` + +```ts +function uploadCAR ( + car: Blob, + options: { + retries?: number + signal?: AbortSignal + onShardStored?: ShardStoredCallback + shardSize?: number + concurrentRequests?: number + rootCID?: CID + } = {} +): Promise +``` + +Uploads a CAR file to the service. The difference between this function and [capability.store.add](#capabilitystoreadd) is that the CAR file is automatically sharded and an "upload" is registered (see [`capability.upload.add`](#capabilityuploadadd)), linking the individual shards. Use the `onShardStored` callback to obtain the CIDs of the CAR file shards. + +More information: [`ShardStoredCallback`](#shardstoredcallback) + +### `agent` + +```ts +function agent (): Signer +``` + +The user agent. The agent is a signer - an entity that can sign UCANs with keys from a `Principal` using a signing algorithm. + +### `authorize` + +```ts +function authorize (email: string, options?: { signal?: AbortSignal }): Promise +``` + +Authorize the current agent to use capabilities granted to the passed email account. + +### `currentSpace` + +```ts +function currentSpace (): Space|undefined +``` + +The current space in use by the agent. + +### `setCurrentSpace` + +```ts +function setCurrentSpace (did: DID): Promise +``` + +Use a specific space. + +### `spaces` + +```ts +function spaces (): Space[] +``` + +Spaces available to this agent. + +### `createSpace` + +```ts +async function createSpace (name?: string): Promise +``` + +Create a new space with an optional name. + +### `registerSpace` + +```ts +async function registerSpace ( + email: string, + options?: { provider?: string, signal?: AbortSignal } +): Promise +``` + +Register the _current_ space with the service. + +By default, the provider is set to web3.storage w3up, but you can register instead of NFT.Storage w3up by setting `provider` to `did:web:nft.storage`. + +### `addSpace` + +```ts +async function addSpace (proof: Delegation): Promise +``` + +Add a space from a received proof. Proofs are delegations with an _audience_ matching the agent DID. + +### `proofs` + +```ts +function proofs (capabilities?: Capability[]): Delegation[] +``` + +Get all the proofs matching the capabilities. Proofs are delegations with an _audience_ matching the agent DID. + +### `addProof` + +```ts +function addProof (proof: Delegation): Promise +``` + +Add a proof to the agent. Proofs are delegations with an _audience_ matching the agent DID. Note: you probably want to use `addSpace` unless you know the delegation you received targets a resource _other_ than a w3 space. + +### `delegations` + +```ts +function delegations (capabilities?: Capability[]): Delegation[] +``` + +Get delegations created by the agent for others. Filtered optionally by capability. + +### `createDelegation` + +```ts +function createDelegation ( + audience: Principal, + abilities: string[], + options?: UCANOptions +): Promise +``` + +Create a delegation to the passed audience for the given abilities with the _current_ space as the resource. + +### `capability.access.authorize` + +```ts +function authorize ( + email: string, + options: { signal?: AbortSignal } = {} +): Promise +``` + +Authorize the current agent to use capabilities granted to the passed email account. + +### `capability.access.claim` + +```ts +function claim (): Promise[]> +``` + +Claim delegations granted to the account associated with this agent. Note: the received delegations are added to the agent's persistent store. + +### `capability.store.add` + +```ts +function add ( + car: Blob, + options: { retries?: number; signal?: AbortSignal } = {} +): Promise +``` + +Store a CAR file to the service. + +### `capability.store.list` + +```ts +function list ( + options: { retries?: number; signal?: AbortSignal } = {} +): Promise> +``` + +List CAR files stored in the current space. + +More information: [`StoreListResult`](#storelistresult), [`ListResponse`](#listresponse) + +### `capability.store.remove` + +```ts +function remove ( + link: CID, + options: { retries?: number; signal?: AbortSignal } = {} +): Promise +``` + +Remove a stored CAR file by CAR CID. + +### `capability.upload.add` + +```ts +function add ( + root: CID, + shards: CID[], + options: { retries?: number; signal?: AbortSignal } = {} +): Promise +``` + +Register a set of stored CAR files as an "upload" in the system. A DAG can be split between multiple CAR files. Calling this function allows multiple stored CAR files to be considered as a single upload. + +### `capability.upload.list` + +```ts +function list( + options: { retries?: number; signal?: AbortSignal } = {} +): Promise> +``` + +List uploads created in the current space. + +More information: [`UploadListResult`](#uploadlistresult), [`ListResponse`](#listresponse) + +### `capability.upload.remove` + +```ts +function remove( + link: CID, + options: { retries?: number; signal?: AbortSignal } = {} +): Promise +``` + +Remove a upload by root data CID. + +## Types + +### `Capability` + +An object describing a UCAN capability, which specifies what action the UCAN holder `can` perform `with` some resource. + +Defined by the [`@ipld/dag-ucan` package](https://github.com/ipld/js-dag-ucan). + +```ts +export interface Capability< + Can extends Ability = Ability, + With extends Resource = Resource, + Caveats extends unknown = unknown +> { + with: With + can: Can + nb?: Caveats +} + + +export type Ability = `${string}/${string}` | "*" + +export type Resource = `${string}:${string}` +``` + +The `can` field contains a string ability identifier, e.g. `store/add` or `space/info`. + +The `with` field contains a resource URI, often a `did:key` URI that identifies a Space. + +The optional `nb` (_nota bene_) field contains "caveats" that add supplemental information to a UCAN invocation or delegation. + +See [the capability spec](https://github.com/web3-storage/w3protocol/blob/main/spec/capabilities.md) for more information about capabilities and how they are defined in w3up services. + +### `CARMetadata` + +Metadata pertaining to a CAR file. + +```ts +export interface CARMetadata { + /** + * CAR version number. + */ + version: number + /** + * Root CIDs present in the CAR header. + */ + roots: CID[] + /** + * CID of the CAR file (not the data it contains). + */ + cid: CID + /** + * Size of the CAR file in bytes. + */ + size: number +} +``` + +### `ClientFactoryOptions` + +Options for constructing new `Client` instances. + +```ts +interface ClientFactoryOptions { + /** + * A storage driver that persists exported agent data. + */ + store?: Driver + /** + * Service DID and URL configuration. + */ + serviceConf?: ServiceConf +} +``` + +More information: [`Driver`](#driver), [`ServiceConf`](#serviceconf) + +### `Delegation` + +An in-memory view of a UCAN delegation, including proofs that can be used to invoke capabilities or delegate to other agents. + +```ts +import { Delegation as CoreDelegation } from '@ucanto/core/delegation' +export interface Delegation extends CoreDelegation { + /** + * User defined delegation metadata. + */ + meta(): Record +} +``` + +The `Delegation` type in `w3up-client` extends the `Delegation` type defined by [`ucanto`][ucanto]: + +```ts +export interface Delegation { + readonly root: UCANBlock + readonly blocks: Map + + readonly cid: UCANLink + readonly bytes: ByteView> + readonly data: UCAN.View + + asCID: UCANLink + + export(): IterableIterator + + issuer: UCAN.Principal + audience: UCAN.Principal + capabilities: C + expiration?: UCAN.UTCUnixTimestamp + notBefore?: UCAN.UTCUnixTimestamp + + nonce?: UCAN.Nonce + + facts: Fact[] + proofs: Proof[] + iterate(): IterableIterator +} +``` + +Delegations can be serialized by calling `export()` and piping the returned `Block` iterator into a `CarWriter` from the [`@ipld/car` package](https://www.npmjs.com/package/@ipld/car). + +### `Driver` + +Storage drivers can be obtained from [`@web3-storage/access/stores`](https://github.com/web3-storage/w3protocol/tree/main/packages/access-client/src/stores). They persist data created and managed by an agent. + +### `ListResponse` + +A paginated list of items. + +```ts +interface ListResponse { + cursor?: string + size: number + results: R[] +} +``` + +### `ServiceConf` + +Service DID and URL configuration. + +### `ShardStoredCallback` + +A function called after a DAG shard has been successfully stored by the service: + +```ts +type ShardStoredCallback = (meta: CARMetadata) => void +``` + +More information: [`CARMetadata`](#carmetadata) + +### `Space` + +An object representing a storage location. Spaces must be [registered](#registerspace) with the service before they can be used for storage. + +```ts +interface Space { + + /** + * The given space name. + */ + name(): string + + /** + * The DID of the space. + */ + did(): string + + /** + * Whether the space has been registered with the service. + */ + registered(): boolean + + + /** + * User defined space metadata. + */ + meta(): Record +} +``` + +### `StoreListResult` + +```ts +interface StoreListResult { + link: CID + size: number + origin?: CID +} +``` + +### `UploadListResult` + +```ts +interface UploadListResult { + root: CID + shards?: CID[] +} +``` + +## Contributing + +Feel free to join in. All welcome. Please [open an issue](https://github.com/web3-storage/w3up-client/issues)! + +## License + +Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3up-client/blob/main/license.md) + + +[w3up-cli-github]: https://github.com/web3-storage/w3up-cli +[access-client-github]: https://github.com/web3-storage/w3protocol/tree/main/packages/access-client +[upload-client-github]: https://github.com/web3-storage/w3protocol/tree/main/packages/upload-client +[elastic-ipfs]: https://github.com/elastic-ipfs/elastic-ipfs +[ucanto]: https://github.com/web3-storage/ucanto +[car-spec]: https://ipld.io/specs/transport/car/ +[web3storage-docs-cars]: https://web3.storage/docs/how-tos/work-with-car-files/ + +[docs]: https://web3-storage.github.io/w3up-client +[docs-Client]: https://web3-storage.github.io/w3up-client/classes/client.Client.html +[docs-Client#agent]: https://web3-storage.github.io/w3up-client/classes/client.Client.html#agent +[docs-Client#createSpace]: https://web3-storage.github.io/w3up-client/classes/client.Client.html#createSpace +[docs-Client#setCurrentSpace]: https://web3-storage.github.io/w3up-client/classes/client.Client.html#setCurrentSpace +[docs-Client#registerSpace]: https://web3-storage.github.io/w3up-client/classes/client.Client.html#registerSpace +[docs-Client#uploadFile]: https://web3-storage.github.io/w3up-client/classes/client.Client.html#uploadFile +[docs-Client#uploadDirectory]: https://web3-storage.github.io/w3up-client/classes/client.Client.html#uploadDirectory +[docs-Space]: https://web3-storage.github.io/w3up-client/classes/space.Space.html + +[docs-create]: #create +[docs-ClientFactoryOptions]: #clientfactoryoptions + +[access-docs-Agent]: https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html diff --git a/packages/w3up-client/docusaurus.config.cjs b/packages/w3up-client/docusaurus.config.cjs new file mode 100644 index 000000000..e3a36b4fd --- /dev/null +++ b/packages/w3up-client/docusaurus.config.cjs @@ -0,0 +1,36 @@ +// Minimal [Docusaurus](https://docusaurus.io) configuration to allow us +// to generate docusaurus-compatible markdown from typedoc output. + +const config = { + title: 'Web3.Storage Documentation', + tagline: 'The simple file storage service for IPFS and Filecoin', + url: 'https://docs.web3.storage', + baseUrl: '/', + onBrokenLinks: 'warn', + onBrokenMarkdownLinks: 'warn', + favicon: 'img/favicon.ico', + + // Even if you don't use internalization, you can use this field to set useful + // metadata like html lang. For example, if your site is Chinese, you may want + // to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'en', + locales: ['en'] + }, + + plugins: [ + [ + 'docusaurus-plugin-typedoc', + { + tsconfig: './tsconfig.json', + out: 'markdown', + sidebar: { + categoryLabel: 'w3up-client' + }, + includeExtension: false + } + ] + ] +} + +module.exports = config diff --git a/packages/w3up-client/package.json b/packages/w3up-client/package.json new file mode 100644 index 000000000..261033ea9 --- /dev/null +++ b/packages/w3up-client/package.json @@ -0,0 +1,131 @@ +{ + "name": "@web3-storage/w3up-client", + "version": "5.1.0", + "description": "Client for the w3up API", + "license": "Apache-2.0 OR MIT", + "type": "module", + "main": "src/index.js", + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "node": "./src/index.node.js", + "import": "./src/index.js" + }, + "./client": { + "types": "./dist/src/client.d.ts", + "import": "./src/client.js" + }, + "./capability/access": { + "types": "./dist/src/capability/access.d.ts", + "import": "./src/capability/access.js" + }, + "./capability/space": { + "types": "./dist/src/capability/space.d.ts", + "import": "./src/capability/space.js" + }, + "./capability/store": { + "types": "./dist/src/capability/store.d.ts", + "import": "./src/capability/store.js" + }, + "./capability/upload": { + "types": "./dist/src/capability/upload.d.ts", + "import": "./src/capability/upload.js" + }, + "./types": "./src/types.js" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "src", + "dist" + ], + "scripts": { + "lint": "standard", + "build": "tsc --build", + "prepare": "npm run build", + "test": "npm-run-all -p -r mock test:all", + "test:all": "run-s test:node test:browser", + "test:node": "hundreds -r html -r text mocha 'test/**/!(*.browser).test.js' -n experimental-vm-modules -n no-warnings -n stack-trace-limit=1000", + "test:browser": "playwright-test 'test/**/!(*.node).test.js'", + "mock": "run-p mock:*", + "mock:bucket-200": "PORT=9200 STATUS=200 node test/helpers/bucket-server.js", + "rc": "npm version prerelease --preid rc", + "docs": "npm run build && typedoc --out docs-generated", + "docs:markdown": "npm run build && docusaurus generate-typedoc" + }, + "dependencies": { + "@ipld/dag-ucan": "^3.0.1", + "@ucanto/client": "^5.1.0", + "@ucanto/core": "^5.1.0", + "@ucanto/interface": "^6.0.0", + "@ucanto/principal": "^5.1.0", + "@ucanto/transport": "^5.1.0", + "@web3-storage/access": "workspace:^", + "@web3-storage/capabilities": "workspace:^", + "@web3-storage/upload-client": "workspace:^" + }, + "devDependencies": { + "@docusaurus/core": "^2.2.0", + "@ipld/car": "^5.1.1", + "@ucanto/server": "^6.1.0", + "assert": "^2.0.0", + "c8": "^7.13.0", + "docusaurus-plugin-typedoc": "^0.18.0", + "hundreds": "^0.0.9", + "mocha": "^10.1.0", + "multiformats": "^11.0.0", + "npm-run-all": "^4.1.5", + "playwright-test": "^8.1.1", + "standard": "^17.0.0", + "typedoc": "^0.23.24", + "typedoc-plugin-markdown": "^3.14.0", + "typedoc-plugin-missing-exports": "^1.0.0", + "typescript": "^4.8.3" + }, + "directories": { + "test": "test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/web3-storage/w3up-client.git" + }, + "keywords": [ + "web3", + "storage", + "upload", + "store", + "DAG", + "IPLD", + "UCAN", + "IPFS" + ], + "author": "DAG House", + "bugs": { + "url": "https://github.com/web3-storage/w3up-client/issues" + }, + "homepage": "https://github.com/web3-storage/w3up-client#readme", + "standard": { + "env": [ + "browser", + "mocha" + ] + } +} diff --git a/packages/w3up-client/src/base.js b/packages/w3up-client/src/base.js new file mode 100644 index 000000000..c7f2f4829 --- /dev/null +++ b/packages/w3up-client/src/base.js @@ -0,0 +1,46 @@ +import { Agent } from '@web3-storage/access/agent' +import { serviceConf } from './service.js' + +export class Base { + /** + * @type {Agent} + * @protected + */ + _agent + + /** + * @type {import('./types').ServiceConf} + * @protected + */ + _serviceConf + + /** + * @param {import('@web3-storage/access').AgentData} agentData + * @param {object} [options] + * @param {import('./types').ServiceConf} [options.serviceConf] + */ + constructor (agentData, options = {}) { + this._serviceConf = options.serviceConf ?? serviceConf + this._agent = new Agent(agentData, { + servicePrincipal: this._serviceConf.access.id, + // @ts-expect-error I know but it will be HTTP for the forseeable. + url: this._serviceConf.access.channel.url, + connection: this._serviceConf.access + }) + } + + /** + * @protected + * @param {import('./types').Ability[]} abilities + */ + async _invocationConfig (abilities) { + const resource = this._agent.currentSpace() + if (!resource) { + throw new Error('missing current space: use createSpace() or setCurrentSpace()') + } + const issuer = this._agent.issuer + const proofs = await this._agent.proofs(abilities.map(can => ({ can, with: resource }))) + const audience = this._serviceConf.upload.id + return { issuer, with: resource, proofs, audience } + } +} diff --git a/packages/w3up-client/src/capability/access.js b/packages/w3up-client/src/capability/access.js new file mode 100644 index 000000000..9ddafacca --- /dev/null +++ b/packages/w3up-client/src/capability/access.js @@ -0,0 +1,28 @@ +import { Base } from '../base.js' +import { claimAccess, authorizeWithSocket } from '@web3-storage/access/agent' + +/** + * Client for interacting with the `access/*` capabilities. + */ +export class AccessClient extends Base { + /* c8 ignore start - testing websocket code is hard */ + /** + * Authorize the current agent to use capabilities granted to the passed + * email account. + * + * @param {`${string}@${string}`} email + * @param {object} [options] + * @param {AbortSignal} [options.signal] + */ + async authorize (email, options) { + return authorizeWithSocket(this._agent, email, options) + } + /* c8 ignore stop */ + + /** + * Claim delegations granted to the account associated with this agent. + */ + async claim () { + return claimAccess(this._agent, this._agent.issuer.did(), { addProofs: true }) + } +} diff --git a/packages/w3up-client/src/capability/space.js b/packages/w3up-client/src/capability/space.js new file mode 100644 index 000000000..e3c0e693f --- /dev/null +++ b/packages/w3up-client/src/capability/space.js @@ -0,0 +1,25 @@ +import { Base } from '../base.js' + +/** + * Client for interacting with the `space/*` capabilities. + */ +export class SpaceClient extends Base { + /** + * Get information about a space. + * + * @param {import('../types').DID} space DID of the space to retrieve info about. + */ + async info (space) { + return await this._agent.getSpaceInfo(space) + } + + /** + * Recover the current space. + * + * @param {string} email Email address to send recovery emaail to. + */ + /* c8 ignore next 3 */ + async recover (email) { + return await this._agent.recover(email) + } +} diff --git a/packages/w3up-client/src/capability/store.js b/packages/w3up-client/src/capability/store.js new file mode 100644 index 000000000..36cc38d28 --- /dev/null +++ b/packages/w3up-client/src/capability/store.js @@ -0,0 +1,43 @@ +import { Store } from '@web3-storage/upload-client' +import { Store as StoreCapabilities } from '@web3-storage/capabilities' +import { Base } from '../base.js' + +/** + * Client for interacting with the `store/*` capabilities. + */ +export class StoreClient extends Base { + /** + * Store a DAG encoded as a CAR file. + * + * @param {Blob} car CAR file data. + * @param {import('../types').RequestOptions} [options] + */ + async add (car, options = {}) { + const conf = await this._invocationConfig([StoreCapabilities.add.can]) + options.connection = this._serviceConf.upload + return Store.add(conf, car, options) + } + + /** + * List CAR files stored to the resource. + * + * @param {import('../types').ListRequestOptions} [options] + */ + async list (options = {}) { + const conf = await this._invocationConfig([StoreCapabilities.add.can]) + options.connection = this._serviceConf.upload + return Store.list(conf, options) + } + + /** + * Remove a stored CAR file by CAR CID. + * + * @param {import('../types').CARLink} link CID of CAR file to remove. + * @param {import('../types').RequestOptions} [options] + */ + async remove (link, options = {}) { + const conf = await this._invocationConfig([StoreCapabilities.remove.can]) + options.connection = this._serviceConf.upload + return Store.remove(conf, link, options) + } +} diff --git a/packages/w3up-client/src/capability/upload.js b/packages/w3up-client/src/capability/upload.js new file mode 100644 index 000000000..d06e0e0bd --- /dev/null +++ b/packages/w3up-client/src/capability/upload.js @@ -0,0 +1,44 @@ +import { Upload } from '@web3-storage/upload-client' +import { Upload as UploadCapabilities } from '@web3-storage/capabilities' +import { Base } from '../base.js' + +/** + * Client for interacting with the `upload/*` capabilities. + */ +export class UploadClient extends Base { + /** + * Register an "upload" to the resource. + * + * @param {import('../types').UnknownLink} root Root data CID for the DAG that was stored. + * @param {import('../types').CARLink[]} shards CIDs of CAR files that contain the DAG. + * @param {import('../types').RequestOptions} [options] + */ + async add (root, shards, options = {}) { + const conf = await this._invocationConfig([UploadCapabilities.add.can]) + options.connection = this._serviceConf.upload + return Upload.add(conf, root, shards, options) + } + + /** + * List uploads registered to the resource. + * + * @param {import('../types').ListRequestOptions} [options] + */ + async list (options = {}) { + const conf = await this._invocationConfig([UploadCapabilities.list.can]) + options.connection = this._serviceConf.upload + return Upload.list(conf, options) + } + + /** + * Remove an upload by root data CID. + * + * @param {import('../types').UnknownLink} root Root data CID to remove. + * @param {import('../types').RequestOptions} [options] + */ + async remove (root, options = {}) { + const conf = await this._invocationConfig([UploadCapabilities.remove.can]) + options.connection = this._serviceConf.upload + return Upload.remove(conf, root, options) + } +} diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js new file mode 100644 index 000000000..ab26946ef --- /dev/null +++ b/packages/w3up-client/src/client.js @@ -0,0 +1,213 @@ +import { uploadFile, uploadDirectory, uploadCAR } from '@web3-storage/upload-client' +import { Store as StoreCapabilities, Upload as UploadCapabilities } from '@web3-storage/capabilities' +import { Base } from './base.js' +import { Space } from './space.js' +import { Delegation as AgentDelegation } from './delegation.js' +import { StoreClient } from './capability/store.js' +import { UploadClient } from './capability/upload.js' +import { SpaceClient } from './capability/space.js' +import { AccessClient } from './capability/access.js' + +export class Client extends Base { + /** + * @param {import('@web3-storage/access').AgentData} agentData + * @param {object} [options] + * @param {import('./types').ServiceConf} [options.serviceConf] + */ + constructor (agentData, options) { + super(agentData, options) + this.capability = { + access: new AccessClient(agentData, options), + store: new StoreClient(agentData, options), + upload: new UploadClient(agentData, options), + space: new SpaceClient(agentData, options) + } + } + + /* c8 ignore start - testing websockets is hard */ + /** + * Authorize the current agent to use capabilities granted to the passed + * email account. + * + * @param {`${string}@${string}`} email + * @param {object} [options] + * @param {AbortSignal} [options.signal] + */ + async authorize (email, options) { + await this.capability.access.authorize(email, options) + } + /* c8 ignore stop */ + + /** + * Uploads a file to the service and returns the root data CID for the + * generated DAG. + * + * @param {import('./types').BlobLike} file File data. + * @param {import('./types').UploadOptions} [options] + */ + async uploadFile (file, options = {}) { + const conf = await this._invocationConfig([StoreCapabilities.add.can, UploadCapabilities.add.can]) + options.connection = this._serviceConf.upload + return uploadFile(conf, file, options) + } + + /** + * Uploads a directory of files to the service and returns the root data CID + * for the generated DAG. All files are added to a container directory, with + * paths in file names preserved. + * + * @param {import('./types').FileLike[]} files File data. + * @param {import('./types').UploadOptions} [options] + */ + async uploadDirectory (files, options = {}) { + const conf = await this._invocationConfig([StoreCapabilities.add.can, UploadCapabilities.add.can]) + options.connection = this._serviceConf.upload + return uploadDirectory(conf, files, options) + } + + /** + * Uploads a CAR file to the service. + * + * The difference between this function and `capability.store.add` is that the + * CAR file is automatically sharded and an "upload" is registered, linking + * the individual shards (see `capability.upload.add`). + * + * Use the `onShardStored` callback to obtain the CIDs of the CAR file shards. + * + * @param {import('./types').BlobLike} car CAR file. + * @param {import('./types').UploadOptions} [options] + */ + async uploadCAR (car, options = {}) { + const conf = await this._invocationConfig([StoreCapabilities.add.can, UploadCapabilities.add.can]) + options.connection = this._serviceConf.upload + return uploadCAR(conf, car, options) + } + + /** + * Return the default provider. + */ + defaultProvider () { + return this._agent.connection.id.did() + } + + /** + * The current user agent (this device). + */ + agent () { + return this._agent.issuer + } + + /** + * The current space. + */ + currentSpace () { + const did = this._agent.currentSpace() + return did ? new Space(did, this._agent.spaces.get(did)) : undefined + } + + /** + * Use a specific space. + * + * @param {import('./types').DID<'key'>} did + */ + async setCurrentSpace (did) { + await this._agent.setCurrentSpace(did) + } + + /** + * Spaces available to this agent. + */ + spaces () { + return [...this._agent.spaces].map(([did, meta]) => new Space(did, meta)) + } + + /** + * Create a new space with an optional name. + * + * @param {string} [name] + */ + async createSpace (name) { + const { did, meta } = await this._agent.createSpace(name) + return new Space(did, meta) + } + + /* c8 ignore start - hard to test this without authorize tests which require websockets */ + /** + * Register the _current_ space with the service. + * + * @param {string} email + * @param {object} [options] + * @param {import('./types').DID<'web'>} [options.provider] + * @param {AbortSignal} [options.signal] + */ + async registerSpace (email, options = {}) { + options.provider = options.provider ?? /** @type {import('./types').DID<'web'>} */(this.defaultProvider()) + await this._agent.registerSpace(email, options) + } + /* c8 ignore stop */ + + /** + * Add a space from a received proof. + * + * @param {import('./types').Delegation} proof + */ + async addSpace (proof) { + const { did, meta } = await this._agent.importSpaceFromDelegation(proof) + return new Space(did, meta) + } + + /** + * Get all the proofs matching the capabilities. + * + * Proofs are delegations with an _audience_ matching the agent DID. + * + * @param {import('./types').Capability[]} [caps] Capabilities to + * filter by. Empty or undefined caps with return all the proofs. + */ + proofs (caps) { + return this._agent.proofs(caps) + } + + /** + * Add a proof to the agent. Proofs are delegations with an _audience_ + * matching the agent DID. + * + * @param {import('./types').Delegation} proof + */ + async addProof (proof) { + await this._agent.addProof(proof) + } + + /** + * Get delegations created by the agent for others. + * + * @param {import('./types').Capability[]} [caps] Capabilities to + * filter by. Empty or undefined caps with return all the delegations. + */ + delegations (caps) { + const delegations = [] + for (const { delegation, meta } of this._agent.delegationsWithMeta(caps)) { + delegations.push(new AgentDelegation(delegation.root, delegation.blocks, meta)) + } + return delegations + } + + /** + * Create a delegation to the passed audience for the given abilities with + * the _current_ space as the resource. + * + * @param {import('./types').Principal} audience + * @param {import('./types').Abilities[]} abilities + * @param {Omit & { audienceMeta?: import('./types').AgentMeta }} [options] + */ + async createDelegation (audience, abilities, options = {}) { + const audienceMeta = options.audienceMeta ?? { name: 'agent', type: 'device' } + const { root, blocks } = await this._agent.delegate({ + ...options, + abilities, + audience, + audienceMeta + }) + return new AgentDelegation(root, blocks, { audience: audienceMeta }) + } +} diff --git a/packages/w3up-client/src/delegation.js b/packages/w3up-client/src/delegation.js new file mode 100644 index 000000000..bc14d81e2 --- /dev/null +++ b/packages/w3up-client/src/delegation.js @@ -0,0 +1,27 @@ +import { Delegation as CoreDelegation } from '@ucanto/core/delegation' + +/** + * @template {import('./types').Capabilities} C + * @extends {CoreDelegation} + */ +export class Delegation extends CoreDelegation { + /** @type {Record} */ + #meta + + /** + * @param {import('./types').UCANBlock} root + * @param {Map} [blocks] + * @param {Record} [meta] + */ + constructor (root, blocks, meta = {}) { + super(root, blocks) + this.#meta = meta + } + + /** + * User defined delegation metadata. + */ + meta () { + return this.#meta + } +} diff --git a/packages/w3up-client/src/index.js b/packages/w3up-client/src/index.js new file mode 100644 index 000000000..c5ccf9b79 --- /dev/null +++ b/packages/w3up-client/src/index.js @@ -0,0 +1,36 @@ +/** + * The main entry point for the `@web3-storage/w3up-client` package. + * + * Use the static {@link create} function to create a new {@link Client} object. + * + * @module + */ +import { AgentData } from '@web3-storage/access/agent' +import { StoreIndexedDB } from '@web3-storage/access/stores/store-indexeddb' +import { generate } from '@ucanto/principal/rsa' +import { Client } from './client.js' + +/** + * Create a new w3up client. + * + * If no backing store is passed one will be created that is appropriate for + * the environment. + * + * If the backing store is empty, a new signing key will be generated and + * persisted to the store. In the browser an unextractable RSA key will be + * generated by default. In other environments an Ed25519 key is generated. + * + * If the backing store already has data stored, it will be loaded and used. + * + * @type {import('./types').ClientFactory} + */ +export async function create (options = {}) { + const store = options.store ?? new StoreIndexedDB('w3up-client') + const raw = await store.load() + if (raw) return new Client(AgentData.fromExport(raw, { store }), options) + const principal = await generate() + const data = await AgentData.create({ principal }, { store }) + return new Client(data, options) +} + +export { Client } diff --git a/packages/w3up-client/src/index.node.js b/packages/w3up-client/src/index.node.js new file mode 100644 index 000000000..d93fada56 --- /dev/null +++ b/packages/w3up-client/src/index.node.js @@ -0,0 +1,33 @@ +/** + * @hidden + * @module + */ +import { AgentData } from '@web3-storage/access/agent' +import { StoreConf } from '@web3-storage/access/stores/store-conf' +import { generate } from '@ucanto/principal/ed25519' +import { Client } from './client.js' + +/** + * Create a new w3up client. + * + * If no backing store is passed one will be created that is appropriate for + * the environment. + * + * If the backing store is empty, a new signing key will be generated and + * persisted to the store. In the browser an unextractable RSA key will be + * generated by default. In other environments an Ed25519 key is generated. + * + * If the backing store already has data stored, it will be loaded and used. + * + * @type {import('./types').ClientFactory} + */ +export async function create (options = {}) { + const store = options.store ?? new StoreConf({ profile: 'w3up-client' }) + const raw = await store.load() + if (raw) return new Client(AgentData.fromExport(raw, { store }), options) + const principal = await generate() + const data = await AgentData.create({ principal }, { store }) + return new Client(data, options) +} + +export { Client } diff --git a/packages/w3up-client/src/service.js b/packages/w3up-client/src/service.js new file mode 100644 index 000000000..83485ab17 --- /dev/null +++ b/packages/w3up-client/src/service.js @@ -0,0 +1,35 @@ +import { connect } from '@ucanto/client' +import { CAR, CBOR, HTTP } from '@ucanto/transport' +import * as DID from '@ipld/dag-ucan/did' + +export const accessServiceURL = new URL('https://access.web3.storage') +export const accessServicePrincipal = DID.parse('did:web:web3.storage') + +export const accessServiceConnection = connect({ + id: accessServicePrincipal, + encoder: CAR, + decoder: CBOR, + channel: HTTP.open({ + url: accessServiceURL, + method: 'POST' + }) +}) + +export const uploadServiceURL = new URL('https://up.web3.storage') +export const uploadServicePrincipal = DID.parse('did:web:web3.storage') + +export const uploadServiceConnection = connect({ + id: uploadServicePrincipal, + encoder: CAR, + decoder: CBOR, + channel: HTTP.open({ + url: uploadServiceURL, + method: 'POST' + }) +}) + +/** @type {import('./types').ServiceConf} */ +export const serviceConf = { + access: accessServiceConnection, + upload: uploadServiceConnection +} diff --git a/packages/w3up-client/src/space.js b/packages/w3up-client/src/space.js new file mode 100644 index 000000000..7cd99b690 --- /dev/null +++ b/packages/w3up-client/src/space.js @@ -0,0 +1,43 @@ +export class Space { + /** @type {import('./types').DID} */ + #did + /** @type {Record} */ + #meta + + /** + * @param {import('./types').DID} did + * @param {Record} meta + */ + constructor (did, meta = {}) { + this.#did = did + this.#meta = meta + } + + /** + * The given space name. + */ + name () { + return this.#meta.name + } + + /** + * The DID of the space. + */ + did () { + return this.#did + } + + /** + * Whether the space has been registered with the service. + */ + registered () { + return Boolean(this.#meta.isRegistered) + } + + /** + * User defined space metadata. + */ + meta () { + return this.#meta + } +} diff --git a/packages/w3up-client/src/types.ts b/packages/w3up-client/src/types.ts new file mode 100644 index 000000000..433b0c7d2 --- /dev/null +++ b/packages/w3up-client/src/types.ts @@ -0,0 +1,82 @@ +import { Driver } from '@web3-storage/access/drivers/types' +import { Service as AccessService, AgentDataExport } from '@web3-storage/access/types' +import { Service as UploadService } from '@web3-storage/upload-client/types' +import { ConnectionView } from '@ucanto/interface' +import { Client } from './client' + +export interface ServiceConf { + access: ConnectionView + upload: ConnectionView +} + +export interface ClientFactoryOptions { + /** + * A storage driver that persists exported agent data. + */ + store?: Driver + /** + * Service DID and URL configuration. + */ + serviceConf?: ServiceConf +} + +export interface ClientFactory { + (options?: ClientFactoryOptions): Promise +} + +export { Client } from './client' + +export type { UnknownLink } from 'multiformats' + +export type { + DID, + Principal, + Delegation, + Ability, + Capability, + Capabilities, + UCANOptions, + UCANBlock, + Block, + ConnectionView +} from '@ucanto/interface' + +export type { + Abilities, + StoreAdd, + StoreList, + StoreRemove, + UploadAdd, + UploadList, + UploadRemove, +} from '@web3-storage/capabilities/types' + +export type { + AgentDataModel, + AgentDataExport, + AgentMeta, + DelegationMeta +} from '@web3-storage/access/types' + +export type { + StoreAddResponse, + UploadAddResponse, + ListResponse, + StoreListResult, + UploadListResult, + AnyLink, + CARLink, + CARFile, + CARMetadata, + Retryable, + Abortable, + Connectable, + Pageable, + RequestOptions, + ListRequestOptions, + ShardingOptions, + ShardStoringOptions, + UploadOptions, + FileLike, + BlobLike +} from '@web3-storage/upload-client/types' diff --git a/packages/w3up-client/static/docs.css b/packages/w3up-client/static/docs.css new file mode 100644 index 000000000..978385383 --- /dev/null +++ b/packages/w3up-client/static/docs.css @@ -0,0 +1,12 @@ +a[href*='_internal_.html'] + ul { + display: none; +} + +.tsd-kind-module > ul { + display: none; + overflow: hidden; +} + +.tsd-kind-module.selected > ul { + display: block; +} diff --git a/packages/w3up-client/test/capability/access.test.js b/packages/w3up-client/test/capability/access.test.js new file mode 100644 index 000000000..72991c0e7 --- /dev/null +++ b/packages/w3up-client/test/capability/access.test.js @@ -0,0 +1,47 @@ +import assert from 'assert' +import { create as createServer, provide } from '@ucanto/server' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/transport/cbor' +import * as Signer from '@ucanto/principal/ed25519' +import * as AccessCapabilities from '@web3-storage/capabilities/access' +import { AgentData } from '@web3-storage/access/agent' +import { mockService, mockServiceConf } from '../helpers/mocks.js' +import { Client } from '../../src/client.js' + +describe('AccessClient', () => { + describe('claim', () => { + it('should claim delegations', async () => { + const service = mockService({ + access: { + claim: provide(AccessCapabilities.claim, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, AccessCapabilities.claim.can) + return { + delegations: [] + } + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const delegations = await alice.capability.access.claim() + + assert(service.access.claim.called) + assert.equal(service.access.claim.callCount, 1) + assert.deepEqual(delegations, []) + }) + }) +}) diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js new file mode 100644 index 000000000..a46ada294 --- /dev/null +++ b/packages/w3up-client/test/capability/space.test.js @@ -0,0 +1,60 @@ +import assert from 'assert' +import { create as createServer, provide } from '@ucanto/server' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/transport/cbor' +import * as Signer from '@ucanto/principal/ed25519' +import * as SpaceCapabilities from '@web3-storage/capabilities/space' +import { AgentData } from '@web3-storage/access/agent' +import { mockService, mockServiceConf } from '../helpers/mocks.js' +import { Client } from '../../src/client.js' + +describe('SpaceClient', () => { + describe('info', () => { + it('should retrieve space info', async () => { + const service = mockService({ + space: { + info: provide(SpaceCapabilities.info, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, SpaceCapabilities.info.can) + assert.equal(invCap.with, space.did()) + return { + did: space.did(), + agent: alice.agent().did(), + email: 'mailto:alice@example.com', + product: 'product:test', + updated_at: '', + inserted_at: '' + } + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const info = await alice.capability.space.info(space.did()) + + assert(service.space.info.called) + assert.equal(service.space.info.callCount, 1) + + assert.equal(info.did, space.did()) + assert.equal(info.agent, alice.agent().did()) + assert.equal(info.email, 'mailto:alice@example.com') + assert.equal(info.product, 'product:test') + }) + }) +}) diff --git a/packages/w3up-client/test/capability/store.test.js b/packages/w3up-client/test/capability/store.test.js new file mode 100644 index 000000000..c59a7faf3 --- /dev/null +++ b/packages/w3up-client/test/capability/store.test.js @@ -0,0 +1,145 @@ +import assert from 'assert' +import { create as createServer, provide } from '@ucanto/server' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/transport/cbor' +import * as Signer from '@ucanto/principal/ed25519' +import { Store as StoreCapabilities } from '@web3-storage/capabilities' +import { AgentData } from '@web3-storage/access/agent' +import { randomCAR } from '../helpers/random.js' +import { mockService, mockServiceConf } from '../helpers/mocks.js' +import { Client } from '../../src/client.js' + +describe('StoreClient', () => { + describe('add', () => { + it('should store a CAR file', async () => { + const service = mockService({ + store: { + add: provide(StoreCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, StoreCapabilities.add.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return { + status: 'upload', + headers: { 'x-test': 'true' }, + url: 'http://localhost:9200' + } + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const car = await randomCAR(128) + const carCID = await alice.capability.store.add(car) + + assert(service.store.add.called) + assert.equal(service.store.add.callCount, 1) + + assert.equal(carCID.toString(), car.cid.toString()) + }) + }) + + describe('list', () => { + it('should list stored CARs', async () => { + const cursor = 'test' + const page = { + cursor, + size: 1, + results: [ + { + link: (await randomCAR(128)).cid, + size: 123 + } + ] + } + + const service = mockService({ + store: { + list: provide(StoreCapabilities.list, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, StoreCapabilities.list.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return page + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const res = await alice.capability.store.list() + + assert(service.store.list.called) + assert.equal(service.store.list.callCount, 1) + + assert.equal(res.cursor, cursor) + assert.equal(res.results[0].link.toString(), page.results[0].link.toString()) + }) + }) + + describe('remove', () => { + it('should remove a stored CAR', async () => { + const service = mockService({ + store: { + remove: provide(StoreCapabilities.remove, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, StoreCapabilities.remove.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return null + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + await alice.capability.store.remove((await randomCAR(128)).cid) + + assert(service.store.remove.called) + assert.equal(service.store.remove.callCount, 1) + }) + }) +}) diff --git a/packages/w3up-client/test/capability/upload.test.js b/packages/w3up-client/test/capability/upload.test.js new file mode 100644 index 000000000..cf2244200 --- /dev/null +++ b/packages/w3up-client/test/capability/upload.test.js @@ -0,0 +1,149 @@ +import assert from 'assert' +import { create as createServer, provide } from '@ucanto/server' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/transport/cbor' +import * as Signer from '@ucanto/principal/ed25519' +import { Upload as UploadCapabilities } from '@web3-storage/capabilities' +import { AgentData } from '@web3-storage/access/agent' +import { randomCAR } from '../helpers/random.js' +import { mockService, mockServiceConf } from '../helpers/mocks.js' +import { Client } from '../../src/client.js' + +describe('StoreClient', () => { + describe('add', () => { + it('should register an upload', async () => { + const car = await randomCAR(128) + + const res = { + root: car.roots[0], + shards: [car.cid] + } + + const service = mockService({ + upload: { + add: provide(UploadCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, UploadCapabilities.add.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + assert.equal(String(invCap.nb?.root), car.roots[0].toString()) + assert.equal(invCap.nb?.shards?.length, 1) + assert.equal(String(invCap.nb?.shards?.[0]), car.cid.toString()) + return res + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + await alice.capability.upload.add(car.roots[0], [car.cid]) + + assert(service.upload.add.called) + assert.equal(service.upload.add.callCount, 1) + }) + }) + + describe('list', () => { + it('should list uploads', async () => { + const car = await randomCAR(128) + const cursor = 'test' + const page = { + cursor, + size: 1, + results: [ + { + root: car.roots[0], + shards: [car.cid] + } + ] + } + + const service = mockService({ + upload: { + list: provide(UploadCapabilities.list, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, UploadCapabilities.list.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return page + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const res = await alice.capability.upload.list() + + assert(service.upload.list.called) + assert.equal(service.upload.list.callCount, 1) + + assert.equal(res.cursor, cursor) + assert.equal(res.results[0].root.toString(), page.results[0].root.toString()) + }) + }) + + describe('remove', () => { + it('should remove an upload', async () => { + const service = mockService({ + upload: { + remove: provide(UploadCapabilities.remove, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, UploadCapabilities.remove.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return null + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + await alice.capability.upload.remove((await randomCAR(128)).roots[0]) + + assert(service.upload.remove.called) + assert.equal(service.upload.remove.callCount, 1) + }) + }) +}) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js new file mode 100644 index 000000000..01def06f8 --- /dev/null +++ b/packages/w3up-client/test/client.test.js @@ -0,0 +1,317 @@ +import assert from 'assert' +import { create as createServer, provide } from '@ucanto/server' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/transport/cbor' +import * as Signer from '@ucanto/principal/ed25519' +import * as StoreCapabilities from '@web3-storage/capabilities/store' +import * as UploadCapabilities from '@web3-storage/capabilities/upload' +import { AgentData } from '@web3-storage/access/agent' +import { randomBytes, randomCAR } from './helpers/random.js' +import { toCAR } from './helpers/car.js' +import { mockService, mockServiceConf } from './helpers/mocks.js' +import { File } from './helpers/shims.js' +import { Client } from '../src/client.js' + +describe('Client', () => { + describe('uploadFile', () => { + it('should upload a file to the service', async () => { + const bytes = await randomBytes(128) + const file = new Blob([bytes]) + const expectedCar = await toCAR(bytes) + + /** @type {import('@web3-storage/upload-client/types').CARLink|undefined} */ + let carCID + + const service = mockService({ + store: { + add: provide(StoreCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, StoreCapabilities.add.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return { + status: 'upload', + headers: { 'x-test': 'true' }, + url: 'http://localhost:9200' + } + }) + }, + upload: { + add: provide(UploadCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, UploadCapabilities.add.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + assert.equal(invCap.nb?.shards?.length, 1) + assert.equal(String(invCap.nb?.shards?.[0]), carCID?.toString()) + return { + root: expectedCar.roots[0], + shards: [expectedCar.cid] + } + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const dataCID = await alice.uploadFile(file, { + onShardStored: meta => { carCID = meta.cid } + }) + + assert(service.store.add.called) + assert.equal(service.store.add.callCount, 1) + assert(service.upload.add.called) + assert.equal(service.upload.add.callCount, 1) + + assert.equal(carCID?.toString(), expectedCar.cid.toString()) + assert.equal(dataCID.toString(), expectedCar.roots[0].toString()) + }) + + it('should not allow upload without a current space', async () => { + const alice = new Client(await AgentData.create()) + + const bytes = await randomBytes(128) + const file = new Blob([bytes]) + + await assert.rejects(alice.uploadFile(file), { message: 'missing current space: use createSpace() or setCurrentSpace()' }) + }) + }) + + describe('uploadDirectory', () => { + it('should upload a directory to the service', async () => { + const files = [ + new File([await randomBytes(128)], '1.txt'), + new File([await randomBytes(32)], '2.txt') + ] + + /** @type {import('@web3-storage/upload-client/types').CARLink|undefined} */ + let carCID + + const service = mockService({ + store: { + add: provide(StoreCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, StoreCapabilities.add.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + return { + status: 'upload', + headers: { 'x-test': 'true' }, + url: 'http://localhost:9200' + } + }) + }, + upload: { + add: provide(UploadCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, UploadCapabilities.add.can) + assert.equal(invCap.with, alice.currentSpace()?.did()) + assert.equal(invCap.nb?.shards?.length, 1) + if (!invCap.nb) throw new Error('nb must be present') + return invCap.nb + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const dataCID = await alice.uploadDirectory(files, { + onShardStored: meta => { carCID = meta.cid } + }) + + assert(service.store.add.called) + assert.equal(service.store.add.callCount, 1) + assert(service.upload.add.called) + assert.equal(service.upload.add.callCount, 1) + + assert(carCID) + assert(dataCID) + }) + }) + + describe('uploadCAR', () => { + it('uploads a CAR file to the service', async () => { + const car = await randomCAR(32) + + /** @type {import('../src/types').CARLink?} */ + let carCID + + const service = mockService({ + store: { + add: provide(StoreCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, StoreCapabilities.add.can) + assert.equal(invCap.with, space.did()) + return { + status: 'upload', + headers: { 'x-test': 'true' }, + url: 'http://localhost:9200' + } + }) + }, + upload: { + add: provide(UploadCapabilities.add, ({ invocation }) => { + assert.equal(invocation.issuer.did(), alice.agent().did()) + assert.equal(invocation.capabilities.length, 1) + const invCap = invocation.capabilities[0] + assert.equal(invCap.can, UploadCapabilities.add.can) + assert.equal(invCap.with, space.did()) + if (!invCap.nb) throw new Error('nb must be present') + assert.equal(invCap.nb.shards?.length, 1) + assert.equal(invCap.nb.shards[0].toString(), carCID.toString()) + return invCap.nb + }) + } + }) + + const server = createServer({ + id: await Signer.generate(), + service, + decoder: CAR, + encoder: CBOR + }) + + const alice = new Client( + await AgentData.create(), + { serviceConf: await mockServiceConf(server) } + ) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + await alice.uploadCAR(car, { onShardStored: meta => { carCID = meta.cid } }) + + assert(service.store.add.called) + assert.equal(service.store.add.callCount, 1) + assert(service.upload.add.called) + assert.equal(service.upload.add.callCount, 1) + + assert(carCID) + assert.equal(carCID.toString(), car.cid.toString()) + }) + }) + + describe('currentSpace', () => { + it('should return undefined or space', async () => { + const alice = new Client(await AgentData.create()) + + const current0 = alice.currentSpace() + assert(current0 == null) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const current1 = alice.currentSpace() + assert(current1) + assert.equal(current1.did(), space.did()) + }) + }) + + describe('spaces', () => { + it('should get agent spaces', async () => { + const alice = new Client(await AgentData.create()) + + const name = `space-${Date.now()}` + const space = await alice.createSpace(name) + + const spaces = alice.spaces() + assert.equal(spaces.length, 1) + assert.equal(spaces[0].did(), space.did()) + assert.equal(spaces[0].name(), name) + }) + + it('should add space', async () => { + const alice = new Client(await AgentData.create()) + const bob = new Client(await AgentData.create()) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const delegation = await alice.createDelegation(bob.agent(), ['*']) + + assert.equal(bob.spaces().length, 0) + await bob.addSpace(delegation) + assert.equal(bob.spaces().length, 1) + + const spaces = bob.spaces() + assert.equal(spaces.length, 1) + assert.equal(spaces[0].did(), space.did()) + }) + }) + + describe('proofs', () => { + it('should get proofs', async () => { + const alice = new Client(await AgentData.create()) + const bob = new Client(await AgentData.create()) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + + const delegation = await alice.createDelegation(bob.agent(), ['*']) + + await bob.addProof(delegation) + + const proofs = bob.proofs() + assert.equal(proofs.length, 1) + assert.equal(proofs[0].cid.toString(), delegation.cid.toString()) + }) + }) + + describe('delegations', () => { + it('should get delegations', async () => { + const alice = new Client(await AgentData.create()) + const bob = new Client(await AgentData.create()) + + const space = await alice.createSpace() + await alice.setCurrentSpace(space.did()) + const name = `delegation-${Date.now()}` + const delegation = await alice.createDelegation(bob.agent(), ['*'], { + audienceMeta: { type: 'device', name } + }) + + const delegations = alice.delegations() + assert.equal(delegations.length, 1) + assert.equal(delegations[0].cid.toString(), delegation.cid.toString()) + assert.equal(delegations[0].meta()?.audience?.name, name) + }) + }) + + describe('defaultProvider', () => { + it('should return the connection ID', async () => { + const alice = new Client(await AgentData.create()) + assert.equal(alice.defaultProvider(), 'did:web:web3.storage') + }) + }) +}) diff --git a/packages/w3up-client/test/helpers/bucket-server.js b/packages/w3up-client/test/helpers/bucket-server.js new file mode 100644 index 000000000..9db6842fd --- /dev/null +++ b/packages/w3up-client/test/helpers/bucket-server.js @@ -0,0 +1,15 @@ +import { createServer } from 'http' + +const port = process.env.PORT ?? 9000 +const status = process.env.STATUS ? parseInt(process.env.STATUS) : 200 + +const server = createServer((req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', '*') + res.setHeader('Access-Control-Allow-Headers', '*') + if (req.method === 'OPTIONS') return res.end() + res.statusCode = status + res.end() +}) + +server.listen(port, () => console.log(`Listening on :${port}`)) diff --git a/packages/w3up-client/test/helpers/car.js b/packages/w3up-client/test/helpers/car.js new file mode 100644 index 000000000..a32411b8b --- /dev/null +++ b/packages/w3up-client/test/helpers/car.js @@ -0,0 +1,26 @@ +import { CarWriter } from '@ipld/car' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' +import { sha256 } from 'multiformats/hashes/sha2' +import * as CAR from '@ucanto/transport/car' + +/** + * @param {Uint8Array} bytes + **/ +export async function toCAR (bytes) { + const hash = await sha256.digest(bytes) + const root = CID.create(1, raw.code, hash) + + const { writer, out } = CarWriter.create(root) + writer.put({ cid: root, bytes }) + writer.close() + + const chunks = [] + for await (const chunk of out) { + chunks.push(chunk) + } + const blob = new Blob(chunks) + const cid = await CAR.codec.link(new Uint8Array(await blob.arrayBuffer())) + + return Object.assign(blob, { cid, roots: [root] }) +} diff --git a/packages/w3up-client/test/helpers/mocks.js b/packages/w3up-client/test/helpers/mocks.js new file mode 100644 index 000000000..d072fad62 --- /dev/null +++ b/packages/w3up-client/test/helpers/mocks.js @@ -0,0 +1,72 @@ +import * as Server from '@ucanto/server' +import { connect } from '@ucanto/client' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/transport/cbor' + +const notImplemented = () => { + throw new Server.Failure('not implemented') +} + +/** + * @param {Partial<{ + * store: Partial + * upload: Partial + * voucher: Partial + * space: Partial + * }>} impl + */ +export function mockService (impl) { + return { + store: { + add: withCallCount(impl.store?.add ?? notImplemented), + list: withCallCount(impl.store?.list ?? notImplemented), + remove: withCallCount(impl.store?.remove ?? notImplemented) + }, + upload: { + add: withCallCount(impl.upload?.add ?? notImplemented), + list: withCallCount(impl.upload?.list ?? notImplemented), + remove: withCallCount(impl.upload?.remove ?? notImplemented) + }, + space: { + info: withCallCount(impl.space?.info ?? notImplemented), + 'recover-validation': withCallCount(impl.space?.['recover-validation'] ?? notImplemented) + }, + access: { + claim: withCallCount(impl.access?.claim ?? notImplemented), + authorize: withCallCount(impl.access?.authorize ?? notImplemented), + delegate: withCallCount(impl.access?.delegate ?? notImplemented) + }, + provider: { + add: withCallCount(impl.provider?.add ?? notImplemented) + } + } +} + +/** + * @template {Function} T + * @param {T} fn + */ +function withCallCount (fn) { + /** @param {T extends (...args: infer A) => any ? A : never} args */ + const countedFn = (...args) => { + countedFn.called = true + countedFn.callCount++ + return fn(...args) + } + countedFn.called = false + countedFn.callCount = 0 + return countedFn +} + +/** + * @param {import('@ucanto/interface').ServerView} server + */ +export async function mockServiceConf (server) { + const connection = connect({ + id: server.id, + encoder: CAR, + decoder: CBOR, + channel: server + }) + return { access: connection, upload: connection } +} diff --git a/packages/w3up-client/test/helpers/random.js b/packages/w3up-client/test/helpers/random.js new file mode 100644 index 000000000..98ccfcf77 --- /dev/null +++ b/packages/w3up-client/test/helpers/random.js @@ -0,0 +1,31 @@ +import { toCAR } from './car.js' + +/** @param {number} size */ +export async function randomBytes (size) { + const bytes = new Uint8Array(size) + while (size) { + const chunk = new Uint8Array(Math.min(size, 65_536)) + if (!globalThis.crypto) { + try { + const { webcrypto } = await import('node:crypto') + webcrypto.getRandomValues(chunk) + } catch (err) { + throw new Error( + 'unknown environment - no global crypto and not Node.js', + { cause: err } + ) + } + } else { + crypto.getRandomValues(chunk) + } + size -= bytes.length + bytes.set(chunk, size) + } + return bytes +} + +/** @param {number} size */ +export async function randomCAR (size) { + const bytes = await randomBytes(size) + return toCAR(bytes) +} diff --git a/packages/w3up-client/test/helpers/shims.js b/packages/w3up-client/test/helpers/shims.js new file mode 100644 index 000000000..fa7580db0 --- /dev/null +++ b/packages/w3up-client/test/helpers/shims.js @@ -0,0 +1,10 @@ +export class File extends Blob { + /** + * @param {BlobPart[]} blobParts + * @param {string} name + */ + constructor (blobParts, name) { + super(blobParts) + this.name = name + } +} diff --git a/packages/w3up-client/test/index.browser.test.js b/packages/w3up-client/test/index.browser.test.js new file mode 100644 index 000000000..c208b15f6 --- /dev/null +++ b/packages/w3up-client/test/index.browser.test.js @@ -0,0 +1,12 @@ +import assert from 'assert' +import { RS256 } from '@ipld/dag-ucan/signature' +import { create } from '../src/index.js' + +describe('create', () => { + it('should create RSA key', async () => { + const client = await create() + const signer = client.agent() + assert.equal(signer.signatureAlgorithm, 'RS256') + assert.equal(signer.signatureCode, RS256) + }) +}) diff --git a/packages/w3up-client/test/index.node.test.js b/packages/w3up-client/test/index.node.test.js new file mode 100644 index 000000000..09aedc98e --- /dev/null +++ b/packages/w3up-client/test/index.node.test.js @@ -0,0 +1,23 @@ +import assert from 'assert' +import { EdDSA } from '@ipld/dag-ucan/signature' +import { StoreConf } from '@web3-storage/access/stores/store-conf' +import { create } from '../src/index.node.js' + +describe('create', () => { + it('should create Ed25519 key', async () => { + const client = await create() + const signer = client.agent() + assert.equal(signer.signatureAlgorithm, 'EdDSA') + assert.equal(signer.signatureCode, EdDSA) + }) + + it('should load from existing store', async () => { + const store = new StoreConf({ profile: 'w3up-client-test' }) + await store.reset() + + const client0 = await create({ store }) + const client1 = await create({ store }) + + assert.equal(client0.agent().did(), client1.agent().did()) + }) +}) diff --git a/packages/w3up-client/test/space.test.js b/packages/w3up-client/test/space.test.js new file mode 100644 index 000000000..cda210904 --- /dev/null +++ b/packages/w3up-client/test/space.test.js @@ -0,0 +1,16 @@ +import * as Signer from '@ucanto/principal/ed25519' +import assert from 'assert' +import { Space } from '../src/space.js' + +describe('spaces', () => { + it('should get meta', async () => { + const signer = await Signer.generate() + const name = `space-${Date.now()}` + const isRegistered = true + const space = new Space(signer.did(), { name, isRegistered }) + assert.equal(space.did(), signer.did()) + assert.equal(space.name(), name) + assert.equal(space.registered(), isRegistered) + assert.equal(space.meta().name, name) + }) +}) diff --git a/packages/w3up-client/tsconfig.json b/packages/w3up-client/tsconfig.json new file mode 100644 index 000000000..2d02108fe --- /dev/null +++ b/packages/w3up-client/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": true, + "composite": true, + "noUnusedParameters": false, + }, + "include": [ + "./src", + // @todo add "./test", + ], + "typedocOptions": { + "entryPoints": ["./src"], + "entryPointStrategy": "expand", + "darkHighlightTheme": "github-dark", + "navigationLinks": { + "Github": "https://github.com/web3-storage/w3up-client" + }, + "customCss": "./static/docs.css", + }, + "references": [ + { "path": "../access-client" }, + { "path": "../capabilities" }, + { "path": "../upload-client" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55f50c422..9ccf76098 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,7 @@ importers: '@web3-storage/access': link:../access-client '@web3-storage/capabilities': link:../capabilities '@web3-storage/worker-utils': 0.4.3-dev + dotenv: 16.0.3 kysely: 0.23.4 kysely-d1: 0.3.0_kysely@0.23.4 multiformats: 11.0.2 @@ -108,7 +109,6 @@ importers: '@ucanto/client': 5.1.0 better-sqlite3: 8.0.1 buffer: 6.0.3 - dotenv: 16.0.3 esbuild: 0.17.5 git-rev-sync: 3.0.2 hd-scripts: 4.0.0 @@ -286,7 +286,7 @@ importers: '@web3-storage/capabilities': workspace:^ assert: ^2.0.0 blockstore-core: ^3.0.0 - c8: ^7.12.0 + c8: ^7.13.0 hd-scripts: ^4.0.0 hundreds: ^0.0.9 ipfs-unixfs-exporter: ^10.0.0 @@ -315,7 +315,7 @@ importers: '@ucanto/server': 6.1.0 assert: 2.0.0 blockstore-core: 3.0.0 - c8: 7.12.0 + c8: 7.13.0 hd-scripts: 4.0.0 hundreds: 0.0.9 ipfs-unixfs-exporter: 10.0.0 @@ -324,6 +324,61 @@ importers: playwright-test: 8.2.0 typescript: 4.9.5 + packages/w3up-client: + specifiers: + '@docusaurus/core': ^2.2.0 + '@ipld/car': ^5.1.1 + '@ipld/dag-ucan': ^3.0.1 + '@ucanto/client': ^5.1.0 + '@ucanto/core': ^5.1.0 + '@ucanto/interface': ^6.0.0 + '@ucanto/principal': ^5.1.0 + '@ucanto/server': ^6.1.0 + '@ucanto/transport': ^5.1.0 + '@web3-storage/access': workspace:^ + '@web3-storage/capabilities': workspace:^ + '@web3-storage/upload-client': workspace:^ + assert: ^2.0.0 + c8: ^7.13.0 + docusaurus-plugin-typedoc: ^0.18.0 + hundreds: ^0.0.9 + mocha: ^10.1.0 + multiformats: ^11.0.0 + npm-run-all: ^4.1.5 + playwright-test: ^8.1.1 + standard: ^17.0.0 + typedoc: ^0.23.24 + typedoc-plugin-markdown: ^3.14.0 + typedoc-plugin-missing-exports: ^1.0.0 + typescript: ^4.8.3 + dependencies: + '@ipld/dag-ucan': 3.2.0 + '@ucanto/client': 5.1.0 + '@ucanto/core': 5.2.0 + '@ucanto/interface': 6.2.0 + '@ucanto/principal': 5.1.0 + '@ucanto/transport': 5.1.1 + '@web3-storage/access': link:../access-client + '@web3-storage/capabilities': link:../capabilities + '@web3-storage/upload-client': link:../upload-client + devDependencies: + '@docusaurus/core': 2.3.1_typescript@4.9.5 + '@ipld/car': 5.1.1 + '@ucanto/server': 6.1.0 + assert: 2.0.0 + c8: 7.13.0 + docusaurus-plugin-typedoc: 0.18.0_bhwftghzp2kjaeaba4ticsx7k4 + hundreds: 0.0.9 + mocha: 10.2.0 + multiformats: 11.0.2 + npm-run-all: 4.1.5 + playwright-test: 8.2.0 + standard: 17.0.0 + typedoc: 0.23.28_typescript@4.9.5 + typedoc-plugin-markdown: 3.14.0_typedoc@0.23.28 + typedoc-plugin-missing-exports: 1.0.0_typedoc@0.23.28 + typescript: 4.9.5 + packages: /@ampproject/remapping/2.2.0: @@ -1896,7 +1951,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.0.28 + '@types/react': 18.0.30 prop-types: 15.8.1 dev: true @@ -3519,11 +3574,11 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: true - /@types/react/18.0.28: - resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==} + /@types/react/18.0.30: + resolution: {integrity: sha512-AnME2cHDH11Pxt/yYX6r0w448BfTwQOLEhQEjCdwB7QskEI7EKtxhGUsExTQe/MsY3D9D5rMtu62WRocw9A8FA==} dependencies: '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 + '@types/scheduler': 0.16.3 csstype: 3.1.1 dev: true @@ -3541,8 +3596,8 @@ packages: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} dev: false - /@types/scheduler/0.16.2: - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + /@types/scheduler/0.16.3: + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: true /@types/semver/7.3.13: @@ -4265,7 +4320,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 get-intrinsic: 1.2.0 is-string: 1.0.7 @@ -4280,7 +4335,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 is-string: 1.0.7 dev: true @@ -4290,7 +4345,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 dev: true @@ -4300,7 +4355,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 dev: true @@ -4309,7 +4364,7 @@ packages: resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 get-intrinsic: 1.2.0 @@ -4504,7 +4559,7 @@ packages: dependencies: buffer: 6.0.3 inherits: 2.0.4 - readable-stream: 3.6.0 + readable-stream: 3.6.2 /blake3-wasm/2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -4662,8 +4717,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /c8/7.12.0: - resolution: {integrity: sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==} + /c8/7.13.0: + resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==} engines: {node: '>=10.12.0'} hasBin: true dependencies: @@ -4676,7 +4731,7 @@ packages: istanbul-reports: 3.1.5 rimraf: 3.0.2 test-exclude: 6.0.0 - v8-to-istanbul: 9.0.1 + v8-to-istanbul: 9.1.0 yargs: 16.2.0 yargs-parser: 20.2.9 dev: true @@ -5054,7 +5109,7 @@ packages: dev: true /concat-map/0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} /conf/10.2.0: resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} @@ -5201,7 +5256,7 @@ packages: resolution: {integrity: sha512-3scnzFj/94eb7y4wyXRWwvzLFaQp87yyfTnChIjlfYrVqp5lVO3E2hIJMeQIltUT0K2ZAB3An1qXcBmwGyvuwA==} engines: {node: '>=10'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 make-dir: 3.1.0 nested-error-stacks: 2.1.1 p-event: 4.2.0 @@ -5571,14 +5626,6 @@ packages: engines: {node: '>=8'} dev: true - /define-properties/1.1.4: - resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} - engines: {node: '>= 0.4'} - dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 - dev: true - /define-properties/1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} @@ -5809,7 +5856,7 @@ packages: /dotenv/16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} - dev: true + dev: false /dotignore/0.1.2: resolution: {integrity: sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==} @@ -6114,6 +6161,16 @@ packages: eslint: 8.33.0 dev: true + /eslint-config-standard-jsx/11.0.0_qfmo47wux3a6s5vhwqly27hd6u: + resolution: {integrity: sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==} + peerDependencies: + eslint: ^8.8.0 + eslint-plugin-react: ^7.28.0 + dependencies: + eslint: 8.33.0 + eslint-plugin-react: 7.32.2_eslint@8.33.0 + dev: true + /eslint-config-standard-with-typescript/30.0.0_frfgwa7fqjzszldru3sxumpviq: resolution: {integrity: sha512-/Ltst1BCZCWrGmqprLHBkTwuAbcoQrR8uMeSzZAv1vHKIVg+2nFje+DULA30SW01yCNhnx0a8yhZBkR0ZZPp+w==} peerDependencies: @@ -6175,6 +6232,34 @@ packages: - supports-color dev: true + /eslint-module-utils/2.7.4_b5qyyy7jj6vxczv7eweintx4wu: + resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.33.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils/2.7.4_ypqpzq5szckeh62pb722iz7nn4: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} @@ -6233,6 +6318,38 @@ packages: - supports-color dev: true + /eslint-plugin-import/2.27.5_eslint@8.33.0: + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.33.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.7.4_b5qyyy7jj6vxczv7eweintx4wu + has: 1.0.3 + is-core-module: 2.11.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.1 + semver: 6.3.0 + tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-import/2.27.5_ufewo3pl5nnmz6lltvjrdi2hii: resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -6924,7 +7041,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 functions-have-names: 1.2.3 dev: true @@ -6972,6 +7089,11 @@ packages: engines: {node: '>=8.0.0'} dev: true + /get-stdin/8.0.0: + resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} + engines: {node: '>=10'} + dev: true + /get-stream/4.1.0: resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} engines: {node: '>=6'} @@ -7092,7 +7214,7 @@ packages: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} dependencies: - define-properties: 1.1.4 + define-properties: 1.2.0 dev: true /globby/11.1.0: @@ -7146,10 +7268,6 @@ packages: resolution: {integrity: sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==} dev: true - /graceful-fs/4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true - /graceful-fs/4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true @@ -7563,7 +7681,7 @@ packages: resolution: {integrity: sha512-4pHIJQl4SiVeMeg00w6WypEjVrSgVUxQS8YlOWW3KehCdlz/PoEhKlJY2DYeR56lPoF5KA+YjHKgnwhCsKJ7IQ==} hasBin: true dependencies: - c8: 7.12.0 + c8: 7.13.0 dev: true /iconv-lite/0.4.24: @@ -7899,7 +8017,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 dev: true /is-negative-zero/2.0.2: @@ -8301,7 +8419,7 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true dependencies: - minimist: 1.2.7 + minimist: 1.2.8 dev: true /json5/2.2.3: @@ -8500,12 +8618,23 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 parse-json: 4.0.0 pify: 3.0.0 strip-bom: 3.0.0 dev: true + /load-json-file/5.3.0: + resolution: {integrity: sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 4.0.1 + strip-bom: 3.0.0 + type-fest: 0.3.1 + dev: true + /loader-runner/4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -9166,7 +9295,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 dev: true /object-keys/1.1.1: @@ -9189,7 +9318,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -9198,14 +9327,14 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true /object.hasown/1.1.2: resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} dependencies: - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -9214,7 +9343,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -9586,6 +9715,19 @@ packages: engines: {node: '>=4'} dev: true + /pify/4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: true + + /pkg-conf/3.1.0: + resolution: {integrity: sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + load-json-file: 5.3.0 + dev: true + /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -9618,7 +9760,7 @@ packages: events: 3.3.0 globby: 13.1.3 kleur: 4.1.5 - lilconfig: 2.0.6 + lilconfig: 2.1.0 lodash: 4.17.21 merge-options: 3.0.4 nanoid: 4.0.0 @@ -9637,7 +9779,7 @@ packages: tape: 5.6.3 tempy: 3.0.0 test-exclude: 6.0.0 - v8-to-istanbul: 9.0.1 + v8-to-istanbul: 9.1.0 dev: true /please-upgrade-node/3.2.0: @@ -10481,14 +10623,6 @@ packages: util-deprecate: 1.0.2 dev: true - /readable-stream/3.6.0: - resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - /readable-stream/3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -10496,7 +10630,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /readable-stream/4.3.0: resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==} @@ -10559,7 +10692,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 functions-have-names: 1.2.3 dev: true @@ -11327,6 +11460,36 @@ packages: stacktrace-gps: 3.1.2 dev: false + /standard-engine/15.0.0: + resolution: {integrity: sha512-4xwUhJNo1g/L2cleysUqUv7/btn7GEbYJvmgKrQ2vd/8pkTmN8cpqAZg+BT8Z1hNeEH787iWUdOpL8fmApLtxA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + get-stdin: 8.0.0 + minimist: 1.2.8 + pkg-conf: 3.1.0 + xdg-basedir: 4.0.0 + dev: true + + /standard/17.0.0: + resolution: {integrity: sha512-GlCM9nzbLUkr+TYR5I2WQoIah4wHA2lMauqbyPLV/oI5gJxqhHzhjl9EG2N0lr/nRqI3KCbCvm/W3smxvLaChA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + eslint: 8.33.0 + eslint-config-standard: 17.0.0_xh3wrndcszbt2l7hdksdjqnjcq + eslint-config-standard-jsx: 11.0.0_qfmo47wux3a6s5vhwqly27hd6u + eslint-plugin-import: 2.27.5_eslint@8.33.0 + eslint-plugin-n: 15.6.1_eslint@8.33.0 + eslint-plugin-promise: 6.1.1_eslint@8.33.0 + eslint-plugin-react: 7.32.2_eslint@8.33.0 + standard-engine: 15.0.0 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /state-toggle/1.0.3: resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} dev: true @@ -11356,7 +11519,7 @@ packages: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} dependencies: inherits: 2.0.4 - readable-stream: 3.6.0 + readable-stream: 3.6.2 dev: true /streaming-iterables/7.1.0: @@ -11394,7 +11557,7 @@ packages: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 get-intrinsic: 1.2.0 has-symbols: 1.0.3 @@ -11408,7 +11571,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -11417,7 +11580,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -11425,7 +11588,7 @@ packages: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -11433,7 +11596,7 @@ packages: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.21.1 dev: true @@ -11591,7 +11754,7 @@ packages: has-dynamic-import: 2.0.1 inherits: 2.0.4 is-regex: 1.1.4 - minimist: 1.2.7 + minimist: 1.2.8 object-inspect: 1.12.3 object-is: 1.1.5 object-keys: 1.1.1 @@ -11778,7 +11941,7 @@ packages: dependencies: '@types/json5': 0.0.29 json5: 1.0.2 - minimist: 1.2.7 + minimist: 1.2.8 strip-bom: 3.0.0 dev: true @@ -11834,6 +11997,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest/0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + dev: true + /type-fest/0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -11895,7 +12063,6 @@ packages: typedoc: 0.22.x || 0.23.x dependencies: typedoc: 0.23.28_typescript@4.9.5 - dev: false /typedoc/0.23.28_typescript@4.9.5: resolution: {integrity: sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==} @@ -12162,8 +12329,8 @@ packages: hasBin: true dev: true - /v8-to-istanbul/9.0.1: - resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} + /v8-to-istanbul/9.1.0: + resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} dependencies: '@jridgewell/trace-mapping': 0.3.17 diff --git a/tsconfig.json b/tsconfig.json index 1e1be7206..b05856bf6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,7 +38,8 @@ "entryPoints": [ "packages/access-client", "packages/capabilities", - "packages/upload-client" + "packages/upload-client", + "packages/w3up-client", ], "excludeExternals": true, "darkHighlightTheme": "github-dark",