├── .gitattributes ├── .github ├── actions │ └── test │ │ └── action.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── account.js ├── api.js ├── api.ts ├── bin.js ├── bridge.js ├── can.js ├── coupon.js ├── dialog.js ├── index.js ├── lib.js ├── package-lock.json ├── package.json ├── piece-hasher-worker.js ├── shim.js ├── space.js ├── test ├── bin.spec.js ├── fixtures │ ├── empty.car │ ├── pinpie.car │ └── pinpie.jpg ├── helpers │ ├── context.js │ ├── env.js │ ├── http-server.js │ ├── process.js │ ├── random.js │ ├── receipt-http-server.js │ ├── stream.js │ └── util.js └── lib.spec.js └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.car -text 3 | -------------------------------------------------------------------------------- /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | description: 'Setup and test' 3 | 4 | runs: 5 | using: 'composite' 6 | steps: 7 | - uses: actions/setup-node@v4 8 | with: 9 | registry-url: 'https://registry.npmjs.org' 10 | node-version: 18 11 | cache: 'npm' 12 | - run: npm ci 13 | shell: bash 14 | - run: npm run lint 15 | shell: bash 16 | - run: npm test 17 | shell: bash 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: Release 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | id-token: write 11 | contents: write 12 | pull-requests: write 13 | steps: 14 | - uses: google-github-actions/release-please-action@v3 15 | id: release 16 | with: 17 | release-type: node 18 | package-name: '@web3-storage/w3cli' 19 | - uses: actions/checkout@v4 20 | if: ${{ steps.release.outputs.release_created }} 21 | - uses: ./.github/actions/test 22 | if: ${{ steps.release.outputs.release_created }} 23 | - run: npm publish --provenance 24 | env: 25 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 26 | if: ${{ steps.release.outputs.release_created }} 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [7.12.0](https://github.com/storacha/w3cli/compare/v7.11.0...v7.12.0) (2025-02-25) 5 | 6 | 7 | ### Features 8 | 9 | * prompt for login method ([#212](https://github.com/storacha/w3cli/issues/212)) ([c7a14da](https://github.com/storacha/w3cli/commit/c7a14da386148fdf49a9860d225d8071a1f7c4fd)) 10 | 11 | ## [7.11.0](https://github.com/storacha/w3cli/compare/v7.10.1...v7.11.0) (2025-02-25) 12 | 13 | 14 | ### Features 15 | 16 | * github login ([#211](https://github.com/storacha/w3cli/issues/211)) ([64838b1](https://github.com/storacha/w3cli/commit/64838b14fa9236fe9a25879874871d3c60f859f6)) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * updated nodejs requirement ([#209](https://github.com/storacha/w3cli/issues/209)) ([1a1b598](https://github.com/storacha/w3cli/commit/1a1b5988a49cea0c71a1304d101f27663633d590)) 22 | 23 | ## [7.10.1](https://github.com/storacha/w3cli/compare/v7.10.0...v7.10.1) (2025-01-14) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * test and error msg ([6e53b5f](https://github.com/storacha/w3cli/commit/6e53b5f65b4f9533c2930531925307771af5e918)) 29 | 30 | ## [7.10.0](https://github.com/storacha/w3cli/compare/v7.9.1...v7.10.0) (2025-01-14) 31 | 32 | 33 | ### Features 34 | 35 | * content serve authorization ([#205](https://github.com/storacha/w3cli/issues/205)) ([34efff2](https://github.com/storacha/w3cli/commit/34efff218576c9bb8b16cfad25cda10863a2f97e)) 36 | * proof ls command shows proof aud ([#174](https://github.com/storacha/w3cli/issues/174)) ([29d2400](https://github.com/storacha/w3cli/commit/29d2400a398b4eded9379e62c7d48f3a06635972)) 37 | 38 | ## [7.9.1](https://github.com/storacha/w3cli/compare/v7.9.0...v7.9.1) (2024-11-15) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * repo url ([7110804](https://github.com/storacha/w3cli/commit/7110804b16f95e7e1c38714056147d16b75b0304)) 44 | 45 | ## [7.9.0](https://github.com/storacha/w3cli/compare/v7.8.2...v7.9.0) (2024-11-15) 46 | 47 | 48 | ### Features 49 | 50 | * use worker thread for piece hashing ([#198](https://github.com/storacha/w3cli/issues/198)) ([dfafbd3](https://github.com/storacha/w3cli/commit/dfafbd3e65cb74b64a62b2f63129a927ddcd79f4)) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * bump files-from-path dependency ([b25861d](https://github.com/storacha/w3cli/commit/b25861d42b3530841a7e136fdd56d1b4ab2494b3)) 56 | 57 | ## [7.8.2](https://github.com/storacha/w3cli/compare/v7.8.1...v7.8.2) (2024-07-02) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * missing deps ([#200](https://github.com/storacha/w3cli/issues/200)) ([e8f13d5](https://github.com/storacha/w3cli/commit/e8f13d5bc75a253c91df353017e0a2ebc0154794)) 63 | 64 | ## [7.8.1](https://github.com/storacha/w3cli/compare/v7.8.0...v7.8.1) (2024-06-21) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * repo URLs ([9da051c](https://github.com/storacha/w3cli/commit/9da051c7f9b82ad27e749cbc3566a891a6513819)) 70 | 71 | ## [7.8.0](https://github.com/storacha/w3cli/compare/v7.7.1...v7.8.0) (2024-06-20) 72 | 73 | 74 | ### Features 75 | 76 | * use wasm piece hasher ([#195](https://github.com/storacha/w3cli/issues/195)) ([8ddc4d2](https://github.com/storacha/w3cli/commit/8ddc4d2b692173a02f6e337a752b84878293ef1f)) 77 | 78 | ## [7.7.1](https://github.com/w3s-project/w3cli/compare/v7.7.0...v7.7.1) (2024-06-05) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * upgrade w3up client with double upload fix ([#191](https://github.com/w3s-project/w3cli/issues/191)) ([31a0bf7](https://github.com/w3s-project/w3cli/commit/31a0bf7d3ed540c6fabf910b6fd87b1994c97b21)) 84 | 85 | ## [7.7.0](https://github.com/w3s-project/w3cli/compare/v7.6.2...v7.7.0) (2024-06-04) 86 | 87 | 88 | ### Features 89 | 90 | * allow pipe to w3 up ([#188](https://github.com/w3s-project/w3cli/issues/188)) ([4961f4a](https://github.com/w3s-project/w3cli/commit/4961f4a94d4c18ec363147293dd8d57750e8f17e)) 91 | * upgrade deps for blob ([#187](https://github.com/w3s-project/w3cli/issues/187)) ([7f52cc0](https://github.com/w3s-project/w3cli/commit/7f52cc0018f917b4bfa33f19106981ba714a3747)) 92 | 93 | ## [7.6.2](https://github.com/w3s-project/w3cli/compare/v7.6.1...v7.6.2) (2024-04-23) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * address npm provenance ([#185](https://github.com/w3s-project/w3cli/issues/185)) ([9981391](https://github.com/w3s-project/w3cli/commit/99813913e8b90de3e6b2e12859d1e760f5301c4a)) 99 | 100 | ## [7.6.1](https://github.com/w3s-project/w3cli/compare/v7.6.0...v7.6.1) (2024-04-23) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * remove leftover semicolon ([#183](https://github.com/w3s-project/w3cli/issues/183)) ([3b697de](https://github.com/w3s-project/w3cli/commit/3b697dee7184114710c7b43aca0ccfb9c947c548)) 106 | 107 | ## [7.6.0](https://github.com/web3-storage/w3cli/compare/v7.5.0...v7.6.0) (2024-03-05) 108 | 109 | 110 | ### Features 111 | 112 | * introduce `bridge generate-tokens` command ([#175](https://github.com/web3-storage/w3cli/issues/175)) ([5de8579](https://github.com/web3-storage/w3cli/commit/5de8579a7d0633c9c232ef4036b70b35ede55ec8)) 113 | * updates from PR feedback ([#179](https://github.com/web3-storage/w3cli/issues/179)) ([75f2195](https://github.com/web3-storage/w3cli/commit/75f2195571c9248a0ba3eacb240cd10b1b44e82d)) 114 | 115 | ## [7.5.0](https://github.com/web3-storage/w3cli/compare/v7.4.0...v7.5.0) (2024-01-25) 116 | 117 | 118 | ### Features 119 | 120 | * add reset command ([#170](https://github.com/web3-storage/w3cli/issues/170)) ([8eea385](https://github.com/web3-storage/w3cli/commit/8eea385e526bd73a9cfb3a674c31168a7b161f30)) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * use remove from client ([#167](https://github.com/web3-storage/w3cli/issues/167)) ([96966ba](https://github.com/web3-storage/w3cli/commit/96966bac7506e490330ac190587c2294627e838f)) 126 | 127 | ## [7.4.0](https://github.com/web3-storage/w3cli/compare/v7.3.0...v7.4.0) (2024-01-24) 128 | 129 | 130 | ### Features 131 | 132 | * w3 usage report catches/warns about errors invoking usage/report ([#169](https://github.com/web3-storage/w3cli/issues/169)) ([e47159e](https://github.com/web3-storage/w3cli/commit/e47159e74c7aef1c18ac83e43c6b78454f61a808)) 133 | 134 | ## [7.3.0](https://github.com/web3-storage/w3cli/compare/v7.2.1...v7.3.0) (2024-01-23) 135 | 136 | 137 | ### Features 138 | 139 | * add `name` to `w3 space info` output ([#164](https://github.com/web3-storage/w3cli/issues/164)) ([2b1bc4a](https://github.com/web3-storage/w3cli/commit/2b1bc4a117b1c611b0df8e574ceb501456a1aee5)) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * clear timeout always ([#166](https://github.com/web3-storage/w3cli/issues/166)) ([c1b7cce](https://github.com/web3-storage/w3cli/commit/c1b7ccee73ca7c54ea75eb6313d6e8a56b090c33)) 145 | 146 | ## [7.2.1](https://github.com/web3-storage/w3cli/compare/v7.2.0...v7.2.1) (2024-01-17) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * make `w3 up --no-wrap` work as advertised. ([#160](https://github.com/web3-storage/w3cli/issues/160)) ([426faad](https://github.com/web3-storage/w3cli/commit/426faadf1860bd5d35dd50b12c77e8acee0a0526)) 152 | 153 | ## [7.2.0](https://github.com/web3-storage/w3cli/compare/v7.1.0...v7.2.0) (2024-01-17) 154 | 155 | 156 | ### Features 157 | 158 | * `w3 delegation create --base64` & `w3 space add <base64>` ([#158](https://github.com/web3-storage/w3cli/issues/158)) ([98284ef](https://github.com/web3-storage/w3cli/commit/98284ef7ef95f5675b040ac49eabfaebe1701132)) 159 | 160 | ## [7.1.0](https://github.com/web3-storage/w3cli/compare/v7.0.4...v7.1.0) (2024-01-15) 161 | 162 | 163 | ### Features 164 | 165 | * `w3 key create` ([#155](https://github.com/web3-storage/w3cli/issues/155)) ([1fe7adb](https://github.com/web3-storage/w3cli/commit/1fe7adb634ae67037a97a6e66def7b2f56ad315a)) 166 | 167 | ## [7.0.4](https://github.com/web3-storage/w3cli/compare/v7.0.3...v7.0.4) (2024-01-09) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * no-wrap option ([#153](https://github.com/web3-storage/w3cli/issues/153)) ([9ae49e9](https://github.com/web3-storage/w3cli/commit/9ae49e931729f86bfa86b57d21cc859a5caf9664)) 173 | * update notification includes -g flag for cli ([#150](https://github.com/web3-storage/w3cli/issues/150)) ([370bfc6](https://github.com/web3-storage/w3cli/commit/370bfc69889cda82976442adb08dd53002f2487d)) 174 | 175 | ## [7.0.3](https://github.com/web3-storage/w3cli/compare/v7.0.2...v7.0.3) (2023-12-13) 176 | 177 | 178 | ### Bug Fixes 179 | 180 | * upgrade to latest access-client ([#146](https://github.com/web3-storage/w3cli/issues/146)) ([34e5d61](https://github.com/web3-storage/w3cli/commit/34e5d616ce36d11cc2d0a9ca7a4f28016e0f7a52)) 181 | 182 | ## [7.0.2](https://github.com/web3-storage/w3cli/compare/v7.0.1...v7.0.2) (2023-12-12) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * pin @web3-storage/access to 18.0.5 ([#144](https://github.com/web3-storage/w3cli/issues/144)) ([a5b2127](https://github.com/web3-storage/w3cli/commit/a5b2127420c570e28242f5e9a7bb161e181e084f)) 188 | 189 | ## [7.0.1](https://github.com/web3-storage/w3cli/compare/v7.0.0...v7.0.1) (2023-12-07) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * memory usage ([084a75e](https://github.com/web3-storage/w3cli/commit/084a75eb7ccede7471a89393b6b8892f66e500dd)) 195 | * types for files-from-path ([20965e9](https://github.com/web3-storage/w3cli/commit/20965e91c5a89885217c4781206686e68459be82)) 196 | 197 | ## [7.0.0](https://github.com/web3-storage/w3cli/compare/v6.1.0...v7.0.0) (2023-11-29) 198 | 199 | 200 | ### ⚠ BREAKING CHANGES 201 | 202 | * upgrade `proof ls` ([#136](https://github.com/web3-storage/w3cli/issues/136)) 203 | 204 | ### Features 205 | 206 | * upgrade `proof ls` ([#136](https://github.com/web3-storage/w3cli/issues/136)) ([d95d1a4](https://github.com/web3-storage/w3cli/commit/d95d1a4b98ec02e8b48496c8c370a455c82b9b1d)) 207 | 208 | ## [6.1.0](https://github.com/web3-storage/w3cli/compare/v6.0.0...v6.1.0) (2023-11-22) 209 | 210 | 211 | ### Features 212 | 213 | * add npm package provenance ([#135](https://github.com/web3-storage/w3cli/issues/135)) ([9b1697c](https://github.com/web3-storage/w3cli/commit/9b1697cd38af5f7a71638b2e6d33b10106e9d151)) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * update deps. pull in w3up-client fixes ([#133](https://github.com/web3-storage/w3cli/issues/133)) ([6aacec8](https://github.com/web3-storage/w3cli/commit/6aacec86a8cd3b46fd81d83f12a14fc182d3073d)) 219 | 220 | ## [6.0.0](https://github.com/web3-storage/w3cli/compare/v5.2.0...v6.0.0) (2023-11-16) 221 | 222 | 223 | ### ⚠ BREAKING CHANGES 224 | 225 | * provision using a proof ([#123](https://github.com/web3-storage/w3cli/issues/123)) 226 | 227 | ### Features 228 | 229 | * provision using a proof ([#123](https://github.com/web3-storage/w3cli/issues/123)) ([d61bdf3](https://github.com/web3-storage/w3cli/commit/d61bdf324254f9f444b989f33fd5434054e9028d)) 230 | 231 | ## [5.2.0](https://github.com/web3-storage/w3cli/compare/v5.1.0...v5.2.0) (2023-11-15) 232 | 233 | 234 | ### Features 235 | 236 | * can filecoin info ([#127](https://github.com/web3-storage/w3cli/issues/127)) ([d8290a6](https://github.com/web3-storage/w3cli/commit/d8290a68bfcf542ab756f8810bbbbeb3cc0c6a29)) 237 | 238 | ## [5.1.0](https://github.com/web3-storage/w3cli/compare/v5.0.0...v5.1.0) (2023-11-15) 239 | 240 | 241 | ### Features 242 | 243 | * delegation create uses space access ([#125](https://github.com/web3-storage/w3cli/issues/125)) ([bddff54](https://github.com/web3-storage/w3cli/commit/bddff54c35ea8b78ff6df8ee3508fcf2daa5369a)) 244 | 245 | ## [5.0.0](https://github.com/web3-storage/w3cli/compare/v4.6.0...v5.0.0) (2023-11-14) 246 | 247 | 248 | ### ⚠ BREAKING CHANGES 249 | 250 | * wait for the plan picker ([#124](https://github.com/web3-storage/w3cli/issues/124)) 251 | * new authorization flow ([#121](https://github.com/web3-storage/w3cli/issues/121)) 252 | 253 | ### Features 254 | 255 | * new authorization flow ([#121](https://github.com/web3-storage/w3cli/issues/121)) ([8d7caf6](https://github.com/web3-storage/w3cli/commit/8d7caf6d784406ff736c1376236ca771338c8be7)) 256 | * setup prettier + linter ([#116](https://github.com/web3-storage/w3cli/issues/116)) ([5707e54](https://github.com/web3-storage/w3cli/commit/5707e5441ccee257208d085c5facf29d7c046713)) 257 | * space usage reports ([#120](https://github.com/web3-storage/w3cli/issues/120)) ([5587a0d](https://github.com/web3-storage/w3cli/commit/5587a0d56161d612cfa86a04a0736e7964103e2d)) 258 | * switch tests to using upload-api ([#118](https://github.com/web3-storage/w3cli/issues/118)) ([be19ff9](https://github.com/web3-storage/w3cli/commit/be19ff945af0b266251999d1f3ec54cdef7e619c)) 259 | * wait for the plan picker ([#124](https://github.com/web3-storage/w3cli/issues/124)) ([dff71c4](https://github.com/web3-storage/w3cli/commit/dff71c46073c2b21319924fec6f15343d793f36f)) 260 | 261 | ## [4.6.0](https://github.com/web3-storage/w3cli/compare/v4.5.0...v4.6.0) (2023-11-01) 262 | 263 | 264 | ### Features 265 | 266 | * upgrade @web3-storage/access to 16.4.0 to fix bug with sessionProof selection with `w3 register space` ([#114](https://github.com/web3-storage/w3cli/issues/114)) ([8ed3c90](https://github.com/web3-storage/w3cli/commit/8ed3c90d1e10c8df4c5769761f362e1dbf372f43)) 267 | 268 | ## [4.5.0](https://github.com/web3-storage/w3cli/compare/v4.4.0...v4.5.0) (2023-10-20) 269 | 270 | 271 | ### Features 272 | 273 | * add `delegation revoke` command ([#106](https://github.com/web3-storage/w3cli/issues/106)) ([3c8f3bc](https://github.com/web3-storage/w3cli/commit/3c8f3bc65c3a6455b5970a9685b854a51f626c7c)) 274 | 275 | ## [4.4.0](https://github.com/web3-storage/w3cli/compare/v4.3.1...v4.4.0) (2023-10-18) 276 | 277 | 278 | ### Features 279 | 280 | * add update notifier ([#108](https://github.com/web3-storage/w3cli/issues/108)) ([9bd4b78](https://github.com/web3-storage/w3cli/commit/9bd4b78f1d6952b699ca46c116bb0590922029f6)) 281 | 282 | ## [4.3.1](https://github.com/web3-storage/w3cli/compare/v4.3.0...v4.3.1) (2023-10-18) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * update dependencies ([10c5502](https://github.com/web3-storage/w3cli/commit/10c5502b123766c4f1206e61aadb7b55a7051951)) 288 | 289 | ## [4.3.0](https://github.com/web3-storage/w3cli/compare/v4.2.1...v4.3.0) (2023-10-18) 290 | 291 | 292 | ### Features 293 | 294 | * add --verbose --json option to upload command and print piece CID ([#97](https://github.com/web3-storage/w3cli/issues/97)) ([775d1db](https://github.com/web3-storage/w3cli/commit/775d1db336ae3879f116af254b90407eb2af68e5)) 295 | * add `can store rm` & `can upload rm` commands ([#101](https://github.com/web3-storage/w3cli/issues/101)) ([a7bda04](https://github.com/web3-storage/w3cli/commit/a7bda049ca9d1c5c1ec16de81e72cb5506a86ca9)) 296 | * introduce unified service config ([#99](https://github.com/web3-storage/w3cli/issues/99)) ([f3b6220](https://github.com/web3-storage/w3cli/commit/f3b6220317dbb5b6a55ad2690e80bdac454651e4)) 297 | 298 | ## [4.2.1](https://github.com/web3-storage/w3cli/compare/v4.2.0...v4.2.1) (2023-09-13) 299 | 300 | 301 | ### Bug Fixes 302 | 303 | * do not print error when space is unknown ([#95](https://github.com/web3-storage/w3cli/issues/95)) ([7f693d8](https://github.com/web3-storage/w3cli/commit/7f693d818de3580f855b0a573c87272f7fc2479d)) 304 | 305 | ## [4.2.0](https://github.com/web3-storage/w3cli/compare/v4.1.2...v4.2.0) (2023-09-08) 306 | 307 | 308 | ### Features 309 | 310 | * display shard size ([#94](https://github.com/web3-storage/w3cli/issues/94)) ([59e22cb](https://github.com/web3-storage/w3cli/commit/59e22cbc4cb6e946a943e7d3227c7f7e5f5670d0)) 311 | 312 | 313 | ### Bug Fixes 314 | 315 | * don't error when email address contains + ([#90](https://github.com/web3-storage/w3cli/issues/90)) ([8240ba5](https://github.com/web3-storage/w3cli/commit/8240ba5a429416144b1b3a4dcce95c69cdbe9a3c)) 316 | * readme ([22a9312](https://github.com/web3-storage/w3cli/commit/22a9312321a4c9d21817c8e0ccda665c6ca41694)) 317 | * readme node version and link to space explainer ([#92](https://github.com/web3-storage/w3cli/issues/92)) ([b63b220](https://github.com/web3-storage/w3cli/commit/b63b220bd64ea96ee2e3d55d8872cf41f6584e51)) 318 | * various linting issues ([#93](https://github.com/web3-storage/w3cli/issues/93)) ([f829c42](https://github.com/web3-storage/w3cli/commit/f829c42ab1d4ca81b99489e109f668c1f5bab9ef)) 319 | 320 | ## [4.1.2](https://github.com/web3-storage/w3cli/compare/v4.1.1...v4.1.2) (2023-08-25) 321 | 322 | 323 | ### Bug Fixes 324 | 325 | * raise test timeout and bump package version ([#88](https://github.com/web3-storage/w3cli/issues/88)) ([cebeccc](https://github.com/web3-storage/w3cli/commit/cebecccaaac267fcf9cb35ddcd326d0085055c18)) 326 | 327 | ## [4.1.1](https://github.com/web3-storage/w3cli/compare/v4.1.0...v4.1.1) (2023-08-25) 328 | 329 | 330 | ### Bug Fixes 331 | 332 | * bump dependencies ([#86](https://github.com/web3-storage/w3cli/issues/86)) ([de2b037](https://github.com/web3-storage/w3cli/commit/de2b03750fc6d180547e75ef28fbd70c8db29c08)) 333 | 334 | ## [4.1.0](https://github.com/web3-storage/w3cli/compare/v4.0.0...v4.1.0) (2023-08-25) 335 | 336 | 337 | ### Features 338 | 339 | * add `space info` command to w3cli ([#83](https://github.com/web3-storage/w3cli/issues/83)) ([a72701c](https://github.com/web3-storage/w3cli/commit/a72701c4443748b5e1aa4037d6d33c978f236475)) 340 | * add ability to specify custom principal ([#84](https://github.com/web3-storage/w3cli/issues/84)) ([6115101](https://github.com/web3-storage/w3cli/commit/6115101fc62f66addf55ec9880ec77eccee9d435)) 341 | * bump versions of `access` and `w3up-client` ([#81](https://github.com/web3-storage/w3cli/issues/81)) ([55de733](https://github.com/web3-storage/w3cli/commit/55de733d175509583781c3b29102ceb8ff78d21d)) 342 | 343 | ## [4.0.0](https://github.com/web3-storage/w3cli/compare/v3.0.0...v4.0.0) (2023-07-19) 344 | 345 | 346 | ### ⚠ BREAKING CHANGES 347 | 348 | * update ucanto dependencies ([#80](https://github.com/web3-storage/w3cli/issues/80)) 349 | 350 | ### Features 351 | 352 | * update ucanto dependencies ([#80](https://github.com/web3-storage/w3cli/issues/80)) ([f2391bd](https://github.com/web3-storage/w3cli/commit/f2391bdf578c1dbe03a50d67b9f350653055fe83)) 353 | 354 | 355 | ### Bug Fixes 356 | 357 | * readme quickstart ([cc9640c](https://github.com/web3-storage/w3cli/commit/cc9640cda18da7d496f3c60521ae42e47e4ccb4f)) 358 | 359 | ## [3.0.0](https://github.com/web3-storage/w3cli/compare/v2.0.0...v3.0.0) (2023-03-29) 360 | 361 | 362 | ### ⚠ BREAKING CHANGES 363 | 364 | * allow specifying capabilities to receive when authorizing ([#70](https://github.com/web3-storage/w3cli/issues/70)) 365 | 366 | ### Features 367 | 368 | * allow specifying capabilities to receive when authorizing ([#70](https://github.com/web3-storage/w3cli/issues/70)) ([40208f5](https://github.com/web3-storage/w3cli/commit/40208f5df9d58b8c6cd30810e22d1b039a2488bf)) 369 | 370 | 371 | ### Bug Fixes 372 | 373 | * permanent data warning more general ([b6b579e](https://github.com/web3-storage/w3cli/commit/b6b579e2832c481c97423a1e6408ab6e740b17e8)) 374 | * typo in readme ([a623422](https://github.com/web3-storage/w3cli/commit/a623422a903e44987aed073e8c639258647fac77)) 375 | 376 | ## [2.0.0](https://github.com/web3-storage/w3cli/compare/v1.2.2...v2.0.0) (2023-03-23) 377 | 378 | 379 | ### ⚠ BREAKING CHANGES 380 | 381 | * upgrade to latest access & upload clients ([#64](https://github.com/web3-storage/w3cli/issues/64)) 382 | * move `space register` email parameter to an `--email` option and add `--provider` option ([#60](https://github.com/web3-storage/w3cli/issues/60)) 383 | * use new account model ([#53](https://github.com/web3-storage/w3cli/issues/53)) 384 | 385 | ### Features 386 | 387 | * move `space register` email parameter to an `--email` option and add `--provider` option ([#60](https://github.com/web3-storage/w3cli/issues/60)) ([c1ed0e5](https://github.com/web3-storage/w3cli/commit/c1ed0e526947f0f9cae50c3974f7e8ec0408f8ec)) 388 | * show help text if no cmd ([#63](https://github.com/web3-storage/w3cli/issues/63)) ([fd5f342](https://github.com/web3-storage/w3cli/commit/fd5f342fae68a6d2f81591f1b0d61d3740c86650)) 389 | * update README with new ToS ([#62](https://github.com/web3-storage/w3cli/issues/62)) ([4ce61d7](https://github.com/web3-storage/w3cli/commit/4ce61d7657dc046004de006b5cabe3f534c58ee3)), closes [#54](https://github.com/web3-storage/w3cli/issues/54) 390 | * upgrade to latest access & upload clients ([#64](https://github.com/web3-storage/w3cli/issues/64)) ([b5851ca](https://github.com/web3-storage/w3cli/commit/b5851ca51e69b9314ced8c962128a673628fcc25)) 391 | * use new account model ([#53](https://github.com/web3-storage/w3cli/issues/53)) ([7f63286](https://github.com/web3-storage/w3cli/commit/7f63286b4f4fa158b0211fc1763dba236a27369b)) 392 | 393 | ## [1.2.2](https://github.com/web3-storage/w3cli/compare/v1.2.1...v1.2.2) (2023-03-20) 394 | 395 | 396 | ### Bug Fixes 397 | 398 | * drop redudant w3 space command for consistency ([#57](https://github.com/web3-storage/w3cli/issues/57)) ([8dfab92](https://github.com/web3-storage/w3cli/commit/8dfab9272b83e175ff2e3016b0f166e5e59e08a7)), closes [#44](https://github.com/web3-storage/w3cli/issues/44) 399 | * warnings about uploads being public/permanent ([84a9fd5](https://github.com/web3-storage/w3cli/commit/84a9fd56c287939c4127c9b8e03e2b661a751d79)) 400 | 401 | ## [1.2.1](https://github.com/web3-storage/w3cli/compare/v1.2.0...v1.2.1) (2023-03-02) 402 | 403 | 404 | ### Bug Fixes 405 | 406 | * pass cursor to list in `w3 ls` ([#50](https://github.com/web3-storage/w3cli/issues/50)) ([324e913](https://github.com/web3-storage/w3cli/commit/324e913bd44f92a79225e4cf24fe3708ff105dbf)) 407 | 408 | ## [1.2.0](https://github.com/web3-storage/w3cli/compare/v1.1.1...v1.2.0) (2023-01-27) 409 | 410 | 411 | ### Features 412 | 413 | * `proof add --dry-run --json` to view a delegation ([#40](https://github.com/web3-storage/w3cli/issues/40)) ([c32283a](https://github.com/web3-storage/w3cli/commit/c32283afa4e77895b35ea2055b1a9edf264bd360)) 414 | 415 | ## [1.1.1](https://github.com/web3-storage/w3cli/compare/v1.1.0...v1.1.1) (2023-01-13) 416 | 417 | 418 | ### Bug Fixes 419 | 420 | * dont error on systems that cant pass flags to env in shebang ([#38](https://github.com/web3-storage/w3cli/issues/38)) ([1e24e9f](https://github.com/web3-storage/w3cli/commit/1e24e9f581059c0d26d92a8882b0c37f997dc66b)) 421 | 422 | ## [1.1.0](https://github.com/web3-storage/w3cli/compare/v1.0.1...v1.1.0) (2023-01-11) 423 | 424 | 425 | ### Features 426 | 427 | * add support for sharded CAR uploads ([#36](https://github.com/web3-storage/w3cli/issues/36)) ([b055c78](https://github.com/web3-storage/w3cli/commit/b055c781e5a955249aaf8a4a07d094c3314a895b)) 428 | * adds `w3 rm <root-cid>` cmd ([#20](https://github.com/web3-storage/w3cli/issues/20)) ([899a4d4](https://github.com/web3-storage/w3cli/commit/899a4d4b5b427e1d1814ce2a7702faa6bb916177)) 429 | 430 | 431 | ### Bug Fixes 432 | 433 | * remove warnings using shebang ([#33](https://github.com/web3-storage/w3cli/issues/33)) ([f3a1aac](https://github.com/web3-storage/w3cli/commit/f3a1aac15eb88cd1096c45d83104fe8b75534c66)) 434 | 435 | ## [1.0.1](https://github.com/web3-storage/w3cli/compare/v1.0.0...v1.0.1) (2022-12-14) 436 | 437 | 438 | ### Bug Fixes 439 | 440 | * missing file name when only single path passed to filesFromPaths ([#31](https://github.com/web3-storage/w3cli/issues/31)) ([fc3b5a0](https://github.com/web3-storage/w3cli/commit/fc3b5a0b22b275a3ecd5e680d69dafed099b82fd)) 441 | 442 | ## 1.0.0 (2022-12-14) 443 | 444 | 445 | ### Features 446 | 447 | * add `list` command ([#10](https://github.com/web3-storage/w3cli/issues/10)) ([b6cb1f0](https://github.com/web3-storage/w3cli/commit/b6cb1f0be92071622ab413dc3b9a8fb9b7cffc5a)) 448 | * add `proof add` command ([#24](https://github.com/web3-storage/w3cli/issues/24)) ([eb32d28](https://github.com/web3-storage/w3cli/commit/eb32d2834b716a352c2e9de7813c69c004b20066)) 449 | * add `space` command with `create` and `register` ([#3](https://github.com/web3-storage/w3cli/issues/3)) ([9c25a2d](https://github.com/web3-storage/w3cli/commit/9c25a2dba25f8c2dfafbf18df3f1a733580d9488)) 450 | * add `w3 can store add` and `w3 can upload add` commands ([#26](https://github.com/web3-storage/w3cli/issues/26)) ([07fa1b0](https://github.com/web3-storage/w3cli/commit/07fa1b0b1b1500dfebddb8a855807bd45843dba6)) 451 | * add `whoami` to print agent DID ([#11](https://github.com/web3-storage/w3cli/issues/11)) ([e3f2497](https://github.com/web3-storage/w3cli/commit/e3f2497514c79c0d1018d92f0ae254d8f2c8ac1e)) 452 | * add delegation create command ([#5](https://github.com/web3-storage/w3cli/issues/5)) ([272c53a](https://github.com/web3-storage/w3cli/commit/272c53ab15766c7728b2ec9b8a54fe05b2ad876c)) 453 | * add old CLI bin reference ([#4](https://github.com/web3-storage/w3cli/issues/4)) ([9a0716c](https://github.com/web3-storage/w3cli/commit/9a0716c9b33a7c14a5b597287e9fd777bd0474f2)) 454 | * delegation ls and proof ls commands ([#22](https://github.com/web3-storage/w3cli/issues/22)) ([04a7d31](https://github.com/web3-storage/w3cli/commit/04a7d31111213a64c5a4a4b9133480c67efeef33)) 455 | * sade cli skeleton ([#1](https://github.com/web3-storage/w3cli/issues/1)) ([3104b9f](https://github.com/web3-storage/w3cli/commit/3104b9f70c38682544099f84e150953e2fc7d5b3)) 456 | * up command ([#7](https://github.com/web3-storage/w3cli/issues/7)) ([283b938](https://github.com/web3-storage/w3cli/commit/283b93835804b849299ec3c336207348cd305f6a)) 457 | 458 | 459 | ### Bug Fixes 460 | 461 | * add expiration parameter ([#21](https://github.com/web3-storage/w3cli/issues/21)) ([9457841](https://github.com/web3-storage/w3cli/commit/945784105d54f2b7d051f58dfead48b18a861999)) 462 | * better error reporting ([#19](https://github.com/web3-storage/w3cli/issues/19)) ([0f6a2a6](https://github.com/web3-storage/w3cli/commit/0f6a2a66897d2dd117841db59f21e040101b7fb4)), closes [#15](https://github.com/web3-storage/w3cli/issues/15) [#16](https://github.com/web3-storage/w3cli/issues/16) 463 | * create space on register ([#13](https://github.com/web3-storage/w3cli/issues/13)) ([f4a1a0f](https://github.com/web3-storage/w3cli/commit/f4a1a0f996ec091dd514478fb12ed7d3d0aebaec)) 464 | * dont emit warnings for fetch api ([#9](https://github.com/web3-storage/w3cli/issues/9)) ([cf52922](https://github.com/web3-storage/w3cli/commit/cf52922157e5601d2dd61c9af29123d27b33bf51)) 465 | * files-from-path with common path prefix removed ([#27](https://github.com/web3-storage/w3cli/issues/27)) ([c849d8e](https://github.com/web3-storage/w3cli/commit/c849d8e2699ac32f4babc661a9b1a8267803cc54)) 466 | * no build ([72b0bfb](https://github.com/web3-storage/w3cli/commit/72b0bfb7e9ae531edde2629e2f6db9add8add317)) 467 | * no-wrap parameter ([#25](https://github.com/web3-storage/w3cli/issues/25)) ([38aa353](https://github.com/web3-storage/w3cli/commit/38aa3539b20c2a1cf309f223eda55c0a944efe07)), closes [#17](https://github.com/web3-storage/w3cli/issues/17) 468 | * space create and register improvements ([#8](https://github.com/web3-storage/w3cli/issues/8)) ([8617b49](https://github.com/web3-storage/w3cli/commit/8617b49ea509e4fec493b77d9c91fc17126e02e3)) 469 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The contents of this repository are Copyright (c) corresponding authors and 2 | contributors, licensed under the `Permissive License Stack` meaning either of: 3 | 4 | - Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 5 | ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) 6 | 7 | - MIT Software License: https://opensource.org/licenses/MIT 8 | ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) 9 | 10 | You may not use the contents of this repository except in compliance 11 | with one of the listed Licenses. For an extended clarification of the 12 | intent behind the choice of Licensing please refer to 13 | https://protocol.ai/blog/announcing-the-permissive-license-stack/ 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the terms listed in this notice is distributed on 17 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 18 | either express or implied. See each License for the specific language 19 | governing permissions and limitations under that License. 20 | 21 | 22 | 23 | `SPDX-License-Identifier: Apache-2.0 OR MIT` 24 | 25 | Verbatim copies of both licenses are included below: 26 | 27 |
Apache-2.0 Software License 28 | 29 | ``` 30 | Apache License 31 | Version 2.0, January 2004 32 | http://www.apache.org/licenses/ 33 | 34 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 35 | 36 | 1. Definitions. 37 | 38 | "License" shall mean the terms and conditions for use, reproduction, 39 | and distribution as defined by Sections 1 through 9 of this document. 40 | 41 | "Licensor" shall mean the copyright owner or entity authorized by 42 | the copyright owner that is granting the License. 43 | 44 | "Legal Entity" shall mean the union of the acting entity and all 45 | other entities that control, are controlled by, or are under common 46 | control with that entity. For the purposes of this definition, 47 | "control" means (i) the power, direct or indirect, to cause the 48 | direction or management of such entity, whether by contract or 49 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 50 | outstanding shares, or (iii) beneficial ownership of such entity. 51 | 52 | "You" (or "Your") shall mean an individual or Legal Entity 53 | exercising permissions granted by this License. 54 | 55 | "Source" form shall mean the preferred form for making modifications, 56 | including but not limited to software source code, documentation 57 | source, and configuration files. 58 | 59 | "Object" form shall mean any form resulting from mechanical 60 | transformation or translation of a Source form, including but 61 | not limited to compiled object code, generated documentation, 62 | and conversions to other media types. 63 | 64 | "Work" shall mean the work of authorship, whether in Source or 65 | Object form, made available under the License, as indicated by a 66 | copyright notice that is included in or attached to the work 67 | (an example is provided in the Appendix below). 68 | 69 | "Derivative Works" shall mean any work, whether in Source or Object 70 | form, that is based on (or derived from) the Work and for which the 71 | editorial revisions, annotations, elaborations, or other modifications 72 | represent, as a whole, an original work of authorship. For the purposes 73 | of this License, Derivative Works shall not include works that remain 74 | separable from, or merely link (or bind by name) to the interfaces of, 75 | the Work and Derivative Works thereof. 76 | 77 | "Contribution" shall mean any work of authorship, including 78 | the original version of the Work and any modifications or additions 79 | to that Work or Derivative Works thereof, that is intentionally 80 | submitted to Licensor for inclusion in the Work by the copyright owner 81 | or by an individual or Legal Entity authorized to submit on behalf of 82 | the copyright owner. For the purposes of this definition, "submitted" 83 | means any form of electronic, verbal, or written communication sent 84 | to the Licensor or its representatives, including but not limited to 85 | communication on electronic mailing lists, source code control systems, 86 | and issue tracking systems that are managed by, or on behalf of, the 87 | Licensor for the purpose of discussing and improving the Work, but 88 | excluding communication that is conspicuously marked or otherwise 89 | designated in writing by the copyright owner as "Not a Contribution." 90 | 91 | "Contributor" shall mean Licensor and any individual or Legal Entity 92 | on behalf of whom a Contribution has been received by Licensor and 93 | subsequently incorporated within the Work. 94 | 95 | 2. Grant of Copyright License. Subject to the terms and conditions of 96 | this License, each Contributor hereby grants to You a perpetual, 97 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 98 | copyright license to reproduce, prepare Derivative Works of, 99 | publicly display, publicly perform, sublicense, and distribute the 100 | Work and such Derivative Works in Source or Object form. 101 | 102 | 3. Grant of Patent License. Subject to the terms and conditions of 103 | this License, each Contributor hereby grants to You a perpetual, 104 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 105 | (except as stated in this section) patent license to make, have made, 106 | use, offer to sell, sell, import, and otherwise transfer the Work, 107 | where such license applies only to those patent claims licensable 108 | by such Contributor that are necessarily infringed by their 109 | Contribution(s) alone or by combination of their Contribution(s) 110 | with the Work to which such Contribution(s) was submitted. If You 111 | institute patent litigation against any entity (including a 112 | cross-claim or counterclaim in a lawsuit) alleging that the Work 113 | or a Contribution incorporated within the Work constitutes direct 114 | or contributory patent infringement, then any patent licenses 115 | granted to You under this License for that Work shall terminate 116 | as of the date such litigation is filed. 117 | 118 | 4. Redistribution. You may reproduce and distribute copies of the 119 | Work or Derivative Works thereof in any medium, with or without 120 | modifications, and in Source or Object form, provided that You 121 | meet the following conditions: 122 | 123 | (a) You must give any other recipients of the Work or 124 | Derivative Works a copy of this License; and 125 | 126 | (b) You must cause any modified files to carry prominent notices 127 | stating that You changed the files; and 128 | 129 | (c) You must retain, in the Source form of any Derivative Works 130 | that You distribute, all copyright, patent, trademark, and 131 | attribution notices from the Source form of the Work, 132 | excluding those notices that do not pertain to any part of 133 | the Derivative Works; and 134 | 135 | (d) If the Work includes a "NOTICE" text file as part of its 136 | distribution, then any Derivative Works that You distribute must 137 | include a readable copy of the attribution notices contained 138 | within such NOTICE file, excluding those notices that do not 139 | pertain to any part of the Derivative Works, in at least one 140 | of the following places: within a NOTICE text file distributed 141 | as part of the Derivative Works; within the Source form or 142 | documentation, if provided along with the Derivative Works; or, 143 | within a display generated by the Derivative Works, if and 144 | wherever such third-party notices normally appear. The contents 145 | of the NOTICE file are for informational purposes only and 146 | do not modify the License. You may add Your own attribution 147 | notices within Derivative Works that You distribute, alongside 148 | or as an addendum to the NOTICE text from the Work, provided 149 | that such additional attribution notices cannot be construed 150 | as modifying the License. 151 | 152 | You may add Your own copyright statement to Your modifications and 153 | may provide additional or different license terms and conditions 154 | for use, reproduction, or distribution of Your modifications, or 155 | for any such Derivative Works as a whole, provided Your use, 156 | reproduction, and distribution of the Work otherwise complies with 157 | the conditions stated in this License. 158 | 159 | 5. Submission of Contributions. Unless You explicitly state otherwise, 160 | any Contribution intentionally submitted for inclusion in the Work 161 | by You to the Licensor shall be under the terms and conditions of 162 | this License, without any additional terms or conditions. 163 | Notwithstanding the above, nothing herein shall supersede or modify 164 | the terms of any separate license agreement you may have executed 165 | with Licensor regarding such Contributions. 166 | 167 | 6. Trademarks. This License does not grant permission to use the trade 168 | names, trademarks, service marks, or product names of the Licensor, 169 | except as required for reasonable and customary use in describing the 170 | origin of the Work and reproducing the content of the NOTICE file. 171 | 172 | 7. Disclaimer of Warranty. Unless required by applicable law or 173 | agreed to in writing, Licensor provides the Work (and each 174 | Contributor provides its Contributions) on an "AS IS" BASIS, 175 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 176 | implied, including, without limitation, any warranties or conditions 177 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 178 | PARTICULAR PURPOSE. You are solely responsible for determining the 179 | appropriateness of using or redistributing the Work and assume any 180 | risks associated with Your exercise of permissions under this License. 181 | 182 | 8. Limitation of Liability. In no event and under no legal theory, 183 | whether in tort (including negligence), contract, or otherwise, 184 | unless required by applicable law (such as deliberate and grossly 185 | negligent acts) or agreed to in writing, shall any Contributor be 186 | liable to You for damages, including any direct, indirect, special, 187 | incidental, or consequential damages of any character arising as a 188 | result of this License or out of the use or inability to use the 189 | Work (including but not limited to damages for loss of goodwill, 190 | work stoppage, computer failure or malfunction, or any and all 191 | other commercial damages or losses), even if such Contributor 192 | has been advised of the possibility of such damages. 193 | 194 | 9. Accepting Warranty or Additional Liability. While redistributing 195 | the Work or Derivative Works thereof, You may choose to offer, 196 | and charge a fee for, acceptance of support, warranty, indemnity, 197 | or other liability obligations and/or rights consistent with this 198 | License. However, in accepting such obligations, You may act only 199 | on Your own behalf and on Your sole responsibility, not on behalf 200 | of any other Contributor, and only if You agree to indemnify, 201 | defend, and hold each Contributor harmless for any liability 202 | incurred by, or claims asserted against, such Contributor by reason 203 | of your accepting any such warranty or additional liability. 204 | 205 | END OF TERMS AND CONDITIONS 206 | ``` 207 | 208 |
209 | 210 |
MIT Software License 211 | 212 | ``` 213 | Permission is hereby granted, free of charge, to any person obtaining a copy 214 | of this software and associated documentation files (the "Software"), to deal 215 | in the Software without restriction, including without limitation the rights 216 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 217 | copies of the Software, and to permit persons to whom the Software is 218 | furnished to do so, subject to the following conditions: 219 | 220 | The above copyright notice and this permission notice shall be included in 221 | all copies or substantial portions of the Software. 222 | 223 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 224 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 225 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 226 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 227 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 228 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 229 | THE SOFTWARE. 230 | ``` 231 | 232 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `w3cli` 2 | 3 | 💾 the `w3` command line interface. 4 | 5 | ## Getting started 6 | 7 | Install the CLI from npm (**requires Node 22 or higher**): 8 | 9 | ```console 10 | npm install -g @web3-storage/w3cli 11 | ``` 12 | 13 | Login with this agent to act on behalf of the account associated with your email address: 14 | 15 | ```console 16 | w3 login alice@example.com 17 | ``` 18 | 19 | Create a new Space for storing your data and register it: 20 | 21 | ```console 22 | w3 space create Documents # pick a good name! 23 | ``` 24 | 25 | If you'd like to learn more about what is going on under the hood with w3up and its use of Spaces, [UCANs](https://ucan.xyz/), and more, check out the `w3up-client` README [here](https://github.com/web3-storage/w3up/tree/main/packages/w3up-client#usage). 26 | 27 | Upload a file or directory: 28 | 29 | ```console 30 | w3 up recipies.txt 31 | ``` 32 | 33 | > ⚠️❗ **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. 34 | 35 | > ⚠️❗ **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. 36 | 37 | ## Commands 38 | 39 | - Basics 40 | - [`w3 login`](#w3-login-email) 41 | - [`w3 up`](#w3-up-path-path) 42 | - [`w3 ls`](#w3-ls) 43 | - [`w3 rm`](#w3-rm-root-cid) 44 | - [`w3 open`](#w3-open-cid) 45 | - [`w3 whoami`](#w3-whoami) 46 | - Space management 47 | - [`w3 space add`](#w3-space-add-proofucan) 48 | - [`w3 space create`](#w3-space-create-name) 49 | - [`w3 space ls`](#w3-space-ls) 50 | - [`w3 space use`](#w3-space-use-did) 51 | - [`w3 space info`](#w3-space-info) 52 | - Capability management 53 | - [`w3 delegation create`](#w3-delegation-create-audience-did) 54 | - [`w3 delegation ls`](#w3-delegation-ls) 55 | - [`w3 delegation revoke`](#w3-delegation-revoke-delegation-cid) 56 | - [`w3 proof add`](#w3-proof-add-proofucan) 57 | - [`w3 proof ls`](#w3-proof-ls) 58 | - Key management 59 | - [`w3 key create`](#w3-key-create) 60 | - UCAN-HTTP Bridge 61 | - [`w3 bridge generate-tokens`](#w3-bridge-generate-tokens) 62 | - Advanced usage 63 | - [`w3 can blob add`](#w3-can-blob-add-path) 64 | - [`w3 can blob ls`](#w3-can-blob-ls) 65 | - [`w3 can blob rm`](#w3-can-blob-rm-multihash) 66 | - [`w3 can index add`](#w3-can-index-add-cid) 67 | - [`w3 can space info`](#w3-can-space-info-did) coming soon! 68 | - [`w3 can space recover`](#w3-can-space-recover-email) coming soon! 69 | - [`w3 can upload add`](#w3-can-upload-add-root-cid-shard-cid-shard-cid) 70 | - [`w3 can upload ls`](#w3-can-upload-ls) 71 | - [`w3 can upload rm`](#w3-can-upload-rm-root-cid) 72 | 73 | --- 74 | 75 | ### `w3 login [email]` 76 | 77 | Authenticate this agent with your email address to get access to all capabilities that had been delegated to it. 78 | 79 | ### `w3 up [path...]` 80 | 81 | Upload file(s) to web3.storage. The IPFS Content ID (CID) for your files is calculated on your machine, and sent up along with your files. web3.storage makes your content available on the IPFS network 82 | 83 | - `--no-wrap` Don't wrap input files with a directory. 84 | - `-H, --hidden` Include paths that start with ".". 85 | - `-c, --car` File is a CAR file. 86 | - `--shard-size` Shard uploads into CAR files of approximately this size in bytes. 87 | - `--concurrent-requests` Send up to this many CAR shards concurrently. 88 | 89 | ### `w3 ls` 90 | 91 | List all the uploads registered in the current space. 92 | 93 | - `--json` Format as newline delimited JSON 94 | - `--shards` Pretty print with shards in output 95 | 96 | ### `w3 rm ` 97 | 98 | Remove an upload from the uploads listing. Note that this command does not remove the data from the IPFS network, nor does it remove it from space storage (by default). 99 | 100 | - `--shards` Also remove all shards referenced by the upload from the store. Use with caution and ensure other uploads do not reference the same shards. 101 | 102 | ### `w3 open ` 103 | 104 | Open a CID on https://w3s.link in your browser. You can also pass a CID and a path. 105 | 106 | ```bash 107 | # opens a browser to https://w3s.link/ipfs/bafybeidluj5ub7okodgg5v6l4x3nytpivvcouuxgzuioa6vodg3xt2uqle 108 | w3 open bafybeidluj5ub7okodgg5v6l4x3nytpivvcouuxgzuioa6vodg3xt2uqle 109 | 110 | # opens a browser to https://w3s.link/ipfs/bafybeidluj5ub7okodgg5v6l4x3nytpivvcouuxgzuioa6vodg3xt2uqle/olizilla.png 111 | w3 open bafybeidluj5ub7okodgg5v6l4x3nytpivvcouuxgzuioa6vodg3xt2uqle/olizilla.png 112 | ``` 113 | 114 | ### `w3 whoami` 115 | 116 | Print information about the current agent. 117 | 118 | ### `w3 space add ` 119 | 120 | Add a space to the agent. The proof is a CAR encoded UCAN delegating capabilities over a space to _this_ agent. 121 | 122 | `proof` is a filesystem path to a CAR encoded UCAN, as generated by `w3 delegation create` _or_ a base64 identity CID string as created by `w3 delegation create --base64`. 123 | 124 | ### `w3 space create [name]` 125 | 126 | Create a new w3 space with an optional name. 127 | 128 | ### `w3 space ls` 129 | 130 | List spaces known to the agent. 131 | 132 | ### `w3 space use ` 133 | 134 | Set the current space in use by the agent. 135 | 136 | ### `w3 space info` 137 | 138 | Get information about a space (by default the current space) from the service, including 139 | which providers the space is currently registered with. 140 | 141 | - `--space` The space to get information about. Defaults to the current space. 142 | - `--json` Format as newline delimited JSON 143 | 144 | ### `w3 delegation create ` 145 | 146 | Create a delegation to the passed audience for the given abilities with the _current_ space as the resource. 147 | 148 | - `--can` A capability to delegate. To specify more than one capability, use this option more than once. 149 | - `--name` Human readable name for the audience receiving the delegation. 150 | - `--type` Type of the audience receiving the delegation, one of: device, app, service. 151 | - `--output` Path of file to write the exported delegation data to. 152 | - `--base64` Format as base64 identity CID string. Useful when saving it as an environment variable. 153 | 154 | ```bash 155 | # delegate space/info to did:key:z6M..., output as a CAR 156 | w3 delegation create did:key:z6M... --can space/info --output ./info.ucan 157 | 158 | # delegate admin capabilities to did:key:z6M..., output as a string 159 | w3 delegation create did:key:z6M... --can 'space/*' --can 'upload/*' --can 'filecoin/*' --base64 160 | 161 | # delegate write (not remove) capabilities to did:key:z6M..., output as a string 162 | w3 delegation create did:key:z6M... \ 163 | --can 'space/blob/add' \ 164 | --can 'upload/add' \ 165 | --can 'filecoin/offer' \ 166 | --base64 167 | ``` 168 | 169 | ### `w3 delegation ls` 170 | 171 | List delegations created by this agent for others. 172 | 173 | - `--json` Format as newline delimited JSON 174 | 175 | ### `w3 delegation revoke ` 176 | 177 | Revoke a delegation by CID. 178 | 179 | - `--proof` Name of a file containing the delegation and any additional proofs needed to prove authority to revoke 180 | 181 | ### `w3 proof add ` 182 | 183 | Add a proof delegated to this agent. The proof is a CAR encoded delegation to _this_ agent. Note: you probably want to use `w3 space add` unless you know the delegation you received targets a resource _other_ than a w3 space. 184 | 185 | ### `w3 proof ls` 186 | 187 | List proofs of delegated capabilities. Proofs are delegations with an audience matching the agent DID. 188 | 189 | - `--json` Format as newline delimited JSON 190 | 191 | ### `w3 key create` 192 | 193 | Print a new key pair. Does not change your current signing key 194 | 195 | - `--json` Export as dag-json 196 | 197 | ### `w3 bridge generate-tokens` 198 | 199 | Generate tokens that can be used as the `X-Auth-Secret` and `Authorization` headers required to use the UCAN-HTTP bridge. 200 | 201 | See the [UCAN Bridge specification](https://github.com/web3-storage/specs/blob/main/w3-ucan-bridge.md) for more information 202 | on how these are expected to be used. 203 | 204 | - `--can` One or more abilities to delegate. 205 | - `--expiration` Unix timestamp (in seconds) when the delegation is no longer valid. Zero indicates no expiration. 206 | - `--json` If set, output JSON suitable to splat into the `headers` field of a `fetch` request. 207 | 208 | ### `w3 can blob add [path]` 209 | 210 | Store a blob file to the service. 211 | 212 | ### `w3 can blob ls` 213 | 214 | List blobs in the current space. 215 | 216 | - `--json` Format as newline delimited JSON 217 | - `--size` The desired number of results to return 218 | - `--cursor` An opaque string included in a prior upload/list response that allows the service to provide the next "page" of results 219 | 220 | ### `w3 can blob rm ` 221 | 222 | Remove a blob from the store by base58btc encoded multihash. 223 | 224 | ### `w3 can space info ` 225 | 226 | ### `w3 can space recover ` 227 | 228 | ### `w3 can upload add [shard-cid...]` 229 | 230 | Register an upload - a DAG with the given root data CID that is stored in the given shard(s), identified by CID. 231 | 232 | ### `w3 can upload ls` 233 | 234 | List uploads in the current space. 235 | 236 | - `--json` Format as newline delimited JSON 237 | - `--shards` Pretty print with shards in output 238 | - `--size` The desired number of results to return 239 | - `--cursor` An opaque string included in a prior upload/list response that allows the service to provide the next "page" of results 240 | - `--pre` If true, return the page of results preceding the cursor 241 | 242 | ### `w3 can upload rm ` 243 | 244 | Remove an upload from the current space's upload list. Does not remove blobs from the store. 245 | 246 | ## Environment Variables 247 | 248 | ### `W3_PRINCIPAL` 249 | 250 | Set the key `w3` should use to sign ucan invocations. By default `w3` will generate a new Ed25519 key on first run and store it. Set it along with a custom `W3_STORE_NAME` to manage multiple custom keys and profiles. Trying to use an existing store with different keys will fail. 251 | 252 | You can generate Ed25519 keys with [`ucan-key`](https://github.com/olizilla/ucan-key) e.g. `npx ucan-key ed` 253 | 254 | **Usage** 255 | 256 | ```bash 257 | W3_PRINCIPAL=$(npx ucan-key ed --json | jq -r .key) W3_STORE_NAME="other" w3 whoami 258 | did:key:z6Mkf7bvSNgoXk67Ubhie8QMurN9E4yaCCGBzXow78zxnmuB 259 | ``` 260 | 261 | Default _unset_, a random Ed25519 key is generated. 262 | 263 | ### `W3_STORE_NAME` 264 | 265 | Allows you to use `w3` with different profiles. You could use it to log in with different emails and keep the delegations separate. 266 | 267 | `w3` stores state to disk using the [`conf`](https://github.com/sindresorhus/conf) module. `W3_STORE_NAME` sets the conf [`configName`](https://github.com/sindresorhus/conf#configname) option. 268 | 269 | Default `w3cli` 270 | 271 | ### `W3UP_SERVICE_URL` 272 | 273 | `w3` will use the w3up service at https://up.web3.storage. If you would like 274 | to use a different w3up-compatible service, set `W3UP_SERVICE_DID` and `W3UP_SERVICE_URL` environment variables to set the service DID and URL endpoint. 275 | 276 | Default `https://up.web3.storage` 277 | 278 | ### `W3UP_SERVICE_DID` 279 | 280 | `w3` will use the w3up `did:web:web3.storage` as the service did. If you would like 281 | to use a different w3up-compatible service, set `W3UP_SERVICE_DID` and `W3UP_SERVICE_URL` environment variables to set the service DID and URL endpoint. 282 | 283 | Default `did:web:web3.storage` 284 | 285 | ## FAQ 286 | 287 | ### Where are my keys and delegations stored? 288 | 289 | In the system default user config directory: 290 | 291 | - macOS: `~/Library/Preferences/w3access` 292 | - Windows: `%APPDATA%\w3access\Config` (for example, `C:\Users\USERNAME\AppData\Roaming\w3access\Config`) 293 | - Linux: `~/.config/w3access` (or `$XDG_CONFIG_HOME/w3access`) 294 | 295 | ## Contributing 296 | 297 | Feel free to join in. All welcome. Please read our [contributing guidelines](https://github.com/web3-storage/w3cli/blob/main/CONTRIBUTING.md) and/or [open an issue](https://github.com/web3-storage/w3cli/issues)! 298 | 299 | ## License 300 | 301 | Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3cli/blob/main/LICENSE.md) 302 | -------------------------------------------------------------------------------- /account.js: -------------------------------------------------------------------------------- 1 | import open from 'open' 2 | import { confirm } from '@inquirer/prompts' 3 | import * as Account from '@web3-storage/w3up-client/account' 4 | import * as Result from '@web3-storage/w3up-client/result' 5 | import * as DidMailto from '@web3-storage/did-mailto' 6 | import { authorize } from '@web3-storage/capabilities/access' 7 | import { base64url } from 'multiformats/bases/base64' 8 | import { select } from '@inquirer/prompts' 9 | import { getClient } from './lib.js' 10 | import ora from 'ora' 11 | 12 | /** 13 | * @typedef {Awaited>['ok']&{}} View 14 | */ 15 | 16 | export const OAuthProviderGitHub = 'github' 17 | const OAuthProviders = /** @type {const} */ ([OAuthProviderGitHub]) 18 | 19 | /** @type {Record} */ 20 | const GitHubOauthClientIDs = { 21 | 'did:web:web3.storage': 'Ov23li0xr95ocCkZiwaD', 22 | 'did:web:staging.web3.storage': 'Ov23liDKQB1ePrcGy5HI', 23 | } 24 | 25 | /** @param {import('@web3-storage/w3up-client/types').DID} serviceID */ 26 | const getGithubOAuthClientID = serviceID => { 27 | const id = process.env.GITHUB_OAUTH_CLIENT_ID || GitHubOauthClientIDs[serviceID] 28 | if (!id) throw new Error(`missing OAuth client ID for: ${serviceID}`) 29 | return id 30 | } 31 | 32 | /** 33 | * @param {DidMailto.EmailAddress} [email] 34 | * @param {object} [options] 35 | * @param {boolean} [options.github] 36 | */ 37 | export const login = async (email, options) => { 38 | let method 39 | if (email) { 40 | method = 'email' 41 | } else if (options?.github) { 42 | method = 'github' 43 | } else { 44 | method = await select({ 45 | message: 'How do you want to login?', 46 | choices: [ 47 | { name: 'Via Email', value: 'email' }, 48 | { name: 'Via GitHub', value: 'github' }, 49 | ], 50 | }) 51 | } 52 | 53 | if (method === 'email' && email) { 54 | await loginWithClient(email, await getClient()) 55 | } else if (method === 'github') { 56 | await oauthLoginWithClient(OAuthProviderGitHub, await getClient()) 57 | } else { 58 | console.error('Error: please provide email address or specify flag for alternate login method') 59 | process.exit(1) 60 | } 61 | } 62 | 63 | /** 64 | * @param {DidMailto.EmailAddress} email 65 | * @param {import('@web3-storage/w3up-client').Client} client 66 | * @returns {Promise} 67 | */ 68 | export const loginWithClient = async (email, client) => { 69 | /** @type {import('ora').Ora|undefined} */ 70 | let spinner 71 | const timeout = setTimeout(() => { 72 | spinner = ora( 73 | `🔗 please click the link sent to ${email} to authorize this agent` 74 | ).start() 75 | }, 1000) 76 | try { 77 | const account = Result.try(await Account.login(client, email)) 78 | 79 | Result.try(await account.save()) 80 | 81 | if (spinner) spinner.stop() 82 | console.log(`⁂ Agent was authorized by ${account.did()}`) 83 | return account 84 | } catch (err) { 85 | if (spinner) spinner.stop() 86 | console.error(err) 87 | process.exit(1) 88 | } finally { 89 | clearTimeout(timeout) 90 | } 91 | } 92 | 93 | /** 94 | * @param {(typeof OAuthProviders)[number]} provider OAuth provider 95 | * @param {import('@web3-storage/w3up-client').Client} client 96 | */ 97 | export const oauthLoginWithClient = async (provider, client) => { 98 | if (provider != OAuthProviderGitHub) { 99 | console.error(`Error: unknown OAuth provider: ${provider}`) 100 | process.exit(1) 101 | } 102 | 103 | /** @type {import('ora').Ora|undefined} */ 104 | let spinner 105 | 106 | try { 107 | // create access/authorize request 108 | const request = await authorize.delegate({ 109 | audience: client.agent.connection.id, 110 | issuer: client.agent.issuer, 111 | // agent that should be granted access 112 | with: client.agent.did(), 113 | // capabilities requested (account access) 114 | nb: { att: [{ can: '*' }] } 115 | }) 116 | const archive = await request.archive() 117 | if (archive.error) { 118 | throw new Error('archiving access authorize delegation', { cause: archive.error }) 119 | } 120 | 121 | const clientID = getGithubOAuthClientID(client.agent.connection.id.did()) 122 | const state = base64url.encode(archive.ok) 123 | const loginURL = `https://github.com/login/oauth/authorize?scope=read:user,user:email&client_id=${clientID}&state=${state}` 124 | 125 | if (await confirm({ message: 'Open the GitHub login URL in your default browser?' })) { 126 | spinner = ora('Waiting for GitHub authorization to be completed in browser...').start() 127 | await open(loginURL) 128 | } else { 129 | spinner = ora(`Click the link to authenticate with GitHub: ${loginURL}`).start() 130 | } 131 | 132 | const expiration = Math.floor(Date.now() / 1000) + (60 * 15) 133 | const account = Result.unwrap(await Account.externalLogin(client, { request: request.cid, expiration })) 134 | 135 | Result.unwrap(await account.save()) 136 | 137 | if (spinner) spinner.stop() 138 | console.log(`⁂ Agent was authorized by ${account.did()}`) 139 | return account 140 | } catch (err) { 141 | if (spinner) spinner.stop() 142 | console.error(err) 143 | process.exit(1) 144 | } 145 | } 146 | 147 | /** 148 | * 149 | */ 150 | export const list = async () => { 151 | const client = await getClient() 152 | const accounts = Object.values(Account.list(client)) 153 | for (const account of accounts) { 154 | console.log(account.did()) 155 | } 156 | 157 | if (accounts.length === 0) { 158 | console.log( 159 | '⁂ Agent has not been authorized yet. Try `w3 login` to authorize this agent with your account.' 160 | ) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /api.ts: -------------------------------------------------------------------------------- 1 | export * from '@ucanto/interface' 2 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import sade from 'sade' 4 | import open from 'open' 5 | import updateNotifier from 'update-notifier' 6 | import { getPkg } from './lib.js' 7 | import { 8 | Account, 9 | Space, 10 | Coupon, 11 | Bridge, 12 | accessClaim, 13 | addSpace, 14 | listSpaces, 15 | useSpace, 16 | spaceInfo, 17 | createDelegation, 18 | listDelegations, 19 | revokeDelegation, 20 | addProof, 21 | listProofs, 22 | upload, 23 | remove, 24 | list, 25 | whoami, 26 | usageReport, 27 | getPlan, 28 | createKey, 29 | reset, 30 | } from './index.js' 31 | import { 32 | blobAdd, 33 | blobList, 34 | blobRemove, 35 | indexAdd, 36 | storeAdd, 37 | storeList, 38 | storeRemove, 39 | uploadAdd, 40 | uploadList, 41 | uploadRemove, 42 | filecoinInfo, 43 | } from './can.js' 44 | 45 | const pkg = getPkg() 46 | 47 | updateNotifier({ pkg }).notify({ isGlobal: true }) 48 | 49 | const cli = sade('w3') 50 | 51 | cli 52 | .version(pkg.version) 53 | .example('login user@example.com') 54 | .example('up path/to/files') 55 | 56 | cli 57 | .command('login [email]') 58 | .example('login user@example.com') 59 | .describe( 60 | 'Authenticate this agent with your email address to gain access to all capabilities that have been delegated to it.' 61 | ) 62 | .option('--github', 'Use GitHub to authenticate. GitHub developer accounts automatically gain access to a trial plan.', false) 63 | .action(Account.login) 64 | 65 | cli 66 | .command('plan get [email]') 67 | .example('plan get user@example.com') 68 | .describe('Displays plan given account is on') 69 | .action(getPlan) 70 | 71 | cli 72 | .command('account ls') 73 | .alias('account list') 74 | .describe('List accounts this agent has been authorized to act on behalf of.') 75 | .action(Account.list) 76 | 77 | cli 78 | .command('up [file]') 79 | .alias('upload', 'put') 80 | .describe('Store a file(s) to the service and register an upload.') 81 | .option('-H, --hidden', 'Include paths that start with ".".', false) 82 | .option('-c, --car', 'File is a CAR file.', false) 83 | .option('--wrap', "Wrap single input file in a directory. Has no effect on directory or CAR uploads. Pass --no-wrap to disable.", true) 84 | .option('--json', 'Format as newline delimited JSON', false) 85 | .option('--verbose', 'Output more details.', false) 86 | .option( 87 | '--shard-size', 88 | 'Shard uploads into CAR files of approximately this size in bytes.' 89 | ) 90 | .option( 91 | '--concurrent-requests', 92 | 'Send up to this many CAR shards concurrently.' 93 | ) 94 | .action(upload) 95 | 96 | cli 97 | .command('open ') 98 | .describe('Open CID on https://w3s.link') 99 | .action((cid) => open(`https://w3s.link/ipfs/${cid}`)) 100 | 101 | cli 102 | .command('ls') 103 | .alias('list') 104 | .describe('List uploads in the current space') 105 | .option('--json', 'Format as newline delimited JSON') 106 | .option('--shards', 'Pretty print with shards in output') 107 | .action(list) 108 | 109 | cli 110 | .command('rm ') 111 | .example('rm bafy...') 112 | .describe( 113 | 'Remove an upload from the uploads listing. Pass --shards to delete the actual data if you are sure no other uploads need them' 114 | ) 115 | .option( 116 | '--shards', 117 | 'Remove all shards referenced by the upload from the store. Use with caution and ensure other uploads do not reference the same shards.' 118 | ) 119 | .action(remove) 120 | 121 | cli 122 | .command('whoami') 123 | .describe('Print information about the current agent.') 124 | .action(whoami) 125 | 126 | cli 127 | .command('space create [name]') 128 | .describe('Create a new w3 space') 129 | .option('-nr, --no-recovery', 'Skips recovery key setup') 130 | .option('-n, --no-caution', 'Prints out recovery key without confirmation') 131 | .option('-nc, --no-customer', 'Skip billing setup') 132 | .option('-c, --customer ', 'Billing account email') 133 | .option('-na, --no-account', 'Skip account setup') 134 | .option('-a, --account ', 'Managing account email') 135 | .option('-ag, --authorize-gateway-services ', 'Authorize Gateways to serve the content uploaded to this space, e.g: \'[{"id":"did:key:z6Mki...","serviceEndpoint":"https://gateway.example.com"}]\'') 136 | .option('-nga, --no-gateway-authorization', 'Skip Gateway Authorization') 137 | .action((name, options) => { 138 | let authorizeGatewayServices = [] 139 | if (options['authorize-gateway-services']) { 140 | try { 141 | authorizeGatewayServices = JSON.parse(options['authorize-gateway-services']) 142 | } catch (err) { 143 | console.error('Invalid JSON format for --authorize-gateway-services') 144 | process.exit(1) 145 | } 146 | } 147 | 148 | const parsedOptions = { 149 | ...options, 150 | // if defined it means we want to skip gateway authorization, so the client will not validate the gateway services 151 | skipGatewayAuthorization: options['gateway-authorization'] === false || options['gateway-authorization'] === undefined, 152 | // default to empty array if not set, so the client will validate the gateway services 153 | authorizeGatewayServices: authorizeGatewayServices || [], 154 | } 155 | 156 | return Space.create(name, parsedOptions) 157 | }) 158 | 159 | cli 160 | .command('space provision [name]') 161 | .describe('Associating space with a billing account') 162 | .option('-c, --customer', 'The email address of the billing account') 163 | .option('--coupon', 'Coupon URL to provision space with') 164 | .option('-p, -password', 'Coupon password') 165 | .option( 166 | '-p, --provider', 167 | 'The storage provider to associate with this space.' 168 | ) 169 | .action(Space.provision) 170 | 171 | cli 172 | .command('space add ') 173 | .describe( 174 | 'Import a space from a proof: a CAR encoded UCAN delegating capabilities to this agent. proof is a filesystem path, or a base64 encoded cid string.' 175 | ) 176 | .action(addSpace) 177 | 178 | cli 179 | .command('space ls') 180 | .describe('List spaces known to the agent') 181 | .action(listSpaces) 182 | 183 | cli 184 | .command('space info') 185 | .describe('Show information about a space. Defaults to the current space.') 186 | .option('-s, --space', 'The space to print information about.') 187 | .option('--json', 'Format as newline delimited JSON') 188 | .action(spaceInfo) 189 | 190 | cli 191 | .command('space use ') 192 | .describe('Set the current space in use by the agent') 193 | .action(useSpace) 194 | 195 | cli 196 | .command('coupon create ') 197 | .option('--password', 'Password for created coupon.') 198 | .option('-c, --can', 'One or more abilities to delegate.') 199 | .option( 200 | '-e, --expiration', 201 | 'Unix timestamp when the delegation is no longer valid. Zero indicates no expiration.', 202 | 0 203 | ) 204 | .option( 205 | '-o, --output', 206 | 'Path of file to write the exported delegation data to.' 207 | ) 208 | .action(Coupon.issue) 209 | 210 | cli 211 | .command('bridge generate-tokens ') 212 | .option('-c, --can', 'One or more abilities to delegate.') 213 | .option( 214 | '-e, --expiration', 215 | 'Unix timestamp (in seconds) when the delegation is no longer valid. Zero indicates no expiration.', 216 | 0 217 | ) 218 | .option( 219 | '-j, --json', 220 | 'If set, output JSON suitable to spread into the `headers` field of a `fetch` request.' 221 | ) 222 | .action(Bridge.generateTokens) 223 | 224 | 225 | cli 226 | .command('delegation create ') 227 | .describe( 228 | 'Output a CAR encoded UCAN that delegates capabilities to the audience for the current space.' 229 | ) 230 | .option('-c, --can', 'One or more abilities to delegate.') 231 | .option( 232 | '-n, --name', 233 | 'Human readable name for the audience receiving the delegation.' 234 | ) 235 | .option( 236 | '-t, --type', 237 | 'Type of the audience receiving the delegation, one of: device, app, service.' 238 | ) 239 | .option( 240 | '-e, --expiration', 241 | 'Unix timestamp when the delegation is no longer valid. Zero indicates no expiration.', 242 | 0 243 | ) 244 | .option( 245 | '-o, --output', 246 | 'Path of file to write the exported delegation data to.' 247 | ) 248 | .option( 249 | '--base64', 250 | 'Format as base64 identity CID string. Useful when saving it as an environment variable.' 251 | ) 252 | .action(createDelegation) 253 | 254 | cli 255 | .command('delegation ls') 256 | .describe('List delegations created by this agent for others.') 257 | .option('--json', 'Format as newline delimited JSON') 258 | .action(listDelegations) 259 | 260 | cli 261 | .command('delegation revoke ') 262 | .describe('Revoke a delegation by CID.') 263 | .option( 264 | '-p, --proof', 265 | 'Name of a file containing the delegation and any additional proofs needed to prove authority to revoke' 266 | ) 267 | .action(revokeDelegation) 268 | 269 | cli 270 | .command('proof add ') 271 | .describe('Add a proof delegated to this agent.') 272 | .option('--json', 'Format as newline delimited JSON') 273 | .option('--dry-run', 'Decode and view the proof but do not add it') 274 | .action(addProof) 275 | 276 | cli 277 | .command('proof ls') 278 | .describe('List proofs of capabilities delegated to this agent.') 279 | .option('--json', 'Format as newline delimited JSON') 280 | .action(listProofs) 281 | 282 | cli 283 | .command('usage report') 284 | .describe('Display report of current space usage in bytes.') 285 | .option('--human', 'Format human readable values.', false) 286 | .option('--json', 'Format as newline delimited JSON', false) 287 | .action(usageReport) 288 | 289 | cli 290 | .command('can access claim') 291 | .describe('Claim delegated capabilities for the authorized account.') 292 | .action(accessClaim) 293 | 294 | cli 295 | .command('can blob add [data-path]') 296 | .describe('Store a blob with the service.') 297 | .action(blobAdd) 298 | 299 | cli 300 | .command('can blob ls') 301 | .describe('List blobs in the current space.') 302 | .option('--json', 'Format as newline delimited JSON') 303 | .option('--size', 'The desired number of results to return') 304 | .option( 305 | '--cursor', 306 | 'An opaque string included in a prior blob/list response that allows the service to provide the next "page" of results' 307 | ) 308 | .action(blobList) 309 | 310 | cli 311 | .command('can blob rm ') 312 | .describe('Remove a blob from the store by base58btc encoded multihash.') 313 | .action(blobRemove) 314 | 315 | cli 316 | .command('can index add ') 317 | .describe('Register an "index" with the service.') 318 | .action(indexAdd) 319 | 320 | cli 321 | .command('can store add ') 322 | .describe('Store a CAR file with the service.') 323 | .action(storeAdd) 324 | 325 | cli 326 | .command('can store ls') 327 | .describe('List CAR files in the current space.') 328 | .option('--json', 'Format as newline delimited JSON') 329 | .option('--size', 'The desired number of results to return') 330 | .option( 331 | '--cursor', 332 | 'An opaque string included in a prior store/list response that allows the service to provide the next "page" of results' 333 | ) 334 | .option('--pre', 'If true, return the page of results preceding the cursor') 335 | .action(storeList) 336 | 337 | cli 338 | .command('can store rm ') 339 | .describe('Remove a CAR shard from the store.') 340 | .action(storeRemove) 341 | 342 | cli 343 | .command('can upload add ') 344 | .describe( 345 | 'Register an upload - a DAG with the given root data CID that is stored in the given CAR shard(s), identified by CAR CIDs.' 346 | ) 347 | .action(uploadAdd) 348 | 349 | cli 350 | .command('can upload ls') 351 | .describe('List uploads in the current space.') 352 | .option('--json', 'Format as newline delimited JSON') 353 | .option('--shards', 'Pretty print with shards in output') 354 | .option('--size', 'The desired number of results to return') 355 | .option( 356 | '--cursor', 357 | 'An opaque string included in a prior upload/list response that allows the service to provide the next "page" of results' 358 | ) 359 | .option('--pre', 'If true, return the page of results preceding the cursor') 360 | .action(uploadList) 361 | 362 | cli 363 | .command('can upload rm ') 364 | .describe('Remove an upload from the uploads listing.') 365 | .action(uploadRemove) 366 | 367 | cli 368 | .command('can filecoin info ') 369 | .describe('Get filecoin information for given PieceCid.') 370 | .action(filecoinInfo) 371 | 372 | cli 373 | .command('key create') 374 | .describe('Generate and print a new ed25519 key pair. Does not change your current signing key.') 375 | .option('--json', 'output as json') 376 | .action(createKey) 377 | 378 | cli 379 | .command('reset') 380 | .describe('Remove all proofs/delegations from the store but retain the agent DID.') 381 | .action(reset) 382 | 383 | // show help text if no command provided 384 | cli.command('help [cmd]', 'Show help text', { default: true }).action((cmd) => { 385 | try { 386 | cli.help(cmd) 387 | } catch (err) { 388 | console.log(` 389 | ERROR 390 | Invalid command: ${cmd} 391 | 392 | Run \`$ w3 --help\` for more info. 393 | `) 394 | process.exit(1) 395 | } 396 | }) 397 | 398 | cli.parse(process.argv) 399 | -------------------------------------------------------------------------------- /bridge.js: -------------------------------------------------------------------------------- 1 | import * as DID from '@ipld/dag-ucan/did' 2 | import * as Account from './account.js' 3 | import * as Space from './space.js' 4 | import { getClient } from './lib.js' 5 | import * as ucanto from '@ucanto/core' 6 | import { base64url } from 'multiformats/bases/base64' 7 | import cryptoRandomString from 'crypto-random-string' 8 | 9 | export { Account, Space } 10 | 11 | /** 12 | * @typedef {object} BridgeGenerateTokensOptions 13 | * @property {string} resource 14 | * @property {string[]|string} [can] 15 | * @property {number} [expiration] 16 | * @property {boolean} [json] 17 | * 18 | * @param {string} resource 19 | * @param {BridgeGenerateTokensOptions} options 20 | */ 21 | export const generateTokens = async ( 22 | resource, 23 | { can = ['store/add', 'upload/add'], expiration, json } 24 | ) => { 25 | const client = await getClient() 26 | 27 | const resourceDID = DID.parse(resource) 28 | const abilities = can ? [can].flat() : [] 29 | if (!abilities.length) { 30 | console.error('Error: missing capabilities for delegation') 31 | process.exit(1) 32 | } 33 | 34 | const capabilities = /** @type {ucanto.API.Capabilities} */ ( 35 | abilities.map((can) => ({ can, with: resourceDID.did() })) 36 | ) 37 | 38 | const password = cryptoRandomString({ length: 32 }) 39 | 40 | const coupon = await client.coupon.issue({ 41 | capabilities, 42 | expiration: expiration === 0 ? Infinity : expiration, 43 | password, 44 | }) 45 | 46 | const { ok: bytes, error } = await coupon.archive() 47 | if (!bytes) { 48 | console.error(error) 49 | return process.exit(1) 50 | } 51 | const xAuthSecret = base64url.encode(new TextEncoder().encode(password)) 52 | const authorization = base64url.encode(bytes) 53 | 54 | if (json) { 55 | console.log(JSON.stringify({ 56 | "X-Auth-Secret": xAuthSecret, 57 | "Authorization": authorization 58 | })) 59 | } else { 60 | console.log(` 61 | X-Auth-Secret header: ${xAuthSecret} 62 | 63 | Authorization header: ${authorization} 64 | `) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /can.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import fs from 'node:fs' 3 | import { Readable } from 'node:stream' 4 | import * as Link from 'multiformats/link' 5 | import * as raw from 'multiformats/codecs/raw' 6 | import { base58btc } from 'multiformats/bases/base58' 7 | import * as Digest from 'multiformats/hashes/digest' 8 | import { Piece } from '@web3-storage/data-segment' 9 | import ora from 'ora' 10 | import { 11 | getClient, 12 | uploadListResponseToString, 13 | storeListResponseToString, 14 | filecoinInfoToString, 15 | parseCarLink, 16 | streamToBlob, 17 | blobListResponseToString, 18 | } from './lib.js' 19 | 20 | /** 21 | * @param {string} [blobPath] 22 | */ 23 | export async function blobAdd(blobPath) { 24 | const client = await getClient() 25 | 26 | const spinner = ora('Reading data').start() 27 | /** @type {Blob} */ 28 | let blob 29 | try { 30 | blob = await streamToBlob( 31 | /** @type {ReadableStream} */ 32 | (Readable.toWeb(blobPath ? fs.createReadStream(blobPath) : process.stdin)) 33 | ) 34 | } catch (/** @type {any} */ err) { 35 | spinner.fail(`Error: failed to read data: ${err.message}`) 36 | process.exit(1) 37 | } 38 | 39 | spinner.start('Storing') 40 | const { digest } = await client.capability.blob.add(blob, { 41 | receiptsEndpoint: client._receiptsEndpoint.toString() 42 | }) 43 | const cid = Link.create(raw.code, digest) 44 | spinner.stopAndPersist({ symbol: '⁂', text: `Stored ${base58btc.encode(digest.bytes)} (${cid})` }) 45 | } 46 | 47 | /** 48 | * Print out all the blobs in the current space. 49 | * 50 | * @param {object} opts 51 | * @param {boolean} [opts.json] 52 | * @param {string} [opts.cursor] 53 | * @param {number} [opts.size] 54 | */ 55 | export async function blobList(opts = {}) { 56 | const client = await getClient() 57 | const listOptions = {} 58 | if (opts.size) { 59 | listOptions.size = parseInt(String(opts.size)) 60 | } 61 | if (opts.cursor) { 62 | listOptions.cursor = opts.cursor 63 | } 64 | 65 | const spinner = ora('Listing Blobs').start() 66 | const res = await client.capability.blob.list(listOptions) 67 | spinner.stop() 68 | console.log(blobListResponseToString(res, opts)) 69 | } 70 | 71 | /** 72 | * @param {string} digestStr 73 | */ 74 | export async function blobRemove(digestStr) { 75 | const spinner = ora(`Removing ${digestStr}`).start() 76 | let digest 77 | try { 78 | digest = Digest.decode(base58btc.decode(digestStr)) 79 | } catch { 80 | spinner.fail(`Error: "${digestStr}" is not a base58btc encoded multihash`) 81 | process.exit(1) 82 | } 83 | const client = await getClient() 84 | try { 85 | await client.capability.blob.remove(digest) 86 | spinner.stopAndPersist({ symbol: '⁂', text: `Removed ${digestStr}` }) 87 | } catch (/** @type {any} */ err) { 88 | spinner.fail(`Error: blob remove failed: ${err.message ?? err}`) 89 | console.error(err) 90 | process.exit(1) 91 | } 92 | } 93 | 94 | /** 95 | * @param {string} cidStr 96 | */ 97 | export async function indexAdd(cidStr) { 98 | const client = await getClient() 99 | 100 | const spinner = ora('Adding').start() 101 | const cid = parseCarLink(cidStr) 102 | if (!cid) { 103 | spinner.fail(`Error: "${cidStr}" is not a valid index CID`) 104 | process.exit(1) 105 | } 106 | await client.capability.index.add(cid) 107 | spinner.stopAndPersist({ symbol: '⁂', text: `Added index ${cid}` }) 108 | } 109 | 110 | /** 111 | * @param {string} carPath 112 | */ 113 | export async function storeAdd(carPath) { 114 | const client = await getClient() 115 | 116 | const spinner = ora('Reading CAR').start() 117 | /** @type {Blob} */ 118 | let blob 119 | try { 120 | const data = await fs.promises.readFile(carPath) 121 | blob = new Blob([data]) 122 | } catch (/** @type {any} */ err) { 123 | spinner.fail(`Error: failed to read CAR: ${err.message}`) 124 | process.exit(1) 125 | } 126 | 127 | spinner.start('Storing') 128 | const cid = await client.capability.store.add(blob) 129 | console.log(cid.toString()) 130 | spinner.stopAndPersist({ symbol: '⁂', text: `Stored ${cid}` }) 131 | } 132 | 133 | /** 134 | * Print out all the CARs in the current space. 135 | * 136 | * @param {object} opts 137 | * @param {boolean} [opts.json] 138 | * @param {string} [opts.cursor] 139 | * @param {number} [opts.size] 140 | * @param {boolean} [opts.pre] 141 | */ 142 | export async function storeList(opts = {}) { 143 | const client = await getClient() 144 | const listOptions = {} 145 | if (opts.size) { 146 | listOptions.size = parseInt(String(opts.size)) 147 | } 148 | if (opts.cursor) { 149 | listOptions.cursor = opts.cursor 150 | } 151 | if (opts.pre) { 152 | listOptions.pre = opts.pre 153 | } 154 | 155 | const spinner = ora('Listing CARs').start() 156 | const res = await client.capability.store.list(listOptions) 157 | spinner.stop() 158 | console.log(storeListResponseToString(res, opts)) 159 | } 160 | 161 | /** 162 | * @param {string} cidStr 163 | */ 164 | export async function storeRemove(cidStr) { 165 | const shard = parseCarLink(cidStr) 166 | if (!shard) { 167 | console.error(`Error: ${cidStr} is not a CAR CID`) 168 | process.exit(1) 169 | } 170 | const client = await getClient() 171 | try { 172 | await client.capability.store.remove(shard) 173 | } catch (/** @type {any} */ err) { 174 | console.error(`Store remove failed: ${err.message ?? err}`) 175 | console.error(err) 176 | process.exit(1) 177 | } 178 | } 179 | 180 | /** 181 | * @param {string} root 182 | * @param {string} shard 183 | * @param {object} opts 184 | * @param {string[]} opts._ 185 | */ 186 | export async function uploadAdd(root, shard, opts) { 187 | const client = await getClient() 188 | 189 | let rootCID 190 | try { 191 | rootCID = Link.parse(root) 192 | } catch (/** @type {any} */ err) { 193 | console.error(`Error: failed to parse root CID: ${root}: ${err.message}`) 194 | process.exit(1) 195 | } 196 | 197 | /** @type {import('@web3-storage/w3up-client/types').CARLink[]} */ 198 | const shards = [] 199 | for (const str of [shard, ...opts._]) { 200 | try { 201 | shards.push(Link.parse(str)) 202 | } catch (/** @type {any} */ err) { 203 | console.error(`Error: failed to parse shard CID: ${str}: ${err.message}`) 204 | process.exit(1) 205 | } 206 | } 207 | 208 | const spinner = ora('Adding upload').start() 209 | await client.capability.upload.add(rootCID, shards) 210 | spinner.stopAndPersist({ symbol: '⁂', text: `Upload added ${rootCID}` }) 211 | } 212 | 213 | /** 214 | * Print out all the uploads in the current space. 215 | * 216 | * @param {object} opts 217 | * @param {boolean} [opts.json] 218 | * @param {boolean} [opts.shards] 219 | * @param {string} [opts.cursor] 220 | * @param {number} [opts.size] 221 | * @param {boolean} [opts.pre] 222 | */ 223 | export async function uploadList(opts = {}) { 224 | const client = await getClient() 225 | const listOptions = {} 226 | if (opts.size) { 227 | listOptions.size = parseInt(String(opts.size)) 228 | } 229 | if (opts.cursor) { 230 | listOptions.cursor = opts.cursor 231 | } 232 | if (opts.pre) { 233 | listOptions.pre = opts.pre 234 | } 235 | 236 | const spinner = ora('Listing uploads').start() 237 | const res = await client.capability.upload.list(listOptions) 238 | spinner.stop() 239 | console.log(uploadListResponseToString(res, opts)) 240 | } 241 | 242 | /** 243 | * Remove the upload from the upload list. 244 | * 245 | * @param {string} rootCid 246 | */ 247 | export async function uploadRemove(rootCid) { 248 | let root 249 | try { 250 | root = Link.parse(rootCid.trim()) 251 | } catch (/** @type {any} */ err) { 252 | console.error(`Error: ${rootCid} is not a CID`) 253 | process.exit(1) 254 | } 255 | const client = await getClient() 256 | try { 257 | await client.capability.upload.remove(root) 258 | } catch (/** @type {any} */ err) { 259 | console.error(`Upload remove failed: ${err.message ?? err}`) 260 | console.error(err) 261 | process.exit(1) 262 | } 263 | } 264 | 265 | /** 266 | * Get filecoin information for given PieceCid. 267 | * 268 | * @param {string} pieceCid 269 | * @param {object} opts 270 | * @param {boolean} [opts.json] 271 | * @param {boolean} [opts.raw] 272 | */ 273 | export async function filecoinInfo(pieceCid, opts) { 274 | let pieceInfo 275 | try { 276 | pieceInfo = Piece.fromString(pieceCid) 277 | } catch (/** @type {any} */ err) { 278 | console.error(`Error: ${pieceCid} is not a Link`) 279 | process.exit(1) 280 | } 281 | const spinner = ora('Getting filecoin info').start() 282 | const client = await getClient() 283 | const info = await client.capability.filecoin.info(pieceInfo.link) 284 | if (info.out.error) { 285 | spinner.fail(`Error: failed to get filecoin info: ${info.out.error.message}`) 286 | process.exit(1) 287 | } 288 | spinner.stop() 289 | console.log(filecoinInfoToString(info.out.ok, opts)) 290 | } 291 | -------------------------------------------------------------------------------- /coupon.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import * as DID from '@ipld/dag-ucan/did' 3 | import * as Account from './account.js' 4 | import * as Space from './space.js' 5 | import { getClient } from './lib.js' 6 | import * as ucanto from '@ucanto/core' 7 | 8 | export { Account, Space } 9 | 10 | /** 11 | * @typedef {object} CouponIssueOptions 12 | * @property {string} customer 13 | * @property {string[]|string} [can] 14 | * @property {string} [password] 15 | * @property {number} [expiration] 16 | * @property {string} [output] 17 | * 18 | * @param {string} customer 19 | * @param {CouponIssueOptions} options 20 | */ 21 | export const issue = async ( 22 | customer, 23 | { can = 'provider/add', expiration, password, output } 24 | ) => { 25 | const client = await getClient() 26 | 27 | const audience = DID.parse(customer) 28 | const abilities = can ? [can].flat() : [] 29 | if (!abilities.length) { 30 | console.error('Error: missing capabilities for delegation') 31 | process.exit(1) 32 | } 33 | 34 | const capabilities = /** @type {ucanto.API.Capabilities} */ ( 35 | abilities.map((can) => ({ can, with: audience.did() })) 36 | ) 37 | 38 | const coupon = await client.coupon.issue({ 39 | capabilities, 40 | expiration: expiration === 0 ? Infinity : expiration, 41 | password, 42 | }) 43 | 44 | const { ok: bytes, error } = await coupon.archive() 45 | if (!bytes) { 46 | console.error(error) 47 | return process.exit(1) 48 | } 49 | 50 | if (output) { 51 | await fs.writeFile(output, bytes) 52 | } else { 53 | process.stdout.write(bytes) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /dialog.js: -------------------------------------------------------------------------------- 1 | import { useState, useKeypress, createPrompt, isEnterKey } from '@inquirer/core' 2 | import chalk from 'chalk' 3 | import ansiEscapes from 'ansi-escapes' 4 | /** 5 | * @typedef {'concealed'|'revealed'|'validating'|'done'} Status 6 | * @typedef {object} MnemonicOptions 7 | * @property {string[]} secret 8 | * @property {string} message 9 | * @property {string} [prefix] 10 | * @property {string} [revealMessage] 11 | * @property {string} [submitMessage] 12 | * @property {string} [validateMessage] 13 | * @property {string} [exitMessage] 14 | */ 15 | export const mnemonic = createPrompt( 16 | /** 17 | * @param {MnemonicOptions} config 18 | * @param {(answer: unknown) => void} done 19 | */ 20 | ( 21 | { 22 | prefix = '🔑', 23 | message, 24 | secret, 25 | revealMessage = 'When ready, hit enter to reveal the key', 26 | submitMessage = 'Please save the key and then hit enter to continue', 27 | validateMessage = 'Please type or paste key to ensure it is correct', 28 | exitMessage = 'Key matched!', 29 | }, 30 | done 31 | ) => { 32 | const [status, setStatus] = useState(/** @type {Status} */ ('concealed')) 33 | const [input, setInput] = useState('') 34 | 35 | useKeypress((key, io) => { 36 | switch (status) { 37 | case 'concealed': 38 | if (isEnterKey(key)) { 39 | setStatus('revealed') 40 | } 41 | return 42 | case 'revealed': 43 | if (isEnterKey(key)) { 44 | setStatus('validating') 45 | } 46 | return 47 | case 'validating': { 48 | // if line break is pasted or typed we want interpret it as 49 | // a space character, this is why we write current input back 50 | // to the terminal with a trailing space. That way user will 51 | // still be able to edit the input afterwards. 52 | if (isEnterKey(key)) { 53 | io.write(`${input} `) 54 | } else { 55 | // If current input matches the secret we are done. 56 | const input = parseInput(io.line) 57 | setInput(io.line) 58 | if (input.join('') === secret.join('')) { 59 | setStatus('done') 60 | done({}) 61 | } 62 | } 63 | return 64 | } 65 | default: 66 | return done({}) 67 | } 68 | }) 69 | 70 | switch (status) { 71 | case 'concealed': 72 | return show({ 73 | prefix, 74 | message, 75 | key: conceal(secret), 76 | hint: revealMessage, 77 | }) 78 | case 'revealed': 79 | return show({ prefix, message, key: secret, hint: submitMessage }) 80 | case 'validating': 81 | return show({ 82 | prefix, 83 | message, 84 | key: diff(parseInput(input), secret), 85 | hint: validateMessage, 86 | }) 87 | case 'done': 88 | return show({ 89 | prefix, 90 | message, 91 | key: conceal(secret, CORRECT), 92 | hint: exitMessage, 93 | }) 94 | } 95 | } 96 | ) 97 | 98 | /** 99 | * @param {string} input 100 | */ 101 | const parseInput = (input) => input.trim().split(/[\n\s]+/) 102 | 103 | /** 104 | * @param {string[]} input 105 | * @param {string[]} key 106 | */ 107 | const diff = (input, key) => { 108 | const source = input.join('') 109 | let offset = 0 110 | const output = [] 111 | for (const word of key) { 112 | let delta = [] 113 | for (const expect of word) { 114 | const actual = source[offset] 115 | if (actual === expect) { 116 | delta.push(CORRECT) 117 | } else if (actual != undefined) { 118 | delta.push(chalk.inverse.strikethrough.red(actual)) 119 | } else { 120 | delta.push(CONCEAL) 121 | } 122 | offset++ 123 | } 124 | output.push(delta.join('')) 125 | } 126 | 127 | return output 128 | } 129 | 130 | /** 131 | * @param {object} state 132 | * @param {string} state.prefix 133 | * @param {string} state.message 134 | * @param {string[]} state.key 135 | * @param {string} state.hint 136 | */ 137 | const show = ({ prefix, message, key, hint }) => 138 | `${prefix} ${message}\n\n${key.join(' ')}\n\n${hint}${ansiEscapes.cursorHide}` 139 | 140 | /** 141 | * @param {string[]} key 142 | * @param {string} [char] 143 | */ 144 | const conceal = (key, char = CONCEAL) => 145 | key.map((word) => char.repeat(word.length)) 146 | 147 | const CONCEAL = '█' 148 | const CORRECT = chalk.inverse('•') 149 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { pipeline } from 'node:stream/promises' 3 | import { Readable } from 'node:stream' 4 | import ora from 'ora' 5 | import { CID } from 'multiformats/cid' 6 | import { base64 } from 'multiformats/bases/base64' 7 | import { identity } from 'multiformats/hashes/identity' 8 | import * as DID from '@ipld/dag-ucan/did' 9 | import * as dagJSON from '@ipld/dag-json' 10 | import { CarWriter } from '@ipld/car' 11 | import { filesFromPaths } from 'files-from-path' 12 | import * as Account from './account.js' 13 | 14 | import { spaceAccess } from '@web3-storage/w3up-client/capability/access' 15 | import { AgentData } from '@web3-storage/access' 16 | import * as Space from './space.js' 17 | import { 18 | getClient, 19 | getStore, 20 | checkPathsExist, 21 | filesize, 22 | filesizeMB, 23 | readProof, 24 | readProofFromBytes, 25 | uploadListResponseToString, 26 | startOfLastMonth, 27 | pieceHasher, 28 | } from './lib.js' 29 | import * as ucanto from '@ucanto/core' 30 | import { ed25519 } from '@ucanto/principal' 31 | import chalk from 'chalk' 32 | export * as Coupon from './coupon.js' 33 | export * as Bridge from './bridge.js' 34 | export { Account, Space } 35 | import ago from 's-ago' 36 | 37 | /** 38 | * 39 | */ 40 | export async function accessClaim() { 41 | const client = await getClient() 42 | await client.capability.access.claim() 43 | } 44 | 45 | /** 46 | * @param {string} email 47 | */ 48 | export const getPlan = async (email = '') => { 49 | const client = await getClient() 50 | const account = 51 | email === '' 52 | ? await Space.selectAccount(client) 53 | : await Space.useAccount(client, { email }) 54 | 55 | if (account) { 56 | const { ok: plan, error } = await account.plan.get() 57 | if (plan) { 58 | console.log(`⁂ ${plan.product}`) 59 | } else if (error?.name === 'PlanNotFound') { 60 | console.log('⁂ no plan has been selected yet') 61 | } else { 62 | console.error(`Failed to get plan - ${error.message}`) 63 | process.exit(1) 64 | } 65 | } else { 66 | process.exit(1) 67 | } 68 | } 69 | 70 | /** 71 | * @param {`${string}@${string}`} email 72 | * @param {object} [opts] 73 | * @param {import('@ucanto/interface').Ability[]|import('@ucanto/interface').Ability} [opts.can] 74 | */ 75 | export async function authorize(email, opts = {}) { 76 | const client = await getClient() 77 | const capabilities = 78 | opts.can != null ? [opts.can].flat().map((can) => ({ can })) : undefined 79 | /** @type {import('ora').Ora|undefined} */ 80 | let spinner 81 | setTimeout(() => { 82 | spinner = ora( 83 | `🔗 please click the link we sent to ${email} to authorize this agent` 84 | ).start() 85 | }, 1000) 86 | try { 87 | await client.authorize(email, { capabilities }) 88 | } catch (err) { 89 | if (spinner) spinner.stop() 90 | console.error(err) 91 | process.exit(1) 92 | } 93 | if (spinner) spinner.stop() 94 | console.log(`⁂ agent authorized to use capabilities delegated to ${email}`) 95 | } 96 | 97 | /** 98 | * @param {string} firstPath 99 | * @param {{ 100 | * _: string[], 101 | * car?: boolean 102 | * hidden?: boolean 103 | * json?: boolean 104 | * verbose?: boolean 105 | * wrap?: boolean 106 | * 'shard-size'?: number 107 | * 'concurrent-requests'?: number 108 | * }} [opts] 109 | */ 110 | export async function upload(firstPath, opts) { 111 | /** @type {import('@web3-storage/w3up-client/types').FileLike[]} */ 112 | let files 113 | /** @type {number} */ 114 | let totalSize // -1 when unknown size (input from stdin) 115 | /** @type {import('ora').Ora} */ 116 | let spinner 117 | const client = await getClient() 118 | if (firstPath) { 119 | const paths = checkPathsExist([firstPath, ...(opts?._ ?? [])]) 120 | const hidden = !!opts?.hidden 121 | spinner = ora({ text: 'Reading files', isSilent: opts?.json }).start() 122 | const localFiles = await filesFromPaths(paths, { hidden }) 123 | totalSize = localFiles.reduce((total, f) => total + f.size, 0) 124 | files = localFiles 125 | spinner.stopAndPersist({ 126 | text: `${files.length} file${files.length === 1 ? '' : 's'} ${chalk.dim( 127 | filesize(totalSize) 128 | )}`, 129 | }) 130 | 131 | if (opts?.car && files.length > 1) { 132 | console.error('Error: multiple CAR files not supported') 133 | process.exit(1) 134 | } 135 | } else { 136 | spinner = ora({ text: 'Reading from stdin', isSilent: opts?.json }).start() 137 | files = [{ 138 | name: 'stdin', 139 | stream: () => 140 | /** @type {ReadableStream} */ 141 | (Readable.toWeb(process.stdin)) 142 | }] 143 | totalSize = -1 144 | opts = opts ?? { _: [] } 145 | opts.wrap = false 146 | } 147 | 148 | spinner.start('Storing') 149 | /** @type {(o?: import('@web3-storage/w3up-client/src/types').UploadOptions) => Promise} */ 150 | const uploadFn = opts?.car 151 | ? client.uploadCAR.bind(client, files[0]) 152 | : files.length === 1 && opts?.wrap === false 153 | ? client.uploadFile.bind(client, files[0]) 154 | : client.uploadDirectory.bind(client, files) 155 | 156 | let totalSent = 0 157 | const getStoringMessage = () => totalSize == -1 158 | // for unknown size, display the amount sent so far 159 | ? `Storing ${filesizeMB(totalSent)}` 160 | // for known size, display percentage of total size that has been sent 161 | : `Storing ${Math.min(Math.round((totalSent / totalSize) * 100), 100)}%` 162 | 163 | const root = await uploadFn({ 164 | pieceHasher, 165 | onShardStored: ({ cid, size, piece }) => { 166 | totalSent += size 167 | if (opts?.verbose) { 168 | spinner.stopAndPersist({ 169 | text: `${cid} ${chalk.dim(filesizeMB(size))}\n${chalk.dim( 170 | ' └── ' 171 | )}Piece CID: ${piece}`, 172 | }) 173 | spinner.start(getStoringMessage()) 174 | } else { 175 | spinner.text = getStoringMessage() 176 | } 177 | opts?.json && 178 | opts?.verbose && 179 | console.log(dagJSON.stringify({ shard: cid, size, piece })) 180 | }, 181 | shardSize: opts?.['shard-size'] && parseInt(String(opts?.['shard-size'])), 182 | concurrentRequests: 183 | opts?.['concurrent-requests'] && 184 | parseInt(String(opts?.['concurrent-requests'])), 185 | receiptsEndpoint: client._receiptsEndpoint.toString() 186 | }) 187 | spinner.stopAndPersist({ 188 | symbol: '⁂', 189 | text: `Stored ${files.length} file${files.length === 1 ? '' : 's'}`, 190 | }) 191 | console.log( 192 | opts?.json ? dagJSON.stringify({ root }) : `⁂ https://w3s.link/ipfs/${root}` 193 | ) 194 | } 195 | 196 | /** 197 | * Print out all the uploads in the current space. 198 | * 199 | * @param {object} opts 200 | * @param {boolean} [opts.json] 201 | * @param {boolean} [opts.shards] 202 | */ 203 | export async function list(opts = {}) { 204 | const client = await getClient() 205 | let count = 0 206 | /** @type {import('@web3-storage/w3up-client/types').UploadListSuccess|undefined} */ 207 | let res 208 | do { 209 | res = await client.capability.upload.list({ cursor: res?.cursor }) 210 | if (!res) throw new Error('missing upload list response') 211 | count += res.results.length 212 | if (res.results.length) { 213 | console.log(uploadListResponseToString(res, opts)) 214 | } 215 | } while (res.cursor && res.results.length) 216 | 217 | if (count === 0 && !opts.json) { 218 | console.log('⁂ No uploads in space') 219 | console.log('⁂ Try out `w3 up ` to upload some') 220 | } 221 | } 222 | /** 223 | * @param {string} rootCid 224 | * @param {object} opts 225 | * @param {boolean} [opts.shards] 226 | */ 227 | export async function remove(rootCid, opts) { 228 | let root 229 | try { 230 | root = CID.parse(rootCid.trim()) 231 | } catch (/** @type {any} */ err) { 232 | console.error(`Error: ${rootCid} is not a CID`) 233 | process.exit(1) 234 | } 235 | const client = await getClient() 236 | 237 | try { 238 | await client.remove(root, opts) 239 | } catch (/** @type {any} */ err) { 240 | console.error(`Remove failed: ${err.message ?? err}`) 241 | console.error(err) 242 | process.exit(1) 243 | } 244 | } 245 | 246 | /** 247 | * @param {string} name 248 | */ 249 | export async function createSpace(name) { 250 | const client = await getClient() 251 | const space = await client.createSpace(name, { 252 | skipGatewayAuthorization: true 253 | }) 254 | await client.setCurrentSpace(space.did()) 255 | console.log(space.did()) 256 | } 257 | 258 | /** 259 | * @param {string} proofPathOrCid 260 | */ 261 | export async function addSpace(proofPathOrCid) { 262 | const client = await getClient() 263 | 264 | let cid 265 | try { 266 | cid = CID.parse(proofPathOrCid, base64) 267 | } catch (/** @type {any} */ err) { 268 | if (err?.message?.includes('Unexpected end of data')) { 269 | console.error(`Error: failed to read proof. The string has been truncated.`) 270 | process.exit(1) 271 | } 272 | /* otherwise, try as path */ 273 | } 274 | 275 | let delegation 276 | if (cid) { 277 | if (cid.multihash.code !== identity.code) { 278 | console.error(`Error: failed to read proof. Must be identity CID. Fetching of remote proof CARs not supported by this command yet`) 279 | process.exit(1) 280 | } 281 | delegation = await readProofFromBytes(cid.multihash.digest) 282 | } else { 283 | delegation = await readProof(proofPathOrCid) 284 | } 285 | 286 | const space = await client.addSpace(delegation) 287 | console.log(space.did()) 288 | } 289 | 290 | /** 291 | * 292 | */ 293 | export async function listSpaces() { 294 | const client = await getClient() 295 | const current = client.currentSpace() 296 | for (const space of client.spaces()) { 297 | const prefix = current && current.did() === space.did() ? '* ' : ' ' 298 | console.log(`${prefix}${space.did()} ${space.name ?? ''}`) 299 | } 300 | } 301 | 302 | /** 303 | * @param {string} did 304 | */ 305 | export async function useSpace(did) { 306 | const client = await getClient() 307 | const spaces = client.spaces() 308 | const space = 309 | spaces.find((s) => s.did() === did) ?? spaces.find((s) => s.name === did) 310 | if (!space) { 311 | console.error(`Error: space not found: ${did}`) 312 | process.exit(1) 313 | } 314 | await client.setCurrentSpace(space.did()) 315 | console.log(space.did()) 316 | } 317 | 318 | /** 319 | * @param {object} opts 320 | * @param {import('@web3-storage/w3up-client/types').DID} [opts.space] 321 | * @param {string} [opts.json] 322 | */ 323 | export async function spaceInfo(opts) { 324 | const client = await getClient() 325 | const spaceDID = opts.space ?? client.currentSpace()?.did() 326 | if (!spaceDID) { 327 | throw new Error( 328 | 'no current space and no space given: please use --space to specify a space or select one using "space use"' 329 | ) 330 | } 331 | 332 | /** @type {import('@web3-storage/access/types').SpaceInfoResult} */ 333 | let info 334 | try { 335 | info = await client.capability.space.info(spaceDID) 336 | } catch (/** @type {any} */ err) { 337 | // if the space was not known to the service then that's ok, there's just 338 | // no info to print about it. Don't make it look like something is wrong, 339 | // just print the space DID since that's all we know. 340 | if (err.name === 'SpaceUnknown') { 341 | // @ts-expect-error spaceDID should be a did:key 342 | info = { did: spaceDID } 343 | } else { 344 | return console.log(`Error getting info about ${spaceDID}: ${err.message}`) 345 | } 346 | } 347 | 348 | const space = client.spaces().find((s) => s.did() === spaceDID) 349 | const name = space ? space.name : undefined 350 | 351 | if (opts.json) { 352 | console.log(JSON.stringify({ ...info, name }, null, 4)) 353 | } else { 354 | const providers = info.providers?.join(', ') ?? '' 355 | console.log(` 356 | DID: ${info.did} 357 | Providers: ${providers || chalk.dim('none')} 358 | Name: ${name ?? chalk.dim('none')}`) 359 | } 360 | } 361 | 362 | /** 363 | * @param {string} audienceDID 364 | * @param {object} opts 365 | * @param {string[]|string} opts.can 366 | * @param {string} [opts.name] 367 | * @param {string} [opts.type] 368 | * @param {number} [opts.expiration] 369 | * @param {string} [opts.output] 370 | * @param {string} [opts.with] 371 | * @param {boolean} [opts.base64] 372 | */ 373 | export async function createDelegation(audienceDID, opts) { 374 | const client = await getClient() 375 | 376 | if (client.currentSpace() == null) { 377 | throw new Error('no current space, use `w3 space create` to create one.') 378 | } 379 | const audience = DID.parse(audienceDID) 380 | 381 | const abilities = opts.can ? [opts.can].flat() : Object.keys(spaceAccess) 382 | if (!abilities.length) { 383 | console.error('Error: missing capabilities for delegation') 384 | process.exit(1) 385 | } 386 | const audienceMeta = {} 387 | if (opts.name) audienceMeta.name = opts.name 388 | if (opts.type) audienceMeta.type = opts.type 389 | const expiration = opts.expiration || Infinity 390 | 391 | // @ts-expect-error createDelegation should validate abilities 392 | const delegation = await client.createDelegation(audience, abilities, { 393 | expiration, 394 | audienceMeta, 395 | }) 396 | 397 | const { writer, out } = CarWriter.create() 398 | const dest = opts.output ? fs.createWriteStream(opts.output) : process.stdout 399 | 400 | pipeline( 401 | out, 402 | async function* maybeBaseEncode(src) { 403 | const chunks = [] 404 | for await (const chunk of src) { 405 | if (!opts.base64) { 406 | yield chunk 407 | } else { 408 | chunks.push(chunk) 409 | } 410 | } 411 | if (!opts.base64) return 412 | const blob = new Blob(chunks) 413 | const bytes = new Uint8Array(await blob.arrayBuffer()) 414 | const idCid = CID.createV1(ucanto.CAR.code, identity.digest(bytes)) 415 | yield idCid.toString(base64) 416 | }, 417 | dest 418 | ) 419 | 420 | for (const block of delegation.export()) { 421 | // @ts-expect-error 422 | await writer.put(block) 423 | } 424 | await writer.close() 425 | } 426 | 427 | /** 428 | * @param {object} opts 429 | * @param {boolean} [opts.json] 430 | */ 431 | export async function listDelegations(opts) { 432 | const client = await getClient() 433 | const delegations = client.delegations() 434 | if (opts.json) { 435 | for (const delegation of delegations) { 436 | console.log( 437 | JSON.stringify({ 438 | cid: delegation.cid.toString(), 439 | audience: delegation.audience.did(), 440 | capabilities: delegation.capabilities.map((c) => ({ 441 | with: c.with, 442 | can: c.can, 443 | })), 444 | }) 445 | ) 446 | } 447 | } else { 448 | for (const delegation of delegations) { 449 | console.log(delegation.cid.toString()) 450 | console.log(` audience: ${delegation.audience.did()}`) 451 | for (const capability of delegation.capabilities) { 452 | console.log(` with: ${capability.with}`) 453 | console.log(` can: ${capability.can}`) 454 | } 455 | } 456 | } 457 | } 458 | 459 | /** 460 | * @param {string} delegationCid 461 | * @param {object} opts 462 | * @param {string} [opts.proof] 463 | */ 464 | export async function revokeDelegation(delegationCid, opts) { 465 | const client = await getClient() 466 | let proof 467 | try { 468 | if (opts.proof) { 469 | proof = await readProof(opts.proof) 470 | } 471 | } catch (/** @type {any} */ err) { 472 | console.log(`Error: reading proof: ${err.message}`) 473 | process.exit(1) 474 | } 475 | let cid 476 | try { 477 | // TODO: we should validate that this is a UCANLink 478 | cid = ucanto.parseLink(delegationCid.trim()) 479 | } catch (/** @type {any} */ err) { 480 | console.error(`Error: invalid CID: ${delegationCid}: ${err.message}`) 481 | process.exit(1) 482 | } 483 | const result = await client.revokeDelegation( 484 | /** @type {import('@ucanto/interface').UCANLink} */ (cid), 485 | { proofs: proof ? [proof] : [] } 486 | ) 487 | if (result.ok) { 488 | console.log(`⁂ delegation ${delegationCid} revoked`) 489 | } else { 490 | console.error(`Error: revoking ${delegationCid}: ${result.error?.message}`) 491 | process.exit(1) 492 | } 493 | } 494 | 495 | /** 496 | * @param {string} proofPath 497 | * @param {{ json?: boolean, 'dry-run'?: boolean }} [opts] 498 | */ 499 | export async function addProof(proofPath, opts) { 500 | const client = await getClient() 501 | let proof 502 | try { 503 | proof = await readProof(proofPath) 504 | if (!opts?.['dry-run']) { 505 | await client.addProof(proof) 506 | } 507 | } catch (/** @type {any} */ err) { 508 | console.log(`Error: ${err.message}`) 509 | process.exit(1) 510 | } 511 | if (opts?.json) { 512 | console.log(JSON.stringify(proof.toJSON())) 513 | } else { 514 | console.log(proof.cid.toString()) 515 | console.log(` issuer: ${proof.issuer.did()}`) 516 | for (const capability of proof.capabilities) { 517 | console.log(` with: ${capability.with}`) 518 | console.log(` can: ${capability.can}`) 519 | } 520 | } 521 | } 522 | 523 | /** 524 | * @param {object} opts 525 | * @param {boolean} [opts.json] 526 | */ 527 | export async function listProofs(opts) { 528 | const client = await getClient() 529 | const proofs = client.proofs() 530 | if (opts.json) { 531 | for (const proof of proofs) { 532 | console.log(JSON.stringify(proof)) 533 | } 534 | } else { 535 | for (const proof of proofs) { 536 | console.log(chalk.dim(`# ${proof.cid.toString()}`)) 537 | console.log(`iss: ${chalk.cyanBright(proof.issuer.did())}`) 538 | console.log(`aud: ${chalk.cyanBright(proof.audience.did())}`) 539 | if (proof.expiration !== Infinity) { 540 | console.log( 541 | `exp: ${chalk.yellow(proof.expiration)} ${chalk.dim( 542 | ` # expires ${ago(new Date(proof.expiration * 1000))}` 543 | )}` 544 | ) 545 | } 546 | console.log('att:') 547 | for (const capability of proof.capabilities) { 548 | console.log(` - can: ${chalk.magentaBright(capability.can)}`) 549 | console.log(` with: ${chalk.green(capability.with)}`) 550 | if (capability.nb) { 551 | console.log(` nb: ${JSON.stringify(capability.nb)}`) 552 | } 553 | } 554 | if (proof.facts.length > 0) { 555 | console.log('fct:') 556 | } 557 | for (const fact of proof.facts) { 558 | console.log(` - ${JSON.stringify(fact)}`) 559 | } 560 | console.log('') 561 | } 562 | console.log( 563 | chalk.dim( 564 | `# ${proofs.length} proof${ 565 | proofs.length === 1 ? '' : 's' 566 | } for ${client.agent.did()}` 567 | ) 568 | ) 569 | } 570 | } 571 | 572 | /** 573 | * 574 | */ 575 | export async function whoami() { 576 | const client = await getClient() 577 | console.log(client.did()) 578 | } 579 | 580 | /** 581 | * @param {object} [opts] 582 | * @param {boolean} [opts.human] 583 | * @param {boolean} [opts.json] 584 | */ 585 | export async function usageReport(opts) { 586 | const client = await getClient() 587 | const now = new Date() 588 | const period = { 589 | // we may not have done a snapshot for this month _yet_, so get report from last month -> now 590 | from: startOfLastMonth(now), 591 | to: now, 592 | } 593 | const failures = [] 594 | let total = 0 595 | for await (const result of getSpaceUsageReports( 596 | client, 597 | period 598 | )) { 599 | if ('error' in result) { 600 | failures.push(result) 601 | } else { 602 | if (opts?.json) { 603 | const { account, provider, space, size } = result 604 | console.log( 605 | dagJSON.stringify({ 606 | account, 607 | provider, 608 | space, 609 | size, 610 | reportedAt: now.toISOString(), 611 | }) 612 | ) 613 | } else { 614 | const { account, provider, space, size } = result 615 | console.log(` Account: ${account}`) 616 | console.log(`Provider: ${provider}`) 617 | console.log(` Space: ${space}`) 618 | console.log( 619 | ` Size: ${opts?.human ? filesize(size.final) : size.final}\n` 620 | ) 621 | } 622 | total += result.size.final 623 | } 624 | } 625 | if (!opts?.json) { 626 | console.log(` Total: ${opts?.human ? filesize(total) : total}`) 627 | if (failures.length) { 628 | console.warn(``) 629 | console.warn(` WARNING: there were ${failures.length} errors getting usage reports for some spaces.`) 630 | console.warn(` This may happen if your agent does not have usage/report authorization for a space.`) 631 | console.warn(` These spaces were not included in the usage report total:`) 632 | for (const fail of failures) { 633 | console.warn(` * space: ${fail.space}`) 634 | // @ts-expect-error error is unknown 635 | console.warn(` error: ${fail.error?.message}`) 636 | console.warn(` account: ${fail.account}`) 637 | } 638 | } 639 | } 640 | } 641 | 642 | /** 643 | * @param {import('@web3-storage/w3up-client').Client} client 644 | * @param {{ from: Date, to: Date }} period 645 | */ 646 | async function* getSpaceUsageReports(client, period) { 647 | for (const account of Object.values(client.accounts())) { 648 | const subscriptions = await client.capability.subscription.list( 649 | account.did() 650 | ) 651 | for (const { consumers } of subscriptions.results) { 652 | for (const space of consumers) { 653 | /** @type {import('@web3-storage/upload-client/types').UsageReportSuccess} */ 654 | let result 655 | try { 656 | result = await client.capability.usage.report(space, period) 657 | } catch (error) { 658 | yield { error, space, period, consumers, account: account.did() } 659 | continue 660 | } 661 | for (const [, report] of Object.entries(result)) { 662 | yield { account: account.did(), ...report } 663 | } 664 | } 665 | } 666 | } 667 | } 668 | 669 | /** 670 | * @param {{ json: boolean }} options 671 | */ 672 | export async function createKey({ json }) { 673 | const signer = await ed25519.generate() 674 | const key = ed25519.format(signer) 675 | if (json) { 676 | console.log(JSON.stringify({ did: signer.did(), key }, null, 2)) 677 | } else { 678 | console.log(`# ${signer.did()}`) 679 | console.log(key) 680 | } 681 | } 682 | 683 | export const reset = async () => { 684 | const store = getStore() 685 | const exportData = await store.load() 686 | if (exportData) { 687 | let data = AgentData.fromExport(exportData) 688 | // do not reset the principal 689 | data = await AgentData.create({ principal: data.principal, meta: data.meta }) 690 | await store.save(data.export()) 691 | } 692 | console.log('⁂ Agent reset.') 693 | } 694 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { Worker } from 'node:worker_threads' 4 | import { fileURLToPath } from 'node:url' 5 | // @ts-expect-error no typings :( 6 | import tree from 'pretty-tree' 7 | import { importDAG } from '@ucanto/core/delegation' 8 | import { connect } from '@ucanto/client' 9 | import * as CAR from '@ucanto/transport/car' 10 | import * as HTTP from '@ucanto/transport/http' 11 | import * as Signer from '@ucanto/principal/ed25519' 12 | import * as Link from 'multiformats/link' 13 | import { base58btc } from 'multiformats/bases/base58' 14 | import * as Digest from 'multiformats/hashes/digest' 15 | import * as raw from 'multiformats/codecs/raw' 16 | import { parse } from '@ipld/dag-ucan/did' 17 | import * as dagJSON from '@ipld/dag-json' 18 | import { create } from '@web3-storage/w3up-client' 19 | import { StoreConf } from '@web3-storage/w3up-client/stores/conf' 20 | import { CarReader } from '@ipld/car' 21 | import chalk from 'chalk' 22 | 23 | /** 24 | * @typedef {import('@web3-storage/w3up-client/types').AnyLink} AnyLink 25 | * @typedef {import('@web3-storage/w3up-client/types').CARLink} CARLink 26 | * @typedef {import('@web3-storage/w3up-client/types').FileLike & { size: number }} FileLike 27 | * @typedef {import('@web3-storage/w3up-client/types').BlobListSuccess} BlobListSuccess 28 | * @typedef {import('@web3-storage/w3up-client/types').StoreListSuccess} StoreListSuccess 29 | * @typedef {import('@web3-storage/w3up-client/types').UploadListSuccess} UploadListSuccess 30 | * @typedef {import('@web3-storage/capabilities/types').FilecoinInfoSuccess} FilecoinInfoSuccess 31 | */ 32 | 33 | const __filename = fileURLToPath(import.meta.url) 34 | const __dirname = path.dirname(__filename) 35 | 36 | export function getPkg() { 37 | // @ts-ignore JSON.parse works with Buffer in Node.js 38 | return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url))) 39 | } 40 | 41 | /** @param {string[]|string} paths */ 42 | export function checkPathsExist(paths) { 43 | paths = Array.isArray(paths) ? paths : [paths] 44 | for (const p of paths) { 45 | if (!fs.existsSync(p)) { 46 | console.error(`The path ${path.resolve(p)} does not exist`) 47 | process.exit(1) 48 | } 49 | } 50 | return paths 51 | } 52 | 53 | /** @param {number} bytes */ 54 | export function filesize(bytes) { 55 | if (bytes < 50) return `${bytes}B` // avoid 0.0KB 56 | if (bytes < 50000) return `${(bytes / 1000).toFixed(1)}KB` // avoid 0.0MB 57 | if (bytes < 50000000) return `${(bytes / 1000 / 1000).toFixed(1)}MB` // avoid 0.0GB 58 | return `${(bytes / 1000 / 1000 / 1000).toFixed(1)}GB` 59 | } 60 | 61 | /** @param {number} bytes */ 62 | export function filesizeMB(bytes) { 63 | return `${(bytes / 1000 / 1000).toFixed(1)}MB` 64 | } 65 | 66 | /** Get a configured w3up store used by the CLI. */ 67 | export function getStore() { 68 | return new StoreConf({ profile: process.env.W3_STORE_NAME ?? 'w3cli' }) 69 | } 70 | 71 | /** 72 | * Get a new API client configured from env vars. 73 | */ 74 | export function getClient() { 75 | const store = getStore() 76 | 77 | if (process.env.W3_ACCESS_SERVICE_URL || process.env.W3_UPLOAD_SERVICE_URL) { 78 | console.warn( 79 | chalk.dim( 80 | 'warning: the W3_ACCESS_SERVICE_URL and W3_UPLOAD_SERVICE_URL environment variables are deprecated and will be removed in a future release - please use W3UP_SERVICE_URL instead.' 81 | ) 82 | ) 83 | } 84 | 85 | if (process.env.W3_ACCESS_SERVICE_DID || process.env.W3_UPLOAD_SERVICE_DID) { 86 | console.warn( 87 | chalk.dim( 88 | 'warning: the W3_ACCESS_SERVICE_DID and W3_UPLOAD_SERVICE_DID environment variables are deprecated and will be removed in a future release - please use W3UP_SERVICE_DID instead.' 89 | ) 90 | ) 91 | } 92 | 93 | const accessServiceDID = 94 | process.env.W3UP_SERVICE_DID || process.env.W3_ACCESS_SERVICE_DID 95 | const accessServiceURL = 96 | process.env.W3UP_SERVICE_URL || process.env.W3_ACCESS_SERVICE_URL 97 | const uploadServiceDID = 98 | process.env.W3UP_SERVICE_DID || process.env.W3_UPLOAD_SERVICE_DID 99 | const uploadServiceURL = 100 | process.env.W3UP_SERVICE_URL || process.env.W3_UPLOAD_SERVICE_URL 101 | const receiptsEndpointString = (process.env.W3UP_RECEIPTS_ENDPOINT || process.env.W3_UPLOAD_RECEIPTS_URL) 102 | let receiptsEndpoint 103 | if (receiptsEndpointString) { 104 | receiptsEndpoint = new URL(receiptsEndpointString) 105 | } 106 | 107 | let serviceConf 108 | if ( 109 | accessServiceDID && 110 | accessServiceURL && 111 | uploadServiceDID && 112 | uploadServiceURL 113 | ) { 114 | serviceConf = 115 | /** @type {import('@web3-storage/w3up-client/types').ServiceConf} */ 116 | ({ 117 | access: connect({ 118 | id: parse(accessServiceDID), 119 | codec: CAR.outbound, 120 | channel: HTTP.open({ 121 | url: new URL(accessServiceURL), 122 | method: 'POST', 123 | }), 124 | }), 125 | upload: connect({ 126 | id: parse(uploadServiceDID), 127 | codec: CAR.outbound, 128 | channel: HTTP.open({ 129 | url: new URL(uploadServiceURL), 130 | method: 'POST', 131 | }), 132 | }), 133 | filecoin: connect({ 134 | id: parse(uploadServiceDID), 135 | codec: CAR.outbound, 136 | channel: HTTP.open({ 137 | url: new URL(uploadServiceURL), 138 | method: 'POST', 139 | }), 140 | }), 141 | }) 142 | } 143 | 144 | /** @type {import('@web3-storage/w3up-client/types').ClientFactoryOptions} */ 145 | const createConfig = { store, serviceConf, receiptsEndpoint } 146 | 147 | const principal = process.env.W3_PRINCIPAL 148 | if (principal) { 149 | createConfig.principal = Signer.parse(principal) 150 | } 151 | 152 | return create(createConfig) 153 | } 154 | 155 | /** 156 | * @param {string} path Path to the proof file. 157 | */ 158 | export async function readProof(path) { 159 | let bytes 160 | try { 161 | const buff = await fs.promises.readFile(path) 162 | bytes = new Uint8Array(buff.buffer) 163 | } catch (/** @type {any} */ err) { 164 | console.error(`Error: failed to read proof: ${err.message}`) 165 | process.exit(1) 166 | } 167 | return readProofFromBytes(bytes) 168 | } 169 | 170 | /** 171 | * @param {Uint8Array} bytes Path to the proof file. 172 | */ 173 | export async function readProofFromBytes(bytes) { 174 | const blocks = [] 175 | try { 176 | const reader = await CarReader.fromBytes(bytes) 177 | for await (const block of reader.blocks()) { 178 | blocks.push(block) 179 | } 180 | } catch (/** @type {any} */ err) { 181 | console.error(`Error: failed to parse proof: ${err.message}`) 182 | process.exit(1) 183 | } 184 | try { 185 | // @ts-expect-error 186 | return importDAG(blocks) 187 | } catch (/** @type {any} */ err) { 188 | console.error(`Error: failed to import proof: ${err.message}`) 189 | process.exit(1) 190 | } 191 | } 192 | 193 | /** 194 | * @param {UploadListSuccess} res 195 | * @param {object} [opts] 196 | * @param {boolean} [opts.raw] 197 | * @param {boolean} [opts.json] 198 | * @param {boolean} [opts.shards] 199 | * @param {boolean} [opts.plainTree] 200 | * @returns {string} 201 | */ 202 | export function uploadListResponseToString(res, opts = {}) { 203 | if (opts.json) { 204 | return res.results 205 | .map(({ root, shards }) => dagJSON.stringify({ root, shards })) 206 | .join('\n') 207 | } else if (opts.shards) { 208 | return res.results 209 | .map(({ root, shards }) => { 210 | const treeBuilder = opts.plainTree ? tree.plain : tree 211 | return treeBuilder({ 212 | label: root.toString(), 213 | nodes: [ 214 | { 215 | label: 'shards', 216 | leaf: shards?.map((s) => s.toString()), 217 | }, 218 | ], 219 | })} 220 | ) 221 | .join('\n') 222 | } else { 223 | return res.results.map(({ root }) => root.toString()).join('\n') 224 | } 225 | } 226 | 227 | /** 228 | * @param {BlobListSuccess} res 229 | * @param {object} [opts] 230 | * @param {boolean} [opts.raw] 231 | * @param {boolean} [opts.json] 232 | * @returns {string} 233 | */ 234 | export function blobListResponseToString(res, opts = {}) { 235 | if (opts.json) { 236 | return res.results 237 | .map(({ blob }) => dagJSON.stringify({ blob })) 238 | .join('\n') 239 | } else { 240 | return res.results 241 | .map(({ blob }) => { 242 | const digest = Digest.decode(blob.digest) 243 | const cid = Link.create(raw.code, digest) 244 | return `${base58btc.encode(digest.bytes)} (${cid})` 245 | }) 246 | .join('\n') 247 | } 248 | } 249 | 250 | /** 251 | * @param {StoreListSuccess} res 252 | * @param {object} [opts] 253 | * @param {boolean} [opts.raw] 254 | * @param {boolean} [opts.json] 255 | * @returns {string} 256 | */ 257 | export function storeListResponseToString(res, opts = {}) { 258 | if (opts.json) { 259 | return res.results 260 | .map(({ link, size }) => dagJSON.stringify({ link, size })) 261 | .join('\n') 262 | } else { 263 | return res.results.map(({ link }) => link.toString()).join('\n') 264 | } 265 | } 266 | 267 | /** 268 | * @param {FilecoinInfoSuccess} res 269 | * @param {object} [opts] 270 | * @param {boolean} [opts.raw] 271 | * @param {boolean} [opts.json] 272 | */ 273 | export function filecoinInfoToString(res, opts = {}) { 274 | if (opts.json) { 275 | return res.deals 276 | .map(deal => dagJSON.stringify(({ 277 | aggregate: deal.aggregate.toString(), 278 | provider: deal.provider, 279 | dealId: deal.aux.dataSource.dealID, 280 | inclusion: res.aggregates.find(a => a.aggregate.toString() === deal.aggregate.toString())?.inclusion 281 | }))) 282 | .join('\n') 283 | } else { 284 | if (!res.deals.length) { 285 | return ` 286 | Piece CID: ${res.piece.toString()} 287 | Deals: Piece being aggregated and offered for deal... 288 | ` 289 | } 290 | // not showing inclusion proof as it would just be bytes 291 | return ` 292 | Piece CID: ${res.piece.toString()} 293 | Deals: ${res.deals.map((deal) => ` 294 | Aggregate: ${deal.aggregate.toString()} 295 | Provider: ${deal.provider} 296 | Deal ID: ${deal.aux.dataSource.dealID} 297 | `).join('')} 298 | ` 299 | } 300 | } 301 | 302 | /** 303 | * Return validated CARLink or undefined 304 | * 305 | * @param {AnyLink} cid 306 | */ 307 | export function asCarLink(cid) { 308 | if (cid.version === 1 && cid.code === CAR.codec.code) { 309 | return /** @type {CARLink} */ (cid) 310 | } 311 | } 312 | 313 | /** 314 | * Return validated CARLink type or exit the process with an error code and message 315 | * 316 | * @param {string} cidStr 317 | */ 318 | export function parseCarLink(cidStr) { 319 | try { 320 | return asCarLink(Link.parse(cidStr.trim())) 321 | } catch { 322 | return undefined 323 | } 324 | } 325 | 326 | /** @param {string|number|Date} now */ 327 | const startOfMonth = (now) => { 328 | const d = new Date(now) 329 | d.setUTCDate(1) 330 | d.setUTCHours(0) 331 | d.setUTCMinutes(0) 332 | d.setUTCSeconds(0) 333 | d.setUTCMilliseconds(0) 334 | return d 335 | } 336 | 337 | /** @param {string|number|Date} now */ 338 | export const startOfLastMonth = (now) => { 339 | const d = startOfMonth(now) 340 | d.setUTCMonth(d.getUTCMonth() - 1) 341 | return d 342 | } 343 | 344 | /** @param {ReadableStream} source */ 345 | export const streamToBlob = async source => { 346 | const chunks = /** @type {Uint8Array[]} */ ([]) 347 | await source.pipeTo(new WritableStream({ 348 | write: chunk => { chunks.push(chunk) } 349 | })) 350 | return new Blob(chunks) 351 | } 352 | 353 | const workerPath = path.join(__dirname, 'piece-hasher-worker.js') 354 | 355 | /** @see https://github.com/multiformats/multicodec/pull/331/files */ 356 | const pieceHasherCode = 0x1011 357 | 358 | /** @type {import('multiformats').MultihashHasher} */ 359 | export const pieceHasher = { 360 | code: pieceHasherCode, 361 | name: 'fr32-sha2-256-trunc254-padded-binary-tree', 362 | async digest (input) { 363 | const bytes = await new Promise((resolve, reject) => { 364 | const worker = new Worker(workerPath, { workerData: input }) 365 | worker.on('message', resolve) 366 | worker.on('error', reject) 367 | worker.on('exit', (code) => { 368 | if (code !== 0) reject(new Error(`Piece hasher worker exited with code: ${code}`)) 369 | }) 370 | }) 371 | const digest = 372 | /** @type {import('multiformats').MultihashDigest} */ 373 | (Digest.decode(bytes)) 374 | return digest 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@web3-storage/w3cli", 3 | "type": "module", 4 | "version": "7.12.0", 5 | "license": "(Apache-2.0 AND MIT)", 6 | "description": "💾 w3 command line interface", 7 | "bin": { 8 | "w3": "shim.js", 9 | "w3up": "shim.js" 10 | }, 11 | "scripts": { 12 | "lint": "eslint '**/*.{js,ts}'", 13 | "lint:fix": "eslint --fix '**/*.{js,ts}'", 14 | "check": "tsc --build", 15 | "format": "prettier --write '**/*.{js,ts,yml,json}' --ignore-path .gitignore", 16 | "test": "entail **/*.spec.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/storacha/w3cli.git" 21 | }, 22 | "keywords": [ 23 | "w3", 24 | "web3", 25 | "storage", 26 | "upload", 27 | "cli" 28 | ], 29 | "bugs": { 30 | "url": "https://github.com/storacha/w3cli/issues" 31 | }, 32 | "homepage": "https://github.com/storacha/w3cli#readme", 33 | "devDependencies": { 34 | "@types/update-notifier": "^6.0.5", 35 | "@ucanto/interface": "^10.0.1", 36 | "@ucanto/server": "^10.0.0", 37 | "@web-std/blob": "^3.0.5", 38 | "@web3-storage/eslint-config-w3up": "^1.0.0", 39 | "@web3-storage/sigv4": "^1.0.2", 40 | "@web3-storage/upload-api": "^17.0.0", 41 | "entail": "^2.1.1", 42 | "npm-run-all": "^4.1.5", 43 | "prettier": "^3.0.3", 44 | "typescript": "^5.2.2" 45 | }, 46 | "dependencies": { 47 | "@inquirer/core": "^5.1.1", 48 | "@inquirer/prompts": "^3.3.0", 49 | "@ipld/car": "^5.2.4", 50 | "@ipld/dag-json": "^10.1.5", 51 | "@ipld/dag-ucan": "^3.4.0", 52 | "@ucanto/client": "^9.0.1", 53 | "@ucanto/core": "^10.0.1", 54 | "@ucanto/principal": "^9.0.1", 55 | "@ucanto/transport": "^9.1.1", 56 | "@web3-storage/access": "^20.2.0", 57 | "@web3-storage/capabilities": "^18.1.0", 58 | "@web3-storage/data-segment": "^5.3.0", 59 | "@web3-storage/did-mailto": "^2.1.0", 60 | "@web3-storage/w3up-client": "^17.2.0", 61 | "ansi-escapes": "^6.2.0", 62 | "chalk": "^5.3.0", 63 | "crypto-random-string": "^5.0.0", 64 | "files-from-path": "^1.1.1", 65 | "fr32-sha2-256-trunc254-padded-binary-tree-multihash": "^3.3.0", 66 | "multiformats": "^13.1.3", 67 | "open": "^9.1.0", 68 | "ora": "^7.0.1", 69 | "pretty-tree": "^1.0.0", 70 | "s-ago": "^2.2.0", 71 | "sade": "^1.8.1", 72 | "update-notifier": "^7.0.0" 73 | }, 74 | "eslintConfig": { 75 | "extends": [ 76 | "@web3-storage/eslint-config-w3up" 77 | ], 78 | "parserOptions": { 79 | "project": "./tsconfig.json" 80 | }, 81 | "env": { 82 | "es2022": true, 83 | "mocha": true, 84 | "browser": true, 85 | "node": true 86 | }, 87 | "ignorePatterns": [ 88 | "dist", 89 | "coverage", 90 | "api.js" 91 | ] 92 | }, 93 | "prettier": { 94 | "trailingComma": "es5", 95 | "tabWidth": 2, 96 | "semi": false, 97 | "singleQuote": true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /piece-hasher-worker.js: -------------------------------------------------------------------------------- 1 | import { parentPort, workerData } from 'node:worker_threads' 2 | import * as PieceHasher from 'fr32-sha2-256-trunc254-padded-binary-tree-multihash' 3 | 4 | const hasher = PieceHasher.create() 5 | hasher.write(workerData) 6 | 7 | const bytes = new Uint8Array(hasher.multihashByteLength()) 8 | hasher.digestInto(bytes, 0, true) 9 | hasher.free() 10 | 11 | parentPort?.postMessage(bytes) 12 | -------------------------------------------------------------------------------- /shim.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Suppress experimental warnings from node 4 | // see: https://github.com/nodejs/node/issues/30810 5 | 6 | const defaultEmit = process.emit 7 | // @ts-expect-error 8 | process.emit = function (...args) { 9 | // @ts-expect-error 10 | if (args[1].name === 'ExperimentalWarning') { 11 | return undefined 12 | } 13 | // @ts-expect-error 14 | return defaultEmit.call(this, ...args) 15 | } 16 | 17 | // @ts-expect-error 18 | await import('./bin.js') 19 | -------------------------------------------------------------------------------- /space.js: -------------------------------------------------------------------------------- 1 | import * as W3Space from '@web3-storage/w3up-client/space' 2 | import * as W3Account from '@web3-storage/w3up-client/account' 3 | import * as UcantoClient from '@ucanto/client' 4 | import { HTTP } from '@ucanto/transport' 5 | import * as CAR from '@ucanto/transport/car' 6 | import { getClient } from './lib.js' 7 | import process from 'node:process' 8 | import * as DIDMailto from '@web3-storage/did-mailto' 9 | import * as Account from './account.js' 10 | import { SpaceDID } from '@web3-storage/capabilities/utils' 11 | import ora from 'ora' 12 | import { select, input } from '@inquirer/prompts' 13 | import { mnemonic } from './dialog.js' 14 | import { API } from '@ucanto/core' 15 | import * as Result from '@web3-storage/w3up-client/result' 16 | 17 | /** 18 | * @typedef {object} CreateOptions 19 | * @property {false} [recovery] 20 | * @property {false} [caution] 21 | * @property {DIDMailto.EmailAddress|false} [customer] 22 | * @property {string|false} [account] 23 | * @property {Array<{id: import('@ucanto/interface').DID, serviceEndpoint: string}>} [authorizeGatewayServices] - The DID Key or DID Web and URL of the Gateway to authorize to serve content from the created space. 24 | * @property {boolean} [skipGatewayAuthorization] - Whether to skip the Gateway authorization. It means that the content of the space will not be served by any Gateway. 25 | * 26 | * @param {string|undefined} name 27 | * @param {CreateOptions} options 28 | */ 29 | export const create = async (name, options) => { 30 | const client = await getClient() 31 | const spaces = client.spaces() 32 | 33 | let space 34 | if (options.skipGatewayAuthorization === true) { 35 | space = await client.createSpace(await chooseName(name ?? '', spaces), { 36 | skipGatewayAuthorization: true, 37 | }) 38 | } else { 39 | const gateways = options.authorizeGatewayServices ?? [] 40 | const connections = gateways.map(({ id, serviceEndpoint }) => { 41 | /** @type {UcantoClient.ConnectionView} */ 42 | const connection = UcantoClient.connect({ 43 | id: { 44 | did: () => id, 45 | }, 46 | codec: CAR.outbound, 47 | channel: HTTP.open({ url: new URL(serviceEndpoint) }), 48 | }) 49 | return connection 50 | }) 51 | space = await client.createSpace(await chooseName(name ?? '', spaces), { 52 | authorizeGatewayServices: connections, 53 | }) 54 | } 55 | 56 | // Unless use opted-out from paper key recovery, we go through the flow 57 | if (options.recovery !== false) { 58 | const recovery = await setupRecovery(space, options) 59 | if (recovery == null) { 60 | console.log( 61 | '⚠️ Aborting, if you want to create space without recovery option pass --no-recovery flag' 62 | ) 63 | process.exit(1) 64 | } 65 | } 66 | 67 | if (options.customer !== false) { 68 | console.log('🏗️ To serve this space we need to set a billing account') 69 | const setup = await setupBilling(client, { 70 | customer: options.customer, 71 | space: space.did(), 72 | message: '🚜 Setting a billing account', 73 | }) 74 | 75 | if (setup.error) { 76 | if (setup.error.reason === 'abort') { 77 | console.log( 78 | '⏭️ Skipped billing setup. You can do it later using `w3 space provision`' 79 | ) 80 | } else { 81 | console.error( 82 | '⚠️ Failed to to set billing account. You can retry using `w3 space provision`' 83 | ) 84 | console.error(setup.error.cause.message) 85 | } 86 | } else { 87 | console.log(`✨ Billing account is set`) 88 | } 89 | } 90 | 91 | // Authorize this client to allow them to use this space. 92 | // ⚠️ This is a temporary solution until we solve the account sync problem 93 | // after which we will simply delegate to the account. 94 | const authorization = await space.createAuthorization(client) 95 | await client.addSpace(authorization) 96 | // set this space as the current default space 97 | await client.setCurrentSpace(space.did()) 98 | 99 | // Unless user opted-out we go through an account authorization flow 100 | if (options.account !== false) { 101 | console.log( 102 | `⛓️ To manage space across devices we need to authorize an account` 103 | ) 104 | 105 | const account = options.account 106 | ? await useAccount(client, { email: options.account }) 107 | : await selectAccount(client) 108 | 109 | if (account) { 110 | const spinner = ora(`📩 Authorizing ${account.toEmail()}`).start() 111 | const recovery = await space.createRecovery(account.did()) 112 | 113 | const result = await client.capability.access.delegate({ 114 | space: space.did(), 115 | delegations: [recovery], 116 | }) 117 | spinner.stop() 118 | 119 | if (result.ok) { 120 | console.log(`✨ Account is authorized`) 121 | } else { 122 | console.error( 123 | `⚠️ Failed to authorize account. You can still manage space using "paper key"` 124 | ) 125 | console.error(result.error) 126 | } 127 | } else { 128 | console.log( 129 | `⏭️ Skip account authorization. You can still can manage space using "paper key"` 130 | ) 131 | } 132 | } 133 | 134 | console.log(`⁂ Space created: ${space.did()}`) 135 | 136 | return space 137 | } 138 | 139 | /** 140 | * @param {import('@web3-storage/w3up-client').Client} client 141 | * @param {object} options 142 | * @param {import('@web3-storage/w3up-client/types').SpaceDID} options.space 143 | * @param {DIDMailto.EmailAddress} [options.customer] 144 | * @param {string} [options.message] 145 | * @param {string} [options.waitMessage] 146 | * @returns {Promise>} 147 | */ 148 | const setupBilling = async ( 149 | client, 150 | { 151 | customer, 152 | space, 153 | message = 'Setting up a billing account', 154 | waitMessage = 'Waiting for payment plan to be selected', 155 | } 156 | ) => { 157 | const account = customer 158 | ? await useAccount(client, { email: customer }) 159 | : await selectAccount(client) 160 | 161 | if (account) { 162 | const spinner = ora(waitMessage).start() 163 | 164 | let plan = null 165 | while (!plan) { 166 | const result = await account.plan.get() 167 | 168 | if (result.ok) { 169 | plan = result.ok 170 | } else { 171 | await new Promise((resolve) => setTimeout(resolve, 1000)) 172 | } 173 | } 174 | 175 | spinner.text = message 176 | 177 | const result = await account.provision(space) 178 | 179 | spinner.stop() 180 | if (result.error) { 181 | return { error: { reason: 'error', cause: result.error } } 182 | } else { 183 | return { ok: {} } 184 | } 185 | } else { 186 | return { error: { reason: 'abort' } } 187 | } 188 | } 189 | 190 | /** 191 | * @typedef {object} ProvisionOptions 192 | * @property {DIDMailto.EmailAddress} [customer] 193 | * @property {string} [coupon] 194 | * @property {string} [provider] 195 | * @property {string} [password] 196 | * 197 | * @param {string} name 198 | * @param {ProvisionOptions} options 199 | */ 200 | export const provision = async (name = '', options = {}) => { 201 | const client = await getClient() 202 | const space = chooseSpace(client, { name }) 203 | if (!space) { 204 | console.log( 205 | `You do not appear to have a space, you can create one by running "w3 space create"` 206 | ) 207 | process.exit(1) 208 | } 209 | 210 | if (options.coupon) { 211 | const { ok: bytes, error: fetchError } = await fetch(options.coupon) 212 | .then((response) => response.arrayBuffer()) 213 | .then((buffer) => Result.ok(new Uint8Array(buffer))) 214 | .catch((error) => Result.error(/** @type {Error} */(error))) 215 | 216 | if (fetchError) { 217 | console.error(`Failed to fetch coupon from ${options.coupon}`) 218 | process.exit(1) 219 | } 220 | 221 | const { ok: access, error: couponError } = await client.coupon 222 | .redeem(bytes, options) 223 | .then(Result.ok, Result.error) 224 | 225 | if (!access) { 226 | console.error(`Failed to redeem coupon: ${couponError.message}}`) 227 | process.exit(1) 228 | } 229 | 230 | const result = await W3Space.provision( 231 | { did: () => space }, 232 | { 233 | proofs: access.proofs, 234 | agent: client.agent, 235 | } 236 | ) 237 | 238 | if (result.error) { 239 | console.log(`Failed to provision space: ${result.error.message}`) 240 | process.exit(1) 241 | } 242 | } else { 243 | const result = await setupBilling(client, { 244 | customer: options.customer, 245 | space, 246 | }) 247 | 248 | if (result.error) { 249 | console.error( 250 | `⚠️ Failed to set up billing account,\n ${Object(result.error).message ?? '' 251 | }` 252 | ) 253 | process.exit(1) 254 | } 255 | } 256 | 257 | console.log(`✨ Billing account is set`) 258 | } 259 | 260 | /** 261 | * @typedef {import('@web3-storage/w3up-client/types').SpaceDID} SpaceDID 262 | * 263 | * @param {import('@web3-storage/w3up-client').Client} client 264 | * @param {object} options 265 | * @param {string} options.name 266 | * @returns {SpaceDID|undefined} 267 | */ 268 | const chooseSpace = (client, { name }) => { 269 | if (name) { 270 | const result = SpaceDID.read(name) 271 | if (result.ok) { 272 | return result.ok 273 | } 274 | 275 | const space = client.spaces().find((space) => space.name === name) 276 | if (space) { 277 | return /** @type {SpaceDID} */ (space.did()) 278 | } 279 | } 280 | 281 | return /** @type {SpaceDID|undefined} */ (client.currentSpace()?.did()) 282 | } 283 | 284 | /** 285 | * 286 | * @param {W3Space.Model} space 287 | * @param {CreateOptions} options 288 | */ 289 | export const setupEmailRecovery = async (space, options = {}) => { } 290 | 291 | /** 292 | * @param {string} email 293 | * @returns {{ok: DIDMailto.EmailAddress, error?:void}|{ok?:void, error: Error}} 294 | */ 295 | const parseEmail = (email) => { 296 | try { 297 | return { ok: DIDMailto.email(email) } 298 | } catch (cause) { 299 | return { error: /** @type {Error} */ (cause) } 300 | } 301 | } 302 | 303 | /** 304 | * @param {W3Space.OwnedSpace} space 305 | * @param {CreateOptions} options 306 | */ 307 | export const setupRecovery = async (space, options = {}) => { 308 | const recoveryKey = W3Space.toMnemonic(space) 309 | 310 | if (options.caution === false) { 311 | console.log(formatRecoveryInstruction(recoveryKey)) 312 | return space 313 | } else { 314 | const verified = await mnemonic({ 315 | secret: recoveryKey.split(/\s+/g), 316 | message: 317 | 'You need to save the following secret recovery key somewhere safe! For example write it down on a piece of paper and put it inside your favorite book.', 318 | revealMessage: 319 | '🤫 Make sure no one is eavesdropping and hit enter to reveal the key', 320 | submitMessage: '📝 Once you have saved the key hit enter to continue', 321 | validateMessage: 322 | '🔒 Please type or paste your recovery key to make sure it is correct', 323 | exitMessage: '🔐 Secret recovery key is correct!', 324 | }).catch(() => null) 325 | 326 | return verified ? space : null 327 | } 328 | } 329 | 330 | /** 331 | * @param {string} key 332 | */ 333 | const formatRecoveryInstruction = (key) => 334 | `🔑 You need to save following secret recovery key somewhere safe! For example write it down on a piece of paper and put it inside your favorite book. 335 | 336 | ${key} 337 | 338 | ` 339 | 340 | /** 341 | * @param {string} name 342 | * @param {{name:string}[]} spaces 343 | * @returns {Promise} 344 | */ 345 | const chooseName = async (name, spaces) => { 346 | const space = spaces.find((space) => String(space.name) === name) 347 | const message = 348 | name === '' 349 | ? 'What would you like to call this space?' 350 | : space 351 | ? `Name "${space.name}" is already taken, please choose a different one` 352 | : null 353 | 354 | if (message == null) { 355 | return name 356 | } else { 357 | return await input({ 358 | message, 359 | }) 360 | } 361 | } 362 | 363 | /** 364 | * @param {import('@web3-storage/w3up-client').Client} client 365 | * @param {{email?:string}} options 366 | */ 367 | export const pickAccount = async (client, { email }) => 368 | email ? await useAccount(client, { email }) : await selectAccount(client) 369 | 370 | /** 371 | * @param {import('@web3-storage/w3up-client').Client} client 372 | * @param {{email?:string}} options 373 | */ 374 | export const useAccount = (client, { email }) => { 375 | const accounts = Object.values(W3Account.list(client)) 376 | const account = accounts.find((account) => account.toEmail() === email) 377 | 378 | if (!account) { 379 | console.error( 380 | `Agent is not authorized by ${email}, please login with it first` 381 | ) 382 | return null 383 | } 384 | 385 | return account 386 | } 387 | 388 | /** 389 | * @param {import('@web3-storage/w3up-client').Client} client 390 | */ 391 | export const selectAccount = async (client) => { 392 | const accounts = Object.values(W3Account.list(client)) 393 | 394 | // If we do not have any accounts yet we take user through setup flow 395 | if (accounts.length === 0) { 396 | return setupAccount(client) 397 | } 398 | // If we have only one account we use it 399 | else if (accounts.length === 1) { 400 | return accounts[0] 401 | } 402 | // Otherwise we ask user to choose one 403 | else { 404 | return chooseAccount(accounts) 405 | } 406 | } 407 | 408 | /** 409 | * @param {import('@web3-storage/w3up-client').Client} client 410 | */ 411 | export const setupAccount = async (client) => { 412 | const method = await select({ 413 | message: 'How do you want to authorize your account?', 414 | choices: [ 415 | { name: 'Via Email', value: 'email' }, 416 | { name: 'Via GitHub', value: 'github' }, 417 | ], 418 | }) 419 | 420 | if (method === 'github') { 421 | return Account.oauthLoginWithClient(Account.OAuthProviderGitHub, client) 422 | } 423 | 424 | const email = await input({ 425 | message: `📧 Please enter an email address to setup an account`, 426 | validate: (input) => parseEmail(input).ok != null, 427 | }).catch(() => null) 428 | 429 | return email 430 | ? await Account.loginWithClient( 431 | /** @type {DIDMailto.EmailAddress} */(email), 432 | client 433 | ) 434 | : null 435 | } 436 | 437 | /** 438 | * @param {Account.View[]} accounts 439 | * @returns {Promise} 440 | */ 441 | export const chooseAccount = async (accounts) => { 442 | const account = await select({ 443 | message: 'Please choose an account you would like to use', 444 | choices: accounts.map((account) => ({ 445 | name: account.toEmail(), 446 | value: account, 447 | })), 448 | }).catch(() => null) 449 | 450 | return account 451 | } 452 | -------------------------------------------------------------------------------- /test/bin.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import os from 'os' 3 | import path from 'path' 4 | import * as Signer from '@ucanto/principal/ed25519' 5 | import { importDAG } from '@ucanto/core/delegation' 6 | import { parseLink } from '@ucanto/server' 7 | import * as DID from '@ipld/dag-ucan/did' 8 | import * as dagJSON from '@ipld/dag-json' 9 | import { SpaceDID } from '@web3-storage/capabilities/utils' 10 | import { CarReader } from '@ipld/car' 11 | import { test } from './helpers/context.js' 12 | import * as Test from './helpers/context.js' 13 | import { pattern, match } from './helpers/util.js' 14 | import * as Command from './helpers/process.js' 15 | import { Absentee, ed25519 } from '@ucanto/principal' 16 | import * as DIDMailto from '@web3-storage/did-mailto' 17 | import { UCAN, Provider } from '@web3-storage/capabilities' 18 | import * as ED25519 from '@ucanto/principal/ed25519' 19 | import { sha256, delegate } from '@ucanto/core' 20 | import * as Result from '@web3-storage/w3up-client/result' 21 | import { base64 } from 'multiformats/bases/base64' 22 | import { base58btc } from 'multiformats/bases/base58' 23 | import * as Digest from 'multiformats/hashes/digest' 24 | 25 | const w3 = Command.create('./bin.js') 26 | 27 | export const testW3 = { 28 | w3: test(async (assert, { env }) => { 29 | const { output } = await w3.env(env.alice).join() 30 | 31 | assert.match(output, /Available Commands/) 32 | }), 33 | 34 | 'w3 nosuchcmd': test(async (assert, context) => { 35 | const { status, output } = await w3 36 | .args(['nosuchcmd']) 37 | .env(context.env.alice) 38 | .join() 39 | .catch() 40 | 41 | assert.equal(status.code, 1) 42 | assert.match(output, /Invalid command: nosuch/) 43 | }), 44 | 45 | 'w3 --version': test(async (assert, context) => { 46 | const { output, status } = await w3.args(['--version']).join() 47 | 48 | assert.equal(status.code, 0) 49 | assert.match(output, /w3, \d+\.\d+\.\d+/) 50 | }), 51 | 52 | 'w3 whoami': test(async (assert) => { 53 | const { output } = await w3.args(['whoami']).join() 54 | 55 | assert.match(output, /^did:key:/) 56 | }), 57 | } 58 | 59 | export const testAccount = { 60 | 'w3 account ls': test(async (assert, context) => { 61 | const { output } = await w3 62 | .env(context.env.alice) 63 | .args(['account ls']) 64 | .join() 65 | 66 | assert.match(output, /has not been authorized yet/) 67 | }), 68 | 69 | 'w3 login': test(async (assert, context) => { 70 | const login = w3 71 | .args(['login', 'alice@web.mail']) 72 | .env(context.env.alice) 73 | .fork() 74 | 75 | const line = await login.error.lines().take().text() 76 | assert.match(line, /please click the link sent/) 77 | 78 | // receive authorization request 79 | const mail = await context.mail.take() 80 | 81 | // confirm authorization 82 | await context.grantAccess(mail) 83 | 84 | const message = await login.output.text() 85 | 86 | assert.match(message ?? '', /authorized by did:mailto:web.mail:alice/) 87 | }), 88 | 89 | 'w3 account list': test(async (assert, context) => { 90 | await login(context) 91 | 92 | const { output } = await w3 93 | .env(context.env.alice) 94 | .args(['account list']) 95 | .join() 96 | 97 | assert.match(output, /did:mailto:web.mail:alice/) 98 | }), 99 | } 100 | 101 | export const testSpace = { 102 | 'w3 space create': test(async (assert, context) => { 103 | const command = w3.args(['space', 'create', '--no-gateway-authorization']).env(context.env.alice).fork() 104 | 105 | const line = await command.output.take(1).text() 106 | 107 | assert.match(line, /What would you like to call this space/) 108 | 109 | await command.terminate().join().catch() 110 | }), 111 | 112 | 'w3 space create home': test(async (assert, context) => { 113 | const create = w3 114 | .args(['space', 'create', 'home', '--no-gateway-authorization']) 115 | .env(context.env.alice) 116 | .fork() 117 | 118 | const message = await create.output.take(1).text() 119 | 120 | const [prefix, key, suffix] = message.split('\n\n') 121 | 122 | assert.match(prefix, /secret recovery key/) 123 | assert.match(suffix, /hit enter to reveal the key/) 124 | 125 | const secret = key.replaceAll(/[\s\n]+/g, '') 126 | assert.equal(secret, '█'.repeat(secret.length), 'key is concealed') 127 | 128 | assert.ok(secret.length > 60, 'there are several words') 129 | 130 | await create.terminate().join().catch() 131 | }), 132 | 133 | 'w3 space create home --no-caution': test(async (assert, context) => { 134 | const create = w3 135 | .args(['space', 'create', 'home', '--no-caution', '--no-gateway-authorization']) 136 | .env(context.env.alice) 137 | .fork() 138 | 139 | const message = await create.output.lines().take(6).text() 140 | 141 | const lines = message.split('\n').filter((line) => line.trim() !== '') 142 | const [prefix, key, suffix] = lines 143 | 144 | assert.match(prefix, /secret recovery key/) 145 | assert.match(suffix, /billing account/, 'no heads up') 146 | const words = key.trim().split(' ') 147 | assert.ok( 148 | words.every((word) => [...word].every((letter) => letter !== '█')), 149 | 'key is revealed' 150 | ) 151 | assert.ok(words.length > 20, 'there are several words') 152 | 153 | await create.terminate().join().catch() 154 | }), 155 | 156 | 'w3 space create my-space --no-recovery': test(async (assert, context) => { 157 | const create = w3 158 | .args(['space', 'create', 'home', '--no-recovery', '--no-gateway-authorization']) 159 | .env(context.env.alice) 160 | .fork() 161 | 162 | const line = await create.output.lines().take().text() 163 | 164 | assert.match(line, /billing account/, 'no paper recovery') 165 | 166 | await create.terminate().join().catch() 167 | }), 168 | 169 | 'w3 space create my-space --no-recovery (logged-in)': test( 170 | async (assert, context) => { 171 | await login(context) 172 | 173 | await selectPlan(context) 174 | 175 | const create = w3 176 | .args(['space', 'create', 'home', '--no-recovery', '--no-gateway-authorization']) 177 | .env(context.env.alice) 178 | .fork() 179 | 180 | const lines = await create.output.lines().take(2).text() 181 | 182 | assert.match(lines, /billing account is set/i) 183 | 184 | await create.terminate().join().catch() 185 | } 186 | ), 187 | 188 | 'w3 space create my-space --no-recovery (multiple accounts)': test( 189 | async (assert, context) => { 190 | await login(context, { email: 'alice@web.mail' }) 191 | await login(context, { email: 'alice@email.me' }) 192 | 193 | const create = w3 194 | .args(['space', 'create', 'my-space', '--no-recovery', '--no-gateway-authorization']) 195 | .env(context.env.alice) 196 | .fork() 197 | 198 | const output = await create.output.take(2).text() 199 | 200 | assert.match( 201 | output, 202 | /choose an account you would like to use/, 203 | 'choose account' 204 | ) 205 | 206 | assert.ok(output.includes('alice@web.mail')) 207 | assert.ok(output.includes('alice@email.me')) 208 | 209 | create.terminate() 210 | } 211 | ), 212 | 213 | 'w3 space create void --skip-paper --provision-as unknown@web.mail --skip-email': 214 | test(async (assert, context) => { 215 | const { output, error } = await w3 216 | .env(context.env.alice) 217 | .args([ 218 | 'space', 219 | 'create', 220 | 'home', 221 | '--no-recovery', 222 | '--no-gateway-authorization', 223 | '--customer', 224 | 'unknown@web.mail', 225 | '--no-account', 226 | ]) 227 | .join() 228 | .catch() 229 | 230 | assert.match(output, /billing account/) 231 | assert.match(output, /Skipped billing setup/) 232 | assert.match(error, /not authorized by unknown@web\.mail/) 233 | }), 234 | 235 | 'w3 space create home --no-recovery --customer alice@web.mail --no-account': 236 | test(async (assert, context) => { 237 | await login(context, { email: 'alice@web.mail' }) 238 | 239 | selectPlan(context) 240 | 241 | const create = await w3 242 | .args([ 243 | 'space', 244 | 'create', 245 | 'home', 246 | '--no-recovery', 247 | '--no-gateway-authorization', 248 | '--customer', 249 | 'alice@web.mail', 250 | '--no-account', 251 | ]) 252 | .env(context.env.alice) 253 | .join() 254 | 255 | assert.match(create.output, /Billing account is set/) 256 | 257 | const info = await w3 258 | .args(['space', 'info']) 259 | .env(context.env.alice) 260 | .join() 261 | 262 | assert.match(info.output, /Providers: did:web:/) 263 | }), 264 | 265 | 'w3 space create home --no-recovery --customer alice@web.mail --account alice@web.mail': 266 | test(async (assert, context) => { 267 | const email = 'alice@web.mail' 268 | await login(context, { email }) 269 | await selectPlan(context, { email }) 270 | 271 | const { output } = await w3 272 | .args([ 273 | 'space', 274 | 'create', 275 | 'home', 276 | '--no-recovery', 277 | '--no-gateway-authorization', 278 | '--customer', 279 | email, 280 | '--account', 281 | email, 282 | ]) 283 | .env(context.env.alice) 284 | .join() 285 | 286 | assert.match(output, /account is authorized/i) 287 | 288 | const result = await context.delegationsStorage.find({ 289 | audience: DIDMailto.fromEmail(email), 290 | }) 291 | 292 | assert.ok( 293 | result.ok?.find((d) => d.capabilities[0].can === '*'), 294 | 'account has been delegated access to the space' 295 | ) 296 | }), 297 | 298 | 'w3 space create home --no-recovery (blocks until plan is selected)': test( 299 | async (assert, context) => { 300 | const email = 'alice@web.mail' 301 | await login(context, { email }) 302 | 303 | context.plansStorage.get = async () => { 304 | return { 305 | ok: { product: 'did:web:free.web3.storage', updatedAt: 'now' }, 306 | } 307 | } 308 | 309 | const { output, error } = await w3 310 | .env(context.env.alice) 311 | .args(['space', 'create', 'home', '--no-recovery', '--no-gateway-authorization']) 312 | .join() 313 | 314 | assert.match(output, /billing account is set/i) 315 | assert.match(error, /wait.*plan.*select/i) 316 | } 317 | ), 318 | 319 | 'storacha space create home --no-recovery --customer alice@web.mail --account alice@web.mail --authorize-gateway-services': 320 | test(async (assert, context) => { 321 | const email = 'alice@web.mail' 322 | await login(context, { email }) 323 | await selectPlan(context, { email }) 324 | 325 | const serverId = context.connection.id 326 | const serverURL = context.serverURL 327 | 328 | const { output } = await w3 329 | .args([ 330 | 'space', 331 | 'create', 332 | 'home', 333 | '--no-recovery', 334 | '--customer', 335 | email, 336 | '--account', 337 | email, 338 | '--authorize-gateway-services', 339 | `[{"id":"${serverId}","serviceEndpoint":"${serverURL}"}]`, 340 | ]) 341 | .env(context.env.alice) 342 | .join() 343 | 344 | assert.match(output, /account is authorized/i) 345 | 346 | const result = await context.delegationsStorage.find({ 347 | audience: DIDMailto.fromEmail(email), 348 | }) 349 | 350 | assert.ok( 351 | result.ok?.find((d) => d.capabilities[0].can === '*'), 352 | 'account has been delegated access to the space' 353 | ) 354 | }), 355 | 356 | 'w3 space add': test(async (assert, context) => { 357 | const { env } = context 358 | 359 | const spaceDID = await loginAndCreateSpace(context, { env: env.alice }) 360 | 361 | const whosBob = await w3.args(['whoami']).env(env.bob).join() 362 | 363 | const bobDID = SpaceDID.from(whosBob.output.trim()) 364 | 365 | const proofPath = path.join( 366 | os.tmpdir(), 367 | `w3cli-test-delegation-${Date.now()}` 368 | ) 369 | 370 | await w3 371 | .args([ 372 | 'delegation', 373 | 'create', 374 | bobDID, 375 | '-c', 376 | 'store/*', 377 | 'upload/*', 378 | '--output', 379 | proofPath, 380 | ]) 381 | .env(env.alice) 382 | .join() 383 | 384 | const listNone = await w3.args(['space', 'ls']).env(env.bob).join() 385 | assert.ok(!listNone.output.includes(spaceDID)) 386 | 387 | const add = await w3.args(['space', 'add', proofPath]).env(env.bob).join() 388 | assert.equal(add.output.trim(), spaceDID) 389 | 390 | const listSome = await w3.args(['space', 'ls']).env(env.bob).join() 391 | assert.ok(listSome.output.includes(spaceDID)) 392 | }), 393 | 394 | 'w3 space add `base64 proof car`': test(async (assert, context) => { 395 | const { env } = context 396 | const spaceDID = await loginAndCreateSpace(context, { env: env.alice }) 397 | const whosBob = await w3.args(['whoami']).env(env.bob).join() 398 | const bobDID = SpaceDID.from(whosBob.output.trim()) 399 | const res = await w3 400 | .args([ 401 | 'delegation', 402 | 'create', 403 | bobDID, 404 | '-c', 405 | 'store/*', 406 | 'upload/*', 407 | '--base64' 408 | ]) 409 | .env(env.alice) 410 | .join() 411 | 412 | const listNone = await w3.args(['space', 'ls']).env(env.bob).join() 413 | assert.ok(!listNone.output.includes(spaceDID)) 414 | 415 | const add = await w3.args(['space', 'add', res.output]).env(env.bob).join() 416 | assert.equal(add.output.trim(), spaceDID) 417 | 418 | const listSome = await w3.args(['space', 'ls']).env(env.bob).join() 419 | assert.ok(listSome.output.includes(spaceDID)) 420 | }), 421 | 422 | 'w3 space add invalid/path': test(async (assert, context) => { 423 | const fail = await w3 424 | .args(['space', 'add', 'djcvbii']) 425 | .env(context.env.alice) 426 | .join() 427 | .catch() 428 | 429 | assert.ok(!fail.status.success()) 430 | assert.match(fail.error, /failed to read proof/) 431 | }), 432 | 433 | 'w3 space add not-a-car.gif': test(async (assert, context) => { 434 | const fail = await w3 435 | .args(['space', 'add', './package.json']) 436 | .env(context.env.alice) 437 | .join() 438 | .catch() 439 | 440 | assert.equal(fail.status.success(), false) 441 | assert.match(fail.error, /failed to parse proof/) 442 | }), 443 | 444 | 'w3 space add empty.car': test(async (assert, context) => { 445 | const fail = await w3 446 | .args(['space', 'add', './test/fixtures/empty.car']) 447 | .env(context.env.alice) 448 | .join() 449 | .catch() 450 | 451 | assert.equal(fail.status.success(), false) 452 | assert.match(fail.error, /failed to import proof/) 453 | }), 454 | 455 | 'w3 space ls': test(async (assert, context) => { 456 | const emptyList = await w3 457 | .args(['space', 'ls']) 458 | .env(context.env.alice) 459 | .join() 460 | 461 | const spaceDID = await loginAndCreateSpace(context) 462 | 463 | const spaceList = await w3 464 | .args(['space', 'ls']) 465 | .env(context.env.alice) 466 | .join() 467 | 468 | assert.ok(!emptyList.output.includes(spaceDID)) 469 | assert.ok(spaceList.output.includes(spaceDID)) 470 | }), 471 | 472 | 'w3 space use': test(async (assert, context) => { 473 | const spaceDID = await loginAndCreateSpace(context, { 474 | env: context.env.alice, 475 | }) 476 | 477 | const listDefault = await w3 478 | .args(['space', 'ls']) 479 | .env(context.env.alice) 480 | .join() 481 | assert.ok(listDefault.output.includes(`* ${spaceDID}`)) 482 | 483 | const spaceName = 'laundry' 484 | 485 | const newSpaceDID = await createSpace(context, { name: spaceName }) 486 | 487 | const listNewDefault = await w3 488 | .args(['space', 'ls']) 489 | .env(context.env.alice) 490 | .join() 491 | 492 | assert.equal( 493 | listNewDefault.output.includes(`* ${spaceDID}`), 494 | false, 495 | 'old space is not default' 496 | ) 497 | assert.equal( 498 | listNewDefault.output.includes(`* ${newSpaceDID}`), 499 | true, 500 | 'new space is the default' 501 | ) 502 | 503 | assert.equal( 504 | listNewDefault.output.includes(spaceDID), 505 | true, 506 | 'old space is still listed' 507 | ) 508 | 509 | await w3.args(['space', 'use', spaceDID]).env(context.env.alice).join() 510 | const listSetDefault = await w3 511 | .args(['space', 'ls']) 512 | .env(context.env.alice) 513 | .join() 514 | 515 | assert.equal( 516 | listSetDefault.output.includes(`* ${spaceDID}`), 517 | true, 518 | 'spaceDID is default' 519 | ) 520 | assert.equal( 521 | listSetDefault.output.includes(`* ${newSpaceDID}`), 522 | false, 523 | 'new space is not default' 524 | ) 525 | 526 | await w3.args(['space', 'use', spaceName]).env(context.env.alice).join() 527 | const listNamedDefault = await w3 528 | .args(['space', 'ls']) 529 | .env(context.env.alice) 530 | .join() 531 | 532 | assert.equal(listNamedDefault.output.includes(`* ${spaceDID}`), false) 533 | assert.equal(listNamedDefault.output.includes(`* ${newSpaceDID}`), true) 534 | }), 535 | 536 | 'w3 space use did:key:unknown': test(async (assert, context) => { 537 | const space = await Signer.generate() 538 | 539 | const useSpace = await w3 540 | .args(['space', 'use', space.did()]) 541 | .env(context.env.alice) 542 | .join() 543 | .catch() 544 | 545 | assert.match(useSpace.error, /space not found/) 546 | }), 547 | 548 | 'w3 space use notfound': test(async (assert, context) => { 549 | const useSpace = await w3 550 | .args(['space', 'use', 'notfound']) 551 | .env(context.env.alice) 552 | .join() 553 | .catch() 554 | 555 | assert.match(useSpace.error, /space not found/) 556 | }), 557 | 558 | 'w3 space info': test(async (assert, context) => { 559 | const spaceDID = await loginAndCreateSpace(context, { 560 | customer: null, 561 | }) 562 | 563 | /** @type {import('@web3-storage/w3up-client/types').DID<'web'>} */ 564 | const providerDID = 'did:web:test.web3.storage' 565 | 566 | const infoWithoutProvider = await w3 567 | .args(['space', 'info']) 568 | .env(context.env.alice) 569 | .join() 570 | 571 | assert.match( 572 | infoWithoutProvider.output, 573 | pattern`DID: ${spaceDID}\nProviders: .*none`, 574 | 'space has no providers' 575 | ) 576 | 577 | assert.match( 578 | infoWithoutProvider.output, 579 | pattern`Name: home`, 580 | 'space name is set' 581 | ) 582 | 583 | Test.provisionSpace(context, { 584 | space: spaceDID, 585 | account: 'did:mailto:web.mail:alice', 586 | provider: providerDID, 587 | }) 588 | 589 | const infoWithProvider = await w3 590 | .args(['space', 'info']) 591 | .env(context.env.alice) 592 | .join() 593 | 594 | assert.match( 595 | infoWithProvider.output, 596 | pattern`DID: ${spaceDID}\nProviders: .*${providerDID}`, 597 | 'added provider shows up in the space info' 598 | ) 599 | 600 | const infoWithProviderJson = await w3 601 | .args(['space', 'info', '--json']) 602 | .env(context.env.alice) 603 | .join() 604 | 605 | assert.deepEqual(JSON.parse(infoWithProviderJson.output), { 606 | did: spaceDID, 607 | providers: [providerDID], 608 | name: 'home' 609 | }) 610 | }), 611 | 612 | 'w3 space provision --coupon': test(async (assert, context) => { 613 | const spaceDID = await loginAndCreateSpace(context, { customer: null }) 614 | 615 | assert.deepEqual( 616 | await context.provisionsStorage.getStorageProviders(spaceDID), 617 | { ok: [] }, 618 | 'space has no providers yet' 619 | ) 620 | 621 | const archive = await createCustomerSession(context) 622 | context.router['/proof.car'] = async () => { 623 | return { 624 | status: 200, 625 | headers: { 'content-type': 'application/car' }, 626 | body: archive, 627 | } 628 | } 629 | 630 | const url = new URL('/proof.car', context.serverURL) 631 | const provision = await w3 632 | .env(context.env.alice) 633 | .args(['space', 'provision', '--coupon', url.href]) 634 | .join() 635 | 636 | assert.match(provision.output, /Billing account is set/) 637 | 638 | const info = await w3.env(context.env.alice).args(['space', 'info']).join() 639 | 640 | assert.match( 641 | info.output, 642 | pattern`Providers: ${context.service.did()}`, 643 | 'space got provisioned' 644 | ) 645 | }), 646 | } 647 | 648 | export const testW3Up = { 649 | 'w3 up': test(async (assert, context) => { 650 | const email = 'alice@web.mail' 651 | await login(context, { email }) 652 | await selectPlan(context, { email }) 653 | 654 | const create = await w3 655 | .args([ 656 | 'space', 657 | 'create', 658 | 'home', 659 | '--no-recovery', 660 | '--no-account', 661 | '--no-gateway-authorization', 662 | '--customer', 663 | email, 664 | ]) 665 | .env(context.env.alice) 666 | .join() 667 | 668 | assert.ok(create.status.success()) 669 | 670 | const up = await w3 671 | .args(['up', 'test/fixtures/pinpie.jpg']) 672 | .env(context.env.alice) 673 | .join() 674 | 675 | assert.match( 676 | up.output, 677 | /bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea/ 678 | ) 679 | assert.match(up.error, /Stored 1 file/) 680 | }), 681 | 682 | 'w3 up --no-wrap': test(async (assert, context) => { 683 | const email = 'alice@web.mail' 684 | await login(context, { email }) 685 | await selectPlan(context, { email }) 686 | 687 | const create = await w3 688 | .args([ 689 | 'space', 690 | 'create', 691 | 'home', 692 | '--no-recovery', 693 | '--no-account', 694 | '--no-gateway-authorization', 695 | '--customer', 696 | email, 697 | ]) 698 | .env(context.env.alice) 699 | .join() 700 | 701 | assert.ok(create.status.success()) 702 | 703 | const up = await w3 704 | .args(['up', 'test/fixtures/pinpie.jpg', '--no-wrap']) 705 | .env(context.env.alice) 706 | .join() 707 | 708 | assert.match( 709 | up.output, 710 | /bafkreiajkbmpugz75eg2tmocmp3e33sg5kuyq2amzngslahgn6ltmqxxfa/ 711 | ) 712 | assert.match(up.error, /Stored 1 file/) 713 | }), 714 | 715 | 'w3 up --wrap false': test(async (assert, context) => { 716 | const email = 'alice@web.mail' 717 | await login(context, { email }) 718 | await selectPlan(context, { email }) 719 | 720 | const create = await w3 721 | .args([ 722 | 'space', 723 | 'create', 724 | 'home', 725 | '--no-recovery', 726 | '--no-account', 727 | '--no-gateway-authorization', 728 | '--customer', 729 | email, 730 | ]) 731 | .env(context.env.alice) 732 | .join() 733 | 734 | assert.ok(create.status.success()) 735 | 736 | const up = await w3 737 | .args(['up', 'test/fixtures/pinpie.jpg', '--wrap', 'false']) 738 | .env(context.env.alice) 739 | .join() 740 | 741 | assert.match( 742 | up.output, 743 | /bafkreiajkbmpugz75eg2tmocmp3e33sg5kuyq2amzngslahgn6ltmqxxfa/ 744 | ) 745 | assert.match(up.error, /Stored 1 file/) 746 | }), 747 | 748 | 'w3 up --car': test(async (assert, context) => { 749 | const email = 'alice@web.mail' 750 | await login(context, { email }) 751 | await selectPlan(context, { email }) 752 | await w3 753 | .args([ 754 | 'space', 755 | 'create', 756 | 'home', 757 | '--no-recovery', 758 | '--no-account', 759 | '--no-gateway-authorization', 760 | '--customer', 761 | email, 762 | ]) 763 | .env(context.env.alice) 764 | .join() 765 | 766 | const up = await w3 767 | .args(['up', '--car', 'test/fixtures/pinpie.car']) 768 | .env(context.env.alice) 769 | .join() 770 | 771 | assert.match( 772 | up.output, 773 | /bafkreiajkbmpugz75eg2tmocmp3e33sg5kuyq2amzngslahgn6ltmqxxfa/ 774 | ) 775 | assert.match(up.error, /Stored 1 file/) 776 | }), 777 | 778 | 'w3 ls': test(async (assert, context) => { 779 | await loginAndCreateSpace(context) 780 | 781 | const list0 = await w3.args(['ls']).env(context.env.alice).join() 782 | assert.match(list0.output, /No uploads in space/) 783 | 784 | await w3 785 | .args(['up', 'test/fixtures/pinpie.jpg']) 786 | .env(context.env.alice) 787 | .join() 788 | 789 | // wait a second for invocation to get a different expiry 790 | await new Promise((resolve) => setTimeout(resolve, 1000)) 791 | 792 | const list1 = await w3.args(['ls', '--json']).env(context.env.alice).join() 793 | 794 | assert.ok(dagJSON.parse(list1.output)) 795 | }), 796 | 797 | 'w3 remove': test(async (assert, context) => { 798 | await loginAndCreateSpace(context) 799 | 800 | const up = await w3 801 | .args(['up', 'test/fixtures/pinpie.jpg']) 802 | .env(context.env.alice) 803 | .join() 804 | 805 | assert.match( 806 | up.output, 807 | /bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea/ 808 | ) 809 | 810 | const rm = await w3 811 | .args([ 812 | 'rm', 813 | 'bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea', 814 | ]) 815 | .env(context.env.alice) 816 | .join() 817 | .catch() 818 | 819 | assert.equal(rm.status.code, 0) 820 | assert.equal(rm.output, '') 821 | }), 822 | 823 | 'w3 remove - no such upload': test(async (assert, context) => { 824 | await loginAndCreateSpace(context) 825 | 826 | const rm = await w3 827 | .args([ 828 | 'rm', 829 | 'bafybeih2k7ughhfwedltjviunmn3esueijz34snyay77zmsml5w24tqamm', 830 | '--shards', 831 | ]) 832 | .env(context.env.alice) 833 | .join() 834 | .catch() 835 | 836 | assert.equal(rm.status.code, 1) 837 | assert.match( 838 | rm.error, 839 | /not found/ 840 | ) 841 | }), 842 | } 843 | 844 | export const testDelegation = { 845 | 'w3 delegation create -c store/* --output file/path': test( 846 | async (assert, context) => { 847 | const env = context.env.alice 848 | const { bob } = Test 849 | 850 | const spaceDID = await loginAndCreateSpace(context) 851 | 852 | const proofPath = path.join( 853 | os.tmpdir(), 854 | `w3cli-test-delegation-${Date.now()}` 855 | ) 856 | 857 | await w3 858 | .args([ 859 | 'delegation', 860 | 'create', 861 | bob.did(), 862 | '-c', 863 | 'store/*', 864 | '--output', 865 | proofPath, 866 | ]) 867 | .env(env) 868 | .join() 869 | 870 | const reader = await CarReader.fromIterable( 871 | fs.createReadStream(proofPath) 872 | ) 873 | const blocks = [] 874 | for await (const block of reader.blocks()) { 875 | blocks.push(block) 876 | } 877 | 878 | // @ts-expect-error 879 | const delegation = importDAG(blocks) 880 | assert.equal(delegation.audience.did(), bob.did()) 881 | assert.equal(delegation.capabilities[0].can, 'store/*') 882 | assert.equal(delegation.capabilities[0].with, spaceDID) 883 | } 884 | ), 885 | 886 | 'w3 delegation create': test(async (assert, context) => { 887 | const env = context.env.alice 888 | const { bob } = Test 889 | await loginAndCreateSpace(context) 890 | 891 | const delegate = await w3 892 | .args(['delegation', 'create', bob.did()]) 893 | .env(env) 894 | .join() 895 | 896 | // TODO: Test output after we switch to Delegation.archive() / Delegation.extract() 897 | assert.equal(delegate.status.success(), true) 898 | }), 899 | 900 | 'w3 delegation create -c store/add -c upload/add --base64': test( 901 | async (assert, context) => { 902 | const env = context.env.alice 903 | const { bob } = Test 904 | const spaceDID = await loginAndCreateSpace(context) 905 | const res = await w3 906 | .args([ 907 | 'delegation', 908 | 'create', 909 | bob.did(), 910 | '-c', 911 | 'store/add', 912 | '-c', 913 | 'upload/add', 914 | '--base64' 915 | ]) 916 | .env(env) 917 | .join() 918 | 919 | assert.equal(res.status.success(), true) 920 | 921 | const identityCid = parseLink(res.output, base64) 922 | const reader = await CarReader.fromBytes(identityCid.multihash.digest) 923 | const blocks = [] 924 | for await (const block of reader.blocks()) { 925 | blocks.push(block) 926 | } 927 | 928 | // @ts-expect-error 929 | const delegation = importDAG(blocks) 930 | assert.equal(delegation.audience.did(), bob.did()) 931 | assert.equal(delegation.capabilities[0].can, 'store/add') 932 | assert.equal(delegation.capabilities[0].with, spaceDID) 933 | assert.equal(delegation.capabilities[1].can, 'upload/add') 934 | assert.equal(delegation.capabilities[1].with, spaceDID) 935 | } 936 | ), 937 | 938 | 'w3 delegation ls --json': test(async (assert, context) => { 939 | const { mallory } = Test 940 | 941 | const spaceDID = await loginAndCreateSpace(context) 942 | 943 | // delegate to mallory 944 | await w3 945 | .args(['delegation', 'create', mallory.did(), '-c', 'store/*']) 946 | .env(context.env.alice) 947 | .join() 948 | 949 | const list = await w3 950 | .args(['delegation', 'ls', '--json']) 951 | .env(context.env.alice) 952 | .join() 953 | 954 | const data = JSON.parse(list.output) 955 | 956 | assert.equal(data.audience, mallory.did()) 957 | assert.equal(data.capabilities.length, 1) 958 | assert.equal(data.capabilities[0].with, spaceDID) 959 | assert.equal(data.capabilities[0].can, 'store/*') 960 | }), 961 | 962 | 'w3 delegation revoke': test(async (assert, context) => { 963 | const env = context.env.alice 964 | const { mallory } = Test 965 | await loginAndCreateSpace(context) 966 | 967 | const delegationPath = `${os.tmpdir()}/delegation-${Date.now()}.ucan` 968 | await w3 969 | .args([ 970 | 'delegation', 971 | 'create', 972 | mallory.did(), 973 | '-c', 974 | 'store/*', 975 | 'upload/*', 976 | '-o', 977 | delegationPath, 978 | ]) 979 | .env(env) 980 | .join() 981 | 982 | const list = await w3 983 | .args(['delegation', 'ls', '--json']) 984 | .env(context.env.alice) 985 | .join() 986 | const { cid } = JSON.parse(list.output) 987 | 988 | // alice should be able to revoke the delegation she just created 989 | const revoke = await w3 990 | .args(['delegation', 'revoke', cid]) 991 | .env(context.env.alice) 992 | .join() 993 | 994 | assert.match(revoke.output, pattern`delegation ${cid} revoked`) 995 | 996 | await loginAndCreateSpace(context, { 997 | env: context.env.bob, 998 | customer: 'bob@super.host', 999 | }) 1000 | 1001 | // bob should not be able to because he doesn't have a copy of the delegation 1002 | const fail = await w3 1003 | .args(['delegation', 'revoke', cid]) 1004 | .env(context.env.bob) 1005 | .join() 1006 | .catch() 1007 | 1008 | assert.match( 1009 | fail.error, 1010 | pattern`Error: revoking ${cid}: could not find delegation ${cid}` 1011 | ) 1012 | 1013 | // but if bob passes the delegation manually, it should succeed - we don't 1014 | // validate that bob is able to issue the revocation, it simply won't apply 1015 | // if it's not legitimate 1016 | 1017 | const pass = await w3 1018 | .args(['delegation', 'revoke', cid, '-p', delegationPath]) 1019 | .env(context.env.bob) 1020 | .join() 1021 | 1022 | assert.match(pass.output, pattern`delegation ${cid} revoked`) 1023 | }), 1024 | } 1025 | 1026 | export const testProof = { 1027 | 'w3 proof add': test(async (assert, context) => { 1028 | const { env } = context 1029 | 1030 | const spaceDID = await loginAndCreateSpace(context, { env: env.alice }) 1031 | const whoisbob = await w3.args(['whoami']).env(env.bob).join() 1032 | const bobDID = DID.parse(whoisbob.output.trim()).did() 1033 | const proofPath = path.join( 1034 | os.tmpdir(), 1035 | `w3cli-test-delegation-${Date.now()}` 1036 | ) 1037 | 1038 | await w3 1039 | .args([ 1040 | 'delegation', 1041 | 'create', 1042 | bobDID, 1043 | '-c', 1044 | 'store/*', 1045 | '--output', 1046 | proofPath, 1047 | ]) 1048 | .env(env.alice) 1049 | .join() 1050 | 1051 | const listNone = await w3.args(['proof', 'ls']).env(env.bob).join() 1052 | assert.ok(!listNone.output.includes(spaceDID)) 1053 | 1054 | const addProof = await w3 1055 | .args(['proof', 'add', proofPath]) 1056 | .env(env.bob) 1057 | .join() 1058 | 1059 | assert.ok(addProof.output.includes(`with: ${spaceDID}`)) 1060 | const listProof = await w3.args(['proof', 'ls']).env(env.bob).join() 1061 | assert.ok(listProof.output.includes(spaceDID)) 1062 | }), 1063 | 'w3 proof add notfound': test(async (assert, context) => { 1064 | const proofAdd = await w3 1065 | .args(['proof', 'add', 'djcvbii']) 1066 | .env(context.env.alice) 1067 | .join() 1068 | .catch() 1069 | 1070 | assert.equal(proofAdd.status.success(), false) 1071 | assert.match(proofAdd.error, /failed to read proof/) 1072 | }), 1073 | 'w3 proof add not-car.json': test(async (assert, context) => { 1074 | const proofAdd = await w3 1075 | .args(['proof', 'add', './package.json']) 1076 | .env(context.env.alice) 1077 | .join() 1078 | .catch() 1079 | 1080 | assert.equal(proofAdd.status.success(), false) 1081 | assert.match(proofAdd.error, /failed to parse proof/) 1082 | }), 1083 | 'w3 proof add invalid.car': test(async (assert, context) => { 1084 | const proofAdd = await w3 1085 | .args(['proof', 'add', './test/fixtures/empty.car']) 1086 | .env(context.env.alice) 1087 | .join() 1088 | .catch() 1089 | 1090 | assert.equal(proofAdd.status.success(), false) 1091 | assert.match(proofAdd.error, /failed to import proof/) 1092 | }), 1093 | 'w3 proof ls': test(async (assert, context) => { 1094 | const { env } = context 1095 | const spaceDID = await loginAndCreateSpace(context, { env: env.alice }) 1096 | const whoisalice = await w3.args(['whoami']).env(env.alice).join() 1097 | const aliceDID = DID.parse(whoisalice.output.trim()).did() 1098 | 1099 | const whoisbob = await w3.args(['whoami']).env(env.bob).join() 1100 | const bobDID = DID.parse(whoisbob.output.trim()).did() 1101 | 1102 | const proofPath = path.join(os.tmpdir(), `w3cli-test-proof-${Date.now()}`) 1103 | await w3 1104 | .args([ 1105 | 'delegation', 1106 | 'create', 1107 | '-c', 1108 | 'store/*', 1109 | bobDID, 1110 | '--output', 1111 | proofPath, 1112 | ]) 1113 | .env(env.alice) 1114 | .join() 1115 | 1116 | await w3.args(['space', 'add', proofPath]).env(env.bob).join() 1117 | 1118 | const proofList = await w3 1119 | .args(['proof', 'ls', '--json']) 1120 | .env(env.bob) 1121 | .join() 1122 | const proofData = JSON.parse(proofList.output) 1123 | assert.equal(proofData.iss, aliceDID) 1124 | assert.equal(proofData.att.length, 1) 1125 | assert.equal(proofData.att[0].with, spaceDID) 1126 | assert.equal(proofData.att[0].can, 'store/*') 1127 | }), 1128 | } 1129 | 1130 | export const testBlob = { 1131 | 'w3 can blob add': test(async (assert, context) => { 1132 | await loginAndCreateSpace(context) 1133 | 1134 | const { error } = await w3 1135 | .args(['can', 'blob', 'add', 'test/fixtures/pinpie.jpg']) 1136 | .env(context.env.alice) 1137 | .join() 1138 | 1139 | assert.match(error, /Stored zQm/) 1140 | }), 1141 | 1142 | 'w3 can blob ls': test(async (assert, context) => { 1143 | await loginAndCreateSpace(context) 1144 | 1145 | await w3 1146 | .args(['can', 'blob', 'add', 'test/fixtures/pinpie.jpg']) 1147 | .env(context.env.alice) 1148 | .join() 1149 | 1150 | const list = await w3 1151 | .args(['can', 'blob', 'ls', '--json']) 1152 | .env(context.env.alice) 1153 | .join() 1154 | 1155 | assert.ok(dagJSON.parse(list.output)) 1156 | }), 1157 | 1158 | 'w3 can blob rm': test(async (assert, context) => { 1159 | await loginAndCreateSpace(context) 1160 | 1161 | await w3 1162 | .args(['can', 'blob', 'add', 'test/fixtures/pinpie.jpg']) 1163 | .env(context.env.alice) 1164 | .join() 1165 | 1166 | const list = await w3 1167 | .args(['can', 'blob', 'ls', '--json']) 1168 | .env(context.env.alice) 1169 | .join() 1170 | 1171 | const digest = Digest.decode(dagJSON.parse(list.output).blob.digest) 1172 | 1173 | const remove = await w3 1174 | .args(['can', 'blob', 'rm', base58btc.encode(digest.bytes)]) 1175 | .env(context.env.alice) 1176 | .join() 1177 | 1178 | assert.match(remove.error, /Removed zQm/) 1179 | }), 1180 | } 1181 | 1182 | export const testStore = { 1183 | 'w3 can store add': test(async (assert, context) => { 1184 | await loginAndCreateSpace(context) 1185 | 1186 | const { error } = await w3 1187 | .args(['can', 'store', 'add', 'test/fixtures/pinpie.car']) 1188 | .env(context.env.alice) 1189 | .join() 1190 | 1191 | assert.match(error, /Stored bag/) 1192 | }), 1193 | } 1194 | 1195 | export const testCan = { 1196 | 'w3 can upload add': test(async (assert, context) => { 1197 | await loginAndCreateSpace(context) 1198 | 1199 | const carPath = 'test/fixtures/pinpie.car' 1200 | const reader = await CarReader.fromBytes( 1201 | await fs.promises.readFile(carPath) 1202 | ) 1203 | const root = (await reader.getRoots())[0]?.toString() 1204 | assert.ok(root) 1205 | 1206 | const canStore = await w3 1207 | .args(['can', 'store', 'add', carPath]) 1208 | .env(context.env.alice) 1209 | .join() 1210 | 1211 | assert.match(canStore.error, /Stored bag/) 1212 | 1213 | const shard = canStore.output.trim() 1214 | const canUpload = await w3 1215 | .args(['can', 'upload', 'add', root, shard]) 1216 | .env(context.env.alice) 1217 | .join() 1218 | 1219 | assert.match(canUpload.error, /Upload added/) 1220 | }), 1221 | 1222 | 'w3 can upload ls': test(async (assert, context) => { 1223 | await loginAndCreateSpace(context) 1224 | 1225 | await w3 1226 | .args(['up', 'test/fixtures/pinpie.jpg']) 1227 | .env(context.env.alice) 1228 | .join() 1229 | 1230 | const list = await w3 1231 | .args(['can', 'upload', 'ls', '--json']) 1232 | .env(context.env.alice) 1233 | .join() 1234 | 1235 | assert.ok(dagJSON.parse(list.output)) 1236 | }), 1237 | 'w3 can upload rm': test(async (assert, context) => { 1238 | await loginAndCreateSpace(context) 1239 | 1240 | const up = await w3 1241 | .args(['up', 'test/fixtures/pinpie.jpg']) 1242 | .env(context.env.alice) 1243 | .join() 1244 | 1245 | assert.match( 1246 | up.output, 1247 | /bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea/ 1248 | ) 1249 | 1250 | const noPath = await w3 1251 | .args(['can', 'upload', 'rm']) 1252 | .env(context.env.alice) 1253 | .join() 1254 | .catch() 1255 | 1256 | assert.match(noPath.error, /Insufficient arguments/) 1257 | 1258 | const invalidCID = await w3 1259 | .args(['can', 'upload', 'rm', 'foo']) 1260 | .env(context.env.alice) 1261 | .join() 1262 | .catch() 1263 | 1264 | assert.match(invalidCID.error, /not a CID/) 1265 | 1266 | const rm = await w3 1267 | .args([ 1268 | 'can', 1269 | 'upload', 1270 | 'rm', 1271 | 'bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea', 1272 | ]) 1273 | .env(context.env.alice) 1274 | .join() 1275 | 1276 | assert.ok(rm.status.success()) 1277 | }), 1278 | 'w3 can store ls': test(async (assert, context) => { 1279 | await loginAndCreateSpace(context) 1280 | 1281 | await w3 1282 | .args(['can', 'store', 'add', 'test/fixtures/pinpie.car']) 1283 | .env(context.env.alice) 1284 | .join() 1285 | 1286 | const list = await w3 1287 | .args(['can', 'store', 'ls', '--json']) 1288 | .env(context.env.alice) 1289 | .join() 1290 | 1291 | assert.ok(dagJSON.parse(list.output)) 1292 | }), 1293 | 'w3 can store rm': test(async (assert, context) => { 1294 | const space = await loginAndCreateSpace(context) 1295 | 1296 | await w3 1297 | .args(['can', 'store', 'add', 'test/fixtures/pinpie.car']) 1298 | .env(context.env.alice) 1299 | .join() 1300 | 1301 | const stores = await context.storeTable.list(space) 1302 | const store = stores.ok?.results[0] 1303 | if (!store) { 1304 | return assert.ok(store, 'stored item should appear in list') 1305 | } 1306 | 1307 | const missingArg = await w3 1308 | .args(['can', 'store', 'rm']) 1309 | .env(context.env.alice) 1310 | .join() 1311 | .catch() 1312 | 1313 | assert.match(missingArg.error, /Insufficient arguments/) 1314 | 1315 | const invalidCID = await w3 1316 | .args(['can', 'store', 'rm', 'foo']) 1317 | .env(context.env.alice) 1318 | .join() 1319 | .catch() 1320 | 1321 | assert.match(invalidCID.error, /not a CAR CID/) 1322 | 1323 | const notCarCID = await w3 1324 | .args(['can', 'store', 'rm', 'bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea']) 1325 | .env(context.env.alice) 1326 | .join() 1327 | .catch() 1328 | assert.match(notCarCID.error, /not a CAR CID/) 1329 | 1330 | const rm = await w3 1331 | .args(['can', 'store', 'rm', store.link.toString()]) 1332 | .env(context.env.alice) 1333 | .join() 1334 | 1335 | assert.ok(rm.status.success()) 1336 | }), 1337 | 'can filecoin info with not found': test(async (assert, context) => { 1338 | await loginAndCreateSpace(context) 1339 | 1340 | const up = await w3 1341 | .args(['up', 'test/fixtures/pinpie.jpg', '--verbose']) 1342 | .env(context.env.alice) 1343 | .join() 1344 | const pieceCid = up.error.split('Piece CID: ')[1].split(`\n`)[0] 1345 | 1346 | const { error } = await w3 1347 | .args(['can', 'filecoin', 'info', pieceCid, '--json']) 1348 | .env(context.env.alice) 1349 | .join() 1350 | .catch() 1351 | // no piece will be available right away 1352 | assert.ok(error) 1353 | assert.ok(error.includes('not found')) 1354 | }), 1355 | } 1356 | 1357 | export const testPlan = { 1358 | 'w3 plan get': test(async (assert, context) => { 1359 | await login(context) 1360 | const notFound = await w3 1361 | .args(['plan', 'get']) 1362 | .env(context.env.alice) 1363 | .join() 1364 | 1365 | assert.match(notFound.output, /no plan/i) 1366 | 1367 | await selectPlan(context) 1368 | 1369 | // wait a second for invocation to get a different expiry 1370 | await new Promise((resolve) => setTimeout(resolve, 1000)) 1371 | 1372 | const plan = await w3.args(['plan', 'get']).env(context.env.alice).join() 1373 | assert.match(plan.output, /did:web:free.web3.storage/) 1374 | }), 1375 | } 1376 | 1377 | export const testKey = { 1378 | 'w3 key create': test(async (assert) => { 1379 | const res = await w3.args(['key', 'create', '--json']).join() 1380 | const key = ED25519.parse(JSON.parse(res.output).key) 1381 | assert.ok(key.did().startsWith('did:key')) 1382 | }), 1383 | } 1384 | 1385 | export const testBridge = { 1386 | 'w3 bridge generate-tokens': test(async (assert, context) => { 1387 | const spaceDID = await loginAndCreateSpace(context) 1388 | const res = await w3.args(['bridge', 'generate-tokens', spaceDID]).join() 1389 | assert.match(res.output, /X-Auth-Secret header: u/) 1390 | assert.match(res.output, /Authorization header: u/) 1391 | }), 1392 | } 1393 | 1394 | /** 1395 | * @param {Test.Context} context 1396 | * @param {object} options 1397 | * @param {string} [options.email] 1398 | * @param {Record} [options.env] 1399 | */ 1400 | export const login = async ( 1401 | context, 1402 | { email = 'alice@web.mail', env = context.env.alice } = {} 1403 | ) => { 1404 | const login = w3.env(env).args(['login', email]).fork() 1405 | 1406 | // wait for the new process to print the status 1407 | await login.error.lines().take().text() 1408 | 1409 | // receive authorization request 1410 | const message = await context.mail.take() 1411 | 1412 | // confirm authorization 1413 | await context.grantAccess(message) 1414 | 1415 | return await login.join() 1416 | } 1417 | 1418 | /** 1419 | * @typedef {import('@web3-storage/w3up-client/types').ProviderDID} Plan 1420 | * 1421 | * @param {Test.Context} context 1422 | * @param {object} options 1423 | * @param {DIDMailto.EmailAddress} [options.email] 1424 | * @param {string} [options.billingID] 1425 | * @param {Plan} [options.plan] 1426 | */ 1427 | export const selectPlan = async ( 1428 | context, 1429 | { email = 'alice@web.mail', billingID = 'test:cus_alice', plan = 'did:web:free.web3.storage' } = {} 1430 | ) => { 1431 | const customer = DIDMailto.fromEmail(email) 1432 | Result.try(await context.plansStorage.initialize(customer, billingID, plan)) 1433 | } 1434 | 1435 | /** 1436 | * @param {Test.Context} context 1437 | * @param {object} options 1438 | * @param {DIDMailto.EmailAddress|null} [options.customer] 1439 | * @param {string} [options.name] 1440 | * @param {Record} [options.env] 1441 | */ 1442 | export const createSpace = async ( 1443 | context, 1444 | { customer = 'alice@web.mail', name = 'home', env = context.env.alice } = {} 1445 | ) => { 1446 | const { output } = await w3 1447 | .args([ 1448 | 'space', 1449 | 'create', 1450 | name, 1451 | '--no-recovery', 1452 | '--no-account', 1453 | '--no-gateway-authorization', 1454 | ...(customer ? ['--customer', customer] : ['--no-customer']), 1455 | ]) 1456 | .env(env) 1457 | .join() 1458 | 1459 | const [did] = match(/(did:key:\w+)/, output) 1460 | 1461 | return SpaceDID.from(did) 1462 | } 1463 | 1464 | /** 1465 | * @param {Test.Context} context 1466 | * @param {object} options 1467 | * @param {DIDMailto.EmailAddress} [options.email] 1468 | * @param {DIDMailto.EmailAddress|null} [options.customer] 1469 | * @param {string} [options.name] 1470 | * @param {Plan} [options.plan] 1471 | * @param {Record} [options.env] 1472 | */ 1473 | export const loginAndCreateSpace = async ( 1474 | context, 1475 | { 1476 | email = 'alice@web.mail', 1477 | customer = email, 1478 | name = 'home', 1479 | plan = 'did:web:free.web3.storage', 1480 | env = context.env.alice, 1481 | } = {} 1482 | ) => { 1483 | await login(context, { email, env }) 1484 | 1485 | if (customer != null && plan != null) { 1486 | await selectPlan(context, { email: customer, plan }) 1487 | } 1488 | 1489 | return createSpace(context, { customer, name, env }) 1490 | } 1491 | 1492 | /** 1493 | * @param {Test.Context} context 1494 | * @param {object} options 1495 | * @param {string} [options.password] 1496 | */ 1497 | export const createCustomerSession = async ( 1498 | context, 1499 | { password = '' } = {} 1500 | ) => { 1501 | // Derive delegation audience from the password 1502 | const { digest } = await sha256.digest(new TextEncoder().encode(password)) 1503 | const audience = await ED25519.derive(digest) 1504 | 1505 | // Generate the agent that will be authorized to act on behalf of the customer 1506 | const agent = await ed25519.generate() 1507 | 1508 | const customer = Absentee.from({ id: 'did:mailto:web.mail:workshop' }) 1509 | 1510 | // First we create delegation from the customer to the agent that authorizing 1511 | // it to perform `provider/add` on their behalf. 1512 | const delegation = await delegate({ 1513 | issuer: customer, 1514 | audience: agent, 1515 | capabilities: [ 1516 | { 1517 | with: 'ucan:*', 1518 | can: '*', 1519 | }, 1520 | ], 1521 | expiration: Infinity, 1522 | }) 1523 | 1524 | // Then we create an attestation from the service to proof that agent has 1525 | // been authorized 1526 | const attestation = await UCAN.attest.delegate({ 1527 | issuer: context.service, 1528 | audience: agent, 1529 | with: context.service.did(), 1530 | nb: { proof: delegation.cid }, 1531 | expiration: delegation.expiration, 1532 | }) 1533 | 1534 | // Finally we create a short lived session that authorizes the audience to 1535 | // provider/add with their billing account. 1536 | const session = await Provider.add.delegate({ 1537 | issuer: agent, 1538 | audience, 1539 | with: customer.did(), 1540 | proofs: [delegation, attestation], 1541 | }) 1542 | 1543 | return Result.try(await session.archive()) 1544 | } 1545 | -------------------------------------------------------------------------------- /test/fixtures/empty.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storacha/w3cli/3a737891c4ad988e091eca9d0fc9bc96a4f83a6f/test/fixtures/empty.car -------------------------------------------------------------------------------- /test/fixtures/pinpie.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storacha/w3cli/3a737891c4ad988e091eca9d0fc9bc96a4f83a6f/test/fixtures/pinpie.car -------------------------------------------------------------------------------- /test/fixtures/pinpie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storacha/w3cli/3a737891c4ad988e091eca9d0fc9bc96a4f83a6f/test/fixtures/pinpie.jpg -------------------------------------------------------------------------------- /test/helpers/context.js: -------------------------------------------------------------------------------- 1 | import * as API from '../../api.js' 2 | 3 | import { 4 | createContext, 5 | cleanupContext, 6 | } from '@web3-storage/upload-api/test/context' 7 | import { createEnv } from './env.js' 8 | import { Signer } from '@ucanto/principal/ed25519' 9 | import { createServer as createHTTPServer } from './http-server.js' 10 | import { createReceiptsServer } from './receipt-http-server.js' 11 | import http from 'node:http' 12 | import { StoreConf } from '@web3-storage/w3up-client/stores/conf' 13 | import * as FS from 'node:fs/promises' 14 | 15 | /** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ 16 | export const alice = Signer.parse( 17 | 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' 18 | ) 19 | /** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ 20 | export const bob = Signer.parse( 21 | 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' 22 | ) 23 | /** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ 24 | export const mallory = Signer.parse( 25 | 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' 26 | ) 27 | 28 | export { createContext, cleanupContext } 29 | 30 | /** 31 | * @typedef {Awaited>} UcantoServerTestContext 32 | * 33 | * @param {UcantoServerTestContext} context 34 | * @param {object} input 35 | * @param {API.DIDKey} input.space 36 | * @param {API.DID<'mailto'>} input.account 37 | * @param {API.DID<'web'>} input.provider 38 | */ 39 | export const provisionSpace = async (context, { space, account, provider }) => { 40 | // add a provider for this space 41 | return await context.provisionsStorage.put({ 42 | cause: /** @type {*} */ ({}), 43 | consumer: space, 44 | customer: account, 45 | provider, 46 | }) 47 | } 48 | 49 | /** 50 | * @typedef {import('@web3-storage/w3up-client/types').StoreAddSuccess} StoreAddSuccess 51 | * @typedef {UcantoServerTestContext & { 52 | * server: import('./http-server').TestingServer['server'] 53 | * receiptsServer: import('./receipt-http-server.js').TestingServer['server'] 54 | * router: import('./http-server').Router 55 | * env: { alice: Record, bob: Record } 56 | * serverURL: URL 57 | * }} Context 58 | * 59 | * @returns {Promise} 60 | */ 61 | export const setup = async () => { 62 | const context = await createContext({ http }) 63 | const { server, serverURL, router } = await createHTTPServer({ 64 | '/': context.connection.channel.request.bind(context.connection.channel), 65 | }) 66 | const { server: receiptsServer, serverURL: receiptsServerUrl } = await createReceiptsServer() 67 | 68 | return Object.assign(context, { 69 | server, 70 | serverURL, 71 | receiptsServer, 72 | router, 73 | serverRouter: router, 74 | env: { 75 | alice: createEnv({ 76 | storeName: `w3cli-test-alice-${context.service.did()}`, 77 | servicePrincipal: context.service, 78 | serviceURL: serverURL, 79 | receiptsEndpoint: new URL('receipt', receiptsServerUrl), 80 | }), 81 | bob: createEnv({ 82 | storeName: `w3cli-test-bob-${context.service.did()}`, 83 | servicePrincipal: context.service, 84 | serviceURL: serverURL, 85 | receiptsEndpoint: new URL('receipt', receiptsServerUrl), 86 | }), 87 | }, 88 | }) 89 | } 90 | 91 | /** 92 | * @param {Context} context 93 | */ 94 | export const teardown = async (context) => { 95 | await cleanupContext(context) 96 | context.server.close() 97 | context.receiptsServer.close() 98 | 99 | const stores = [ 100 | context.env.alice.W3_STORE_NAME, 101 | context.env.bob.W3_STORE_NAME, 102 | ] 103 | 104 | await Promise.all( 105 | stores.map(async (name) => { 106 | const { path } = new StoreConf({ profile: name }) 107 | try { 108 | await FS.rm(path) 109 | } catch (/** @type {any} */ err) { 110 | if (err.code === 'ENOENT') return // is ok maybe it wasn't used in the test 111 | throw err 112 | } 113 | }) 114 | ) 115 | } 116 | 117 | /** 118 | * @param {(assert: import('entail').Assert, context: Context) => unknown} unit 119 | * @returns {import('entail').Test} 120 | */ 121 | export const test = (unit) => async (assert) => { 122 | const context = await setup() 123 | try { 124 | await unit(assert, context) 125 | } finally { 126 | await teardown(context) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/helpers/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {object} [options] 3 | * @param {import('@ucanto/interface').Principal} [options.servicePrincipal] 4 | * @param {URL} [options.serviceURL] 5 | * @param {string} [options.storeName] 6 | * @param {URL} [options.receiptsEndpoint] 7 | */ 8 | export function createEnv(options = {}) { 9 | const { servicePrincipal, serviceURL, storeName, receiptsEndpoint } = options 10 | const env = { W3_STORE_NAME: storeName ?? 'w3cli-test' } 11 | if (servicePrincipal && serviceURL) { 12 | Object.assign(env, { 13 | W3UP_SERVICE_DID: servicePrincipal.did(), 14 | W3UP_SERVICE_URL: serviceURL.toString(), 15 | W3UP_RECEIPTS_ENDPOINT: receiptsEndpoint?.toString() 16 | }) 17 | } 18 | return env 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/http-server.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import { once } from 'events' 3 | 4 | /** 5 | * @typedef {import('@ucanto/interface').HTTPRequest} HTTPRequest 6 | * @typedef {import('@ucanto/server').HTTPResponse} HTTPResponse 7 | * @typedef {Record PromiseLike|HTTPResponse>} Router 8 | * 9 | * @typedef {{ 10 | * server: http.Server 11 | * serverURL: URL 12 | * router: Router 13 | * }} TestingServer 14 | */ 15 | 16 | /** 17 | * @param {Router} router 18 | * @returns {Promise} 19 | */ 20 | export async function createServer(router) { 21 | /** 22 | * @param {http.IncomingMessage} request 23 | * @param {http.ServerResponse} response 24 | */ 25 | const listener = async (request, response) => { 26 | const chunks = [] 27 | for await (const chunk of request) { 28 | chunks.push(chunk) 29 | } 30 | 31 | const handler = router[request.url ?? '/'] 32 | if (!handler) { 33 | response.writeHead(404) 34 | response.end() 35 | return undefined 36 | } 37 | 38 | const { headers, body } = await handler({ 39 | headers: /** @type {Readonly>} */ ( 40 | request.headers 41 | ), 42 | body: Buffer.concat(chunks), 43 | }) 44 | 45 | response.writeHead(200, headers) 46 | response.write(body) 47 | response.end() 48 | return undefined 49 | } 50 | 51 | const server = http.createServer(listener).listen() 52 | 53 | await once(server, 'listening') 54 | 55 | return { 56 | server, 57 | router, 58 | // @ts-expect-error 59 | serverURL: new URL(`http://127.0.0.1:${server.address().port}`), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/helpers/process.js: -------------------------------------------------------------------------------- 1 | import Process from 'node:child_process' 2 | import { TextDecoder } from 'node:util' 3 | import { ByteStream } from './stream.js' 4 | 5 | /** 6 | * @typedef {object} Command 7 | * @property {string} program 8 | * @property {string[]} args 9 | * @property {Record} env 10 | * 11 | * @typedef {object} Outcome 12 | * @property {Status} status 13 | * @property {string} output 14 | * @property {string} error 15 | * 16 | * 17 | * @param {string} program 18 | */ 19 | export const create = (program) => 20 | new CommandView({ 21 | program, 22 | args: [], 23 | env: process.env, 24 | }) 25 | 26 | class CommandView { 27 | /** 28 | * @param {Command} model 29 | */ 30 | constructor(model) { 31 | this.model = model 32 | } 33 | 34 | /** 35 | * @param {string[]} args 36 | */ 37 | args(args) { 38 | return new CommandView({ 39 | ...this.model, 40 | args: [...this.model.args, ...args], 41 | }) 42 | } 43 | 44 | /** 45 | * @param {Record} env 46 | */ 47 | env(env) { 48 | return new CommandView({ 49 | ...this.model, 50 | env: { ...this.model.env, ...env }, 51 | }) 52 | } 53 | 54 | fork() { 55 | return fork(this.model) 56 | } 57 | 58 | join() { 59 | return join(this.model) 60 | } 61 | } 62 | 63 | /** 64 | * @param {Command} command 65 | */ 66 | export const fork = (command) => { 67 | const process = Process.spawn(command.program, command.args, { 68 | env: command.env, 69 | }) 70 | return new Fork(process) 71 | } 72 | 73 | /** 74 | * @param {Command} command 75 | */ 76 | export const join = (command) => fork(command).join() 77 | 78 | class Status { 79 | /** 80 | * @param {{code:number, signal?: void}|{signal:NodeJS.Signals, code?:void}} model 81 | */ 82 | constructor(model) { 83 | this.model = model 84 | } 85 | 86 | success() { 87 | return this.model.code === 0 88 | } 89 | 90 | get code() { 91 | return this.model.code ?? null 92 | } 93 | get signal() { 94 | return this.model.signal ?? null 95 | } 96 | } 97 | 98 | class Fork { 99 | /** 100 | * @param {Process.ChildProcess} process 101 | */ 102 | constructor(process) { 103 | this.process = process 104 | this.output = ByteStream.from(process.stdout ?? []) 105 | 106 | this.error = ByteStream.from(process.stderr ?? []) 107 | } 108 | join() { 109 | return new Join(this) 110 | } 111 | terminate() { 112 | this.process.kill() 113 | return this 114 | } 115 | } 116 | 117 | class Join { 118 | /** 119 | * @param {Fork} fork 120 | */ 121 | constructor(fork) { 122 | this.fork = fork 123 | this.output = '' 124 | this.error = '' 125 | 126 | readInto(fork.output.reader(), this, 'output') 127 | readInto(fork.error.reader(), this, 'error') 128 | } 129 | 130 | /** 131 | * @param {(ok: Outcome) => unknown} succeed 132 | * @param {(error: Outcome) => unknown} fail 133 | */ 134 | then(succeed, fail) { 135 | this.fork.process.once('close', (code, signal) => { 136 | const status = 137 | signal !== null 138 | ? new Status({ signal }) 139 | : new Status({ code: /** @type {number} */ (code) }) 140 | 141 | const { output, error } = this 142 | const outcome = { status, output, error } 143 | if (status.success()) { 144 | succeed(outcome) 145 | } else { 146 | fail( 147 | Object.assign( 148 | new Error(`command failed with status ${status.code}\n ${error}`), 149 | outcome 150 | ) 151 | ) 152 | } 153 | }) 154 | } 155 | 156 | /** 157 | * @returns {Promise} 158 | */ 159 | catch() { 160 | return Promise.resolve(this).catch((error) => error) 161 | } 162 | } 163 | 164 | 165 | /** 166 | * @template {string} Channel 167 | * @param {AsyncIterable} source 168 | * @param {{[key in Channel]: string}} output 169 | * @param {Channel} channel 170 | */ 171 | const readInto = async (source, output, channel) => { 172 | const decoder = new TextDecoder() 173 | for await (const chunk of source) { 174 | // Uncomment to debugger easily 175 | // console.log(decoder.decode(chunk)) 176 | output[channel] += decoder.decode(chunk) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/helpers/random.js: -------------------------------------------------------------------------------- 1 | import { CarWriter } from '@ipld/car' 2 | import * as CAR from '@ucanto/transport/car' 3 | import { CID } from 'multiformats/cid' 4 | import * as raw from 'multiformats/codecs/raw' 5 | import { sha256 } from 'multiformats/hashes/sha2' 6 | 7 | /** @param {number} size */ 8 | export async function randomBytes(size) { 9 | const bytes = new Uint8Array(size) 10 | while (size) { 11 | const chunk = new Uint8Array(Math.min(size, 65_536)) 12 | if (!globalThis.crypto) { 13 | try { 14 | const { webcrypto } = await import('node:crypto') 15 | webcrypto.getRandomValues(chunk) 16 | } catch (err) { 17 | throw new Error( 18 | 'unknown environment - no global crypto and not Node.js', 19 | { cause: err } 20 | ) 21 | } 22 | } else { 23 | crypto.getRandomValues(chunk) 24 | } 25 | size -= chunk.length 26 | bytes.set(chunk, size) 27 | } 28 | return bytes 29 | } 30 | 31 | /** @param {number} size */ 32 | export async function randomCAR(size) { 33 | const bytes = await randomBytes(size) 34 | return toCAR(bytes) 35 | } 36 | 37 | /** @param {Uint8Array} bytes */ 38 | export async function toBlock(bytes) { 39 | const hash = await sha256.digest(bytes) 40 | const cid = CID.createV1(raw.code, hash) 41 | return { cid, bytes } 42 | } 43 | 44 | /** 45 | * @param {Uint8Array} bytes 46 | */ 47 | export async function toCAR(bytes) { 48 | const block = await toBlock(bytes) 49 | const { writer, out } = CarWriter.create(block.cid) 50 | writer.put(block) 51 | writer.close() 52 | 53 | const chunks = [] 54 | for await (const chunk of out) { 55 | chunks.push(chunk) 56 | } 57 | const blob = new Blob(chunks) 58 | const cid = await CAR.codec.link(new Uint8Array(await blob.arrayBuffer())) 59 | 60 | return Object.assign(blob, { cid, roots: [block.cid] }) 61 | } 62 | -------------------------------------------------------------------------------- /test/helpers/receipt-http-server.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import { once } from 'events' 3 | 4 | import { parseLink } from '@ucanto/server' 5 | import * as Signer from '@ucanto/principal/ed25519' 6 | import { Receipt, Message } from '@ucanto/core' 7 | import * as CAR from '@ucanto/transport/car' 8 | import { Assert } from '@web3-storage/content-claims/capability' 9 | import { randomCAR } from './random.js' 10 | 11 | /** 12 | * @typedef {{ 13 | * server: http.Server 14 | * serverURL: URL 15 | * }} TestingServer 16 | */ 17 | 18 | /** 19 | * @returns {Promise} 20 | */ 21 | export async function createReceiptsServer() { 22 | /** 23 | * @param {http.IncomingMessage} request 24 | * @param {http.ServerResponse} response 25 | */ 26 | const listener = async (request, response) => { 27 | const taskCid = request.url?.split('/')[1] ?? '' 28 | const body = await generateReceipt(taskCid) 29 | response.writeHead(200) 30 | response.end(body) 31 | return undefined 32 | } 33 | 34 | const server = http.createServer(listener).listen() 35 | 36 | await once(server, 'listening') 37 | 38 | return { 39 | server, 40 | // @ts-expect-error 41 | serverURL: new URL(`http://127.0.0.1:${server.address().port}`), 42 | } 43 | } 44 | 45 | /** 46 | * @param {string} taskCid 47 | */ 48 | const generateReceipt = async (taskCid) => { 49 | const issuer = await Signer.generate() 50 | const content = (await randomCAR(128)).cid 51 | const locationClaim = await Assert.location.delegate({ 52 | issuer, 53 | audience: issuer, 54 | with: issuer.toDIDKey(), 55 | nb: { 56 | content, 57 | location: ['http://localhost'], 58 | }, 59 | expiration: Infinity, 60 | }) 61 | 62 | const receipt = await Receipt.issue({ 63 | issuer, 64 | fx: { 65 | fork: [locationClaim], 66 | }, 67 | /** @ts-expect-error not a UCAN Link */ 68 | ran: parseLink(taskCid), 69 | result: { 70 | ok: { 71 | site: locationClaim.link(), 72 | }, 73 | }, 74 | }) 75 | 76 | const message = await Message.build({ 77 | receipts: [receipt], 78 | }) 79 | return CAR.request.encode(message).body 80 | } 81 | -------------------------------------------------------------------------------- /test/helpers/stream.js: -------------------------------------------------------------------------------- 1 | const empty = () => EMPTY 2 | 3 | /** 4 | * @template {{}} T 5 | * @typedef {ReadableStream|AsyncIterable|Iterable} Source 6 | */ 7 | 8 | /** 9 | * @template {{}} T 10 | * @param {Source} source 11 | * @returns {Resource} 12 | */ 13 | const toResource = (source) => { 14 | if ('getReader' in source) { 15 | return source.getReader() 16 | } else { 17 | const iterator = 18 | Symbol.asyncIterator in source 19 | ? source[Symbol.asyncIterator]() 20 | : source[Symbol.iterator]() 21 | 22 | return { 23 | async read() { 24 | return /** @type {ReadableStreamReadResult} */ ( 25 | await iterator.next() 26 | ) 27 | }, 28 | releaseLock() { 29 | return iterator.return?.() 30 | }, 31 | async cancel(reason) { 32 | if (reason != null) { 33 | if (iterator.throw) { 34 | iterator.throw(reason) 35 | } else if (iterator.return) { 36 | iterator.return() 37 | } 38 | } else { 39 | iterator.return?.() 40 | } 41 | }, 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * @template {{}} T 48 | * @param {ReadableStream|AsyncIterable|Iterable} source 49 | * @returns {Stream} 50 | */ 51 | export const from = (source) => new Stream(toResource(source), {}, Direct) 52 | 53 | /** 54 | * @template {{}} T 55 | * @param {Resource} source 56 | * @param {number} n 57 | * @returns {Stream} 58 | */ 59 | const take = (source, n = 1) => 60 | new Stream(source, n, /** @type {Transform} */ (Take)) 61 | 62 | const Take = { 63 | /** 64 | * @template T 65 | * @param {number} n 66 | * @param {T} input 67 | * @returns {[number|undefined, T[]]} 68 | */ 69 | write: (n, input) => { 70 | if (n > 0) { 71 | return input != null ? [n - 1, [input]] : [n, []] 72 | } else { 73 | return [undefined, []] 74 | } 75 | }, 76 | flush: empty, 77 | } 78 | 79 | /** 80 | * @param {Resource} source 81 | * @returns {ByteStream<{}>} 82 | */ 83 | const toByteStream = (source) => new ByteStream(source, {}, Direct) 84 | 85 | /** 86 | * @template {{}} T 87 | * @param {Resource} source 88 | * @returns {Reader} 89 | */ 90 | const toReader = (source) => new Reader(source) 91 | 92 | /** 93 | * @template T 94 | * @param {Resource} source 95 | */ 96 | const collect = async (source) => { 97 | const chunks = [] 98 | for await (const chunk of iterate(source)) { 99 | chunks.push(chunk) 100 | } 101 | 102 | return chunks 103 | } 104 | 105 | /** 106 | * @param {Resource} source 107 | * @param {number} chunkSize 108 | * @returns {ByteStream} 109 | */ 110 | const chop = (source, chunkSize) => 111 | new ByteStream(source, new Uint8Array(chunkSize), Chop) 112 | 113 | const Chop = { 114 | /** 115 | * @param {Uint8Array} bytes 116 | * @param {Uint8Array} input 117 | * @returns {[Uint8Array, Uint8Array[]]} 118 | */ 119 | write(bytes, input) { 120 | const { byteLength } = bytes.buffer 121 | if (bytes.length + input.length < byteLength) { 122 | const buffer = new Uint8Array( 123 | bytes.buffer, 124 | 0, 125 | bytes.length + input.length 126 | ) 127 | buffer.set(input, bytes.length) 128 | return [buffer, []] 129 | } else { 130 | const chunk = new Uint8Array(byteLength) 131 | chunk.set(bytes, 0) 132 | chunk.set(input.slice(0, byteLength - bytes.length), bytes.length) 133 | 134 | const chunks = [chunk] 135 | 136 | let offset = byteLength - bytes.length 137 | while (offset + byteLength < input.length) { 138 | chunks.push(input.subarray(offset, offset + byteLength)) 139 | offset += byteLength 140 | } 141 | 142 | const buffer = new Uint8Array(bytes.buffer, 0, input.length - offset) 143 | buffer.set(input.subarray(offset), 0) 144 | 145 | return [buffer, chunks] 146 | } 147 | }, 148 | /** 149 | * @param {Uint8Array} bytes 150 | */ 151 | flush(bytes) { 152 | return bytes.length ? [bytes] : [] 153 | }, 154 | } 155 | 156 | /** 157 | * @param {Resource} source 158 | * @param {number} byte 159 | */ 160 | const delimit = (source, byte) => 161 | new ByteStream(source, { buffer: new Uint8Array(0), code: byte }, Delimiter) 162 | 163 | const Delimiter = { 164 | /** 165 | * @param {{code: number, buffer:Uint8Array}} state 166 | * @param {Uint8Array} input 167 | * @returns {[{code: number, buffer:Uint8Array}|undefined, Uint8Array[]]} 168 | */ 169 | write({ code, buffer }, input) { 170 | let start = 0 171 | let end = 0 172 | const chunks = [] 173 | while (end < input.length) { 174 | const byte = input[end] 175 | end++ 176 | if (byte === code) { 177 | const segment = input.subarray(start, end) 178 | if (buffer.length > 0) { 179 | const chunk = new Uint8Array(buffer.length + segment.length) 180 | chunk.set(buffer, 0) 181 | chunk.set(segment, buffer.length) 182 | chunks.push(chunk) 183 | buffer = new Uint8Array(0) 184 | } else { 185 | chunks.push(segment) 186 | } 187 | start = end 188 | } 189 | } 190 | 191 | const segment = input.subarray(start, end) 192 | const chunk = new Uint8Array(buffer.length + segment.length) 193 | chunk.set(buffer, 0) 194 | chunk.set(segment, buffer.length) 195 | 196 | return [{ code, buffer }, chunks] 197 | }, 198 | /** 199 | * @param {{code: number, buffer:Uint8Array}} state 200 | */ 201 | flush({ buffer }) { 202 | return buffer.length ? [buffer] : [] 203 | }, 204 | } 205 | 206 | /** 207 | * @template {{}} Out 208 | * @template {{}} State 209 | * @template {{}} [In=Out] 210 | * @typedef {object} Transform 211 | * @property {(state: State, input: In) => [State|undefined, Out[]]} write 212 | * @property {(state: State) => Out[]} flush 213 | */ 214 | /** 215 | * @template {{}} Out 216 | * @template {{}} State 217 | * @template {{}} In 218 | * @param {Resource} source 219 | * @param {State} state 220 | * @param {Transform} transform 221 | * @returns {Stream} 222 | */ 223 | const transform = (source, state, transform) => 224 | new Stream(source, state, transform) 225 | 226 | /** 227 | * @template T 228 | * @param {Resource} source 229 | */ 230 | const iterate = async function* (source) { 231 | try { 232 | while (true) { 233 | const { value, done } = await source.read() 234 | if (done) break 235 | yield value 236 | } 237 | } catch (error) { 238 | source.cancel(/** @type {{}} */ (error)) 239 | source.releaseLock() 240 | throw error 241 | } 242 | } 243 | 244 | const Direct = { 245 | /** 246 | * @template {{}} T 247 | * @template {{}} State 248 | * @param {State} state 249 | * @param {T} input 250 | */ 251 | write(state, input) { 252 | OUT.pop() 253 | if (input != null) { 254 | OUT.push(input) 255 | } 256 | STEP[0] = state 257 | return STEP 258 | }, 259 | /** 260 | * @returns {never[]} 261 | */ 262 | flush() { 263 | return EMPTY 264 | }, 265 | } 266 | /** 267 | * @template {{}} Out 268 | * @template {{}} [State={}] 269 | * @template {{}} [In=Out] 270 | * @extends {ReadableStream} 271 | */ 272 | export class Stream extends ReadableStream { 273 | /** 274 | * @param {Resource} source 275 | * @param {State} state 276 | * @param {Transform} transformer 277 | */ 278 | constructor(source, state, { write, flush }) { 279 | super({ 280 | /** 281 | * @param {ReadableStreamDefaultController} controller 282 | */ 283 | pull: async (controller) => { 284 | try { 285 | const { done, value } = await source.read() 286 | if (done) { 287 | controller.close() 288 | source.releaseLock() 289 | } else { 290 | const [next, output] = write(state, value) 291 | for (const item of output) { 292 | controller.enqueue(item) 293 | } 294 | 295 | if (next) { 296 | state = next 297 | } else { 298 | controller.close() 299 | source.cancel() 300 | source.releaseLock() 301 | } 302 | } 303 | } catch (error) { 304 | controller.error(error) 305 | source.releaseLock() 306 | } 307 | }, 308 | cancel(controller) { 309 | source.cancel() 310 | source.releaseLock() 311 | for (const item of flush(state)) { 312 | controller.enqueue(item) 313 | } 314 | }, 315 | }) 316 | } 317 | 318 | /** 319 | * @template {{}} State 320 | * @template {{}} T 321 | * @param {State} state 322 | * @param {Transform} transformer 323 | */ 324 | transform(state, transformer) { 325 | return transform(this.getReader(), state, transformer) 326 | } 327 | 328 | /** 329 | * @returns {Reader} 330 | */ 331 | reader() { 332 | return toReader(this.getReader()) 333 | } 334 | 335 | /** 336 | * @returns {AsyncIterable} 337 | */ 338 | [Symbol.asyncIterator]() { 339 | return iterate(this.getReader()) 340 | } 341 | 342 | /** 343 | * @param {number} n 344 | */ 345 | take(n = 1) { 346 | return take(this.getReader(), n) 347 | } 348 | 349 | collect() { 350 | return collect(this.getReader()) 351 | } 352 | } 353 | 354 | /** 355 | * @template {{}} [State={}] 356 | * @extends {Stream} 357 | */ 358 | export class ByteStream extends Stream { 359 | /** 360 | * @param {Source} source 361 | */ 362 | static from(source) { 363 | return new ByteStream(toResource(source), {}, Direct) 364 | } 365 | 366 | reader() { 367 | return new BytesReader(this.getReader()) 368 | } 369 | 370 | text() { 371 | return this.reader().text() 372 | } 373 | bytes() { 374 | return this.reader().bytes() 375 | } 376 | 377 | /** 378 | * @param {number} n 379 | */ 380 | take(n = 1) { 381 | return toByteStream(take(this.getReader(), n).getReader()) 382 | } 383 | /** 384 | * @param {number} size 385 | */ 386 | chop(size) { 387 | return chop(this.getReader(), size) 388 | } 389 | 390 | /** 391 | * @param {number} byte 392 | */ 393 | delimit(byte) { 394 | return delimit(this.getReader(), byte) 395 | } 396 | 397 | lines() { 398 | return this.delimit('\n'.charCodeAt(0)) 399 | } 400 | } 401 | 402 | /** 403 | * @template T 404 | * @typedef {object} Resource 405 | * @property {() => Promise>} read 406 | * @property {() => void} releaseLock 407 | * @property {(reason?: {}) => void} cancel 408 | */ 409 | 410 | /** @type {never[]} */ 411 | const EMPTY = [] 412 | 413 | /** @type {any[]} */ 414 | const OUT = [] 415 | /** @type {[any, any[]]} */ 416 | const STEP = [{}, OUT] 417 | 418 | /** 419 | * @template {{}} T 420 | */ 421 | class Reader { 422 | /** 423 | * @param {Resource} source 424 | */ 425 | constructor(source) { 426 | this.source = source 427 | } 428 | read() { 429 | return this.source.read() 430 | } 431 | releaseLock() { 432 | return this.source.releaseLock() 433 | } 434 | 435 | /** 436 | * @param {{}} [reason] 437 | */ 438 | cancel(reason) { 439 | const result = this.source.cancel(reason) 440 | this.source.releaseLock() 441 | return result 442 | } 443 | async *[Symbol.asyncIterator]() { 444 | while (true) { 445 | const { value, done } = await this.read() 446 | if (done) break 447 | yield value 448 | } 449 | this.cancel() 450 | } 451 | 452 | take(n = 1) { 453 | return take(this.source, n).reader() 454 | } 455 | 456 | collect() { 457 | return collect(this.source) 458 | } 459 | } 460 | 461 | /** 462 | * @extends {Reader} 463 | */ 464 | class BytesReader extends Reader { 465 | async bytes() { 466 | const chunks = [] 467 | let length = 0 468 | for await (const chunk of this) { 469 | chunks.push(chunk) 470 | length += chunk.length 471 | } 472 | 473 | const bytes = new Uint8Array(length) 474 | let offset = 0 475 | for (const chunk of chunks) { 476 | bytes.set(chunk, offset) 477 | offset += chunk.length 478 | } 479 | 480 | return bytes 481 | } 482 | async text() { 483 | return new TextDecoder().decode(await this.bytes()) 484 | } 485 | 486 | take(n = 1) { 487 | return ByteStream.from(take(this.source, n)).reader() 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /test/helpers/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {{ raw: ArrayLike }} template 3 | * @param {unknown[]} substitutions 4 | */ 5 | export const pattern = (template, ...substitutions) => 6 | new RegExp(String.raw(template, ...substitutions)) 7 | 8 | /** 9 | * @param {RegExp} pattern 10 | * @param {string} source 11 | * @returns {string[]} 12 | */ 13 | export const match = (pattern, source) => { 14 | const match = source.match(pattern) 15 | if (!match) { 16 | return [] 17 | } 18 | return match 19 | } 20 | -------------------------------------------------------------------------------- /test/lib.spec.js: -------------------------------------------------------------------------------- 1 | import * as Link from 'multiformats/link' 2 | import { 3 | filesize, 4 | uploadListResponseToString, 5 | storeListResponseToString, 6 | asCarLink, 7 | parseCarLink, 8 | } from '../lib.js' 9 | 10 | /** 11 | * @typedef {import('multiformats').LinkJSON} LinkJSON 12 | * @typedef {import('@web3-storage/w3up-client/types').CARLink} CARLink 13 | */ 14 | 15 | /** @type {import('entail').Suite} */ 16 | export const testFilesize = { 17 | filesize: (assert) => { 18 | /** @type {Array<[number, string]>} */ 19 | const testdata = [ 20 | [5, '5B'], 21 | [50, '0.1KB'], 22 | [500, '0.5KB'], 23 | [5_000, '5.0KB'], 24 | [50_000, '0.1MB'], 25 | [500_000, '0.5MB'], 26 | [5_000_000, '5.0MB'], 27 | [50_000_000, '0.1GB'], 28 | [500_000_000, '0.5GB'], 29 | [5_000_000_000, '5.0GB'], 30 | ] 31 | testdata.forEach(([size, str]) => assert.equal(filesize(size), str)) 32 | }, 33 | } 34 | 35 | /** @type {import('@web3-storage/w3up-client/types').UploadListSuccess} */ 36 | const uploadListResponse = { 37 | size: 2, 38 | cursor: 'bafybeibvbxjeodaa6hdqlgbwmv4qzdp3bxnwdoukay4dpl7aemkiwc2eje', 39 | results: [ 40 | { 41 | root: Link.parse( 42 | 'bafybeia7tr4dgyln7zeyyyzmkppkcts6azdssykuluwzmmswysieyadcbm' 43 | ), 44 | shards: [ 45 | Link.parse( 46 | 'bagbaierantza4rfjnhqksp2stcnd2tdjrn3f2kgi2wrvaxmayeuolryi66fq' 47 | ), 48 | ], 49 | updatedAt: new Date().toISOString(), 50 | insertedAt: new Date().toISOString(), 51 | }, 52 | { 53 | root: Link.parse( 54 | 'bafybeibvbxjeodaa6hdqlgbwmv4qzdp3bxnwdoukay4dpl7aemkiwc2eje' 55 | ), 56 | shards: [ 57 | Link.parse( 58 | 'bagbaieraxqbkzwvx5on6an4br5hagfgesdfc6adchy3hf5qt34pupfjd3rbq' 59 | ), 60 | ], 61 | updatedAt: new Date().toISOString(), 62 | insertedAt: new Date().toISOString(), 63 | }, 64 | ], 65 | after: 'bafybeibvbxjeodaa6hdqlgbwmv4qzdp3bxnwdoukay4dpl7aemkiwc2eje', 66 | before: 'bafybeia7tr4dgyln7zeyyyzmkppkcts6azdssykuluwzmmswysieyadcbm', 67 | } 68 | 69 | /** @type {import('entail').Suite} */ 70 | export const testUpload = { 71 | 'uploadListResponseToString can return the upload roots CIDs as strings': ( 72 | assert 73 | ) => { 74 | assert.equal( 75 | uploadListResponseToString(uploadListResponse, {}), 76 | `bafybeia7tr4dgyln7zeyyyzmkppkcts6azdssykuluwzmmswysieyadcbm 77 | bafybeibvbxjeodaa6hdqlgbwmv4qzdp3bxnwdoukay4dpl7aemkiwc2eje` 78 | ) 79 | }, 80 | 81 | 'uploadListResponseToString can return the upload roots as newline delimited JSON': 82 | (assert) => { 83 | assert.equal( 84 | uploadListResponseToString(uploadListResponse, { shards: true, plainTree: true }), 85 | `bafybeia7tr4dgyln7zeyyyzmkppkcts6azdssykuluwzmmswysieyadcbm 86 | └─┬ shards 87 | └── bagbaierantza4rfjnhqksp2stcnd2tdjrn3f2kgi2wrvaxmayeuolryi66fq 88 | 89 | bafybeibvbxjeodaa6hdqlgbwmv4qzdp3bxnwdoukay4dpl7aemkiwc2eje 90 | └─┬ shards 91 | └── bagbaieraxqbkzwvx5on6an4br5hagfgesdfc6adchy3hf5qt34pupfjd3rbq 92 | ` 93 | ) 94 | }, 95 | 96 | 'uploadListResponseToString can return the upload roots and shards as a tree': 97 | (assert) => { 98 | assert.equal( 99 | uploadListResponseToString(uploadListResponse, { json: true }), 100 | `{"root":{"/":"bafybeia7tr4dgyln7zeyyyzmkppkcts6azdssykuluwzmmswysieyadcbm"},"shards":[{"/":"bagbaierantza4rfjnhqksp2stcnd2tdjrn3f2kgi2wrvaxmayeuolryi66fq"}]} 101 | {"root":{"/":"bafybeibvbxjeodaa6hdqlgbwmv4qzdp3bxnwdoukay4dpl7aemkiwc2eje"},"shards":[{"/":"bagbaieraxqbkzwvx5on6an4br5hagfgesdfc6adchy3hf5qt34pupfjd3rbq"}]}` 102 | ) 103 | }, 104 | } 105 | 106 | /** @type {import('@web3-storage/w3up-client/types').StoreListSuccess} */ 107 | const storeListResponse = { 108 | size: 2, 109 | cursor: 'bagbaieracmkgwrw6rowsk5jse5eihyhszyrq5w23aqosajyckn2tfbotdcqq', 110 | results: [ 111 | { 112 | link: Link.parse( 113 | 'bagbaierablvu5d2q5uoimuy2tlc3tcntahnw2j7s7jjaznawc23zgdgcisma' 114 | ), 115 | size: 5336, 116 | insertedAt: new Date().toISOString(), 117 | }, 118 | { 119 | link: Link.parse( 120 | 'bagbaieracmkgwrw6rowsk5jse5eihyhszyrq5w23aqosajyckn2tfbotdcqq' 121 | ), 122 | size: 3297, 123 | insertedAt: new Date().toISOString(), 124 | }, 125 | ], 126 | after: 'bagbaieracmkgwrw6rowsk5jse5eihyhszyrq5w23aqosajyckn2tfbotdcqq', 127 | before: 'bagbaierablvu5d2q5uoimuy2tlc3tcntahnw2j7s7jjaznawc23zgdgcisma', 128 | } 129 | 130 | /** @type {import('entail').Suite} */ 131 | export const testStore = { 132 | 'storeListResponseToString can return the CAR CIDs as strings': (assert) => { 133 | assert.equal( 134 | storeListResponseToString(storeListResponse, {}), 135 | `bagbaierablvu5d2q5uoimuy2tlc3tcntahnw2j7s7jjaznawc23zgdgcisma 136 | bagbaieracmkgwrw6rowsk5jse5eihyhszyrq5w23aqosajyckn2tfbotdcqq` 137 | ) 138 | }, 139 | 140 | 'storeListResponseToString can return the CAR CIDs as newline delimited JSON': 141 | (assert) => { 142 | assert.equal( 143 | storeListResponseToString(storeListResponse, { json: true }), 144 | `{"link":{"/":"bagbaierablvu5d2q5uoimuy2tlc3tcntahnw2j7s7jjaznawc23zgdgcisma"},"size":5336} 145 | {"link":{"/":"bagbaieracmkgwrw6rowsk5jse5eihyhszyrq5w23aqosajyckn2tfbotdcqq"},"size":3297}` 146 | ) 147 | }, 148 | 149 | asCarLink: (assert) => { 150 | assert.equal( 151 | asCarLink( 152 | Link.parse( 153 | 'bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea' 154 | ) 155 | ), 156 | undefined 157 | ) 158 | const carLink = Link.parse( 159 | 'bagbaieraxkuzouwfuphnqlbbpobywmypb26stej5vbwkelrv7chdqoxfuuea' 160 | ) 161 | assert.equal(asCarLink(carLink), /** @type {CARLink} */ (carLink)) 162 | }, 163 | 164 | parseCarLink: (assert) => { 165 | const carLink = Link.parse( 166 | 'bagbaieraxkuzouwfuphnqlbbpobywmypb26stej5vbwkelrv7chdqoxfuuea' 167 | ) 168 | assert.deepEqual( 169 | parseCarLink(carLink.toString()), 170 | /** @type {CARLink} */ (carLink) 171 | ) 172 | assert.equal(parseCarLink('nope'), undefined) 173 | assert.equal( 174 | parseCarLink( 175 | 'bafybeiajdopsmspomlrpaohtzo5sdnpknbolqjpde6huzrsejqmvijrcea' 176 | ), 177 | undefined 178 | ) 179 | }, 180 | } 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "dist", 5 | // project options 6 | "allowJs": true, 7 | "checkJs": true, 8 | "target": "ES2022", 9 | "module": "ES2022", 10 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 11 | "noEmit": true, 12 | "isolatedModules": true, 13 | "removeComments": false, 14 | // module resolution 15 | "esModuleInterop": true, 16 | "moduleResolution": "Node", 17 | // linter checks 18 | "noImplicitReturns": false, 19 | "noFallthroughCasesInSwitch": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": false, 22 | // advanced 23 | "importsNotUsedAsValues": "remove", 24 | "forceConsistentCasingInFileNames": true, 25 | "skipLibCheck": true, 26 | "stripInternal": true, 27 | "resolveJsonModule": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------