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