@interop/did-cli 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +144 -6
  3. package/dist/commands/did.d.ts.map +1 -1
  4. package/dist/commands/did.js +41 -12
  5. package/dist/commands/did.js.map +1 -1
  6. package/dist/commands/key.d.ts.map +1 -1
  7. package/dist/commands/key.js +46 -8
  8. package/dist/commands/key.js.map +1 -1
  9. package/dist/commands/was/collection.d.ts +121 -0
  10. package/dist/commands/was/collection.d.ts.map +1 -0
  11. package/dist/commands/was/collection.js +316 -0
  12. package/dist/commands/was/collection.js.map +1 -0
  13. package/dist/commands/was/policy.d.ts +50 -0
  14. package/dist/commands/was/policy.d.ts.map +1 -0
  15. package/dist/commands/was/policy.js +133 -0
  16. package/dist/commands/was/policy.js.map +1 -0
  17. package/dist/commands/was/publish.d.ts +67 -0
  18. package/dist/commands/was/publish.d.ts.map +1 -0
  19. package/dist/commands/was/publish.js +151 -0
  20. package/dist/commands/was/publish.js.map +1 -0
  21. package/dist/commands/was/resource.d.ts +148 -0
  22. package/dist/commands/was/resource.d.ts.map +1 -0
  23. package/dist/commands/was/resource.js +402 -0
  24. package/dist/commands/was/resource.js.map +1 -0
  25. package/dist/commands/was/shared.d.ts +156 -0
  26. package/dist/commands/was/shared.d.ts.map +1 -0
  27. package/dist/commands/was/shared.js +237 -0
  28. package/dist/commands/was/shared.js.map +1 -0
  29. package/dist/commands/was/space.d.ts +196 -0
  30. package/dist/commands/was/space.d.ts.map +1 -0
  31. package/dist/commands/was/space.js +516 -0
  32. package/dist/commands/was/space.js.map +1 -0
  33. package/dist/commands/was/tree.d.ts +44 -0
  34. package/dist/commands/was/tree.d.ts.map +1 -0
  35. package/dist/commands/was/tree.js +135 -0
  36. package/dist/commands/was/tree.js.map +1 -0
  37. package/dist/commands/was.d.ts +29 -496
  38. package/dist/commands/was.d.ts.map +1 -1
  39. package/dist/commands/was.js +84 -1473
  40. package/dist/commands/was.js.map +1 -1
  41. package/dist/commands/zcap.d.ts +22 -2
  42. package/dist/commands/zcap.d.ts.map +1 -1
  43. package/dist/commands/zcap.js +6 -20
  44. package/dist/commands/zcap.js.map +1 -1
  45. package/dist/meta.js +1 -1
  46. package/dist/meta.js.map +1 -1
  47. package/dist/was/capability.d.ts +10 -13
  48. package/dist/was/capability.d.ts.map +1 -1
  49. package/dist/was/capability.js +1 -59
  50. package/dist/was/capability.js.map +1 -1
  51. package/dist/zcap/delegate.d.ts +2 -2
  52. package/dist/zcap/delegate.js +2 -2
  53. package/dist/zcap/resolve.d.ts +15 -0
  54. package/dist/zcap/resolve.d.ts.map +1 -0
  55. package/dist/zcap/resolve.js +55 -0
  56. package/dist/zcap/resolve.js.map +1 -0
  57. package/package.json +29 -16
@@ -10,9 +10,12 @@
10
10
  * DID so day-to-day commands need no `--server`/`--did` flags.
11
11
  *
12
12
  * Subcommand groups: `space` (`create`, `list`, `show`, `update` alias
13
- * `configure`, `delete`, `forget`, `add`), `collection` (alias `coll`;
14
- * `create`, `list`, `show`, `update`, `delete`), and `resource` (alias
15
- * `res`; `add`, `put`, `get`, `list`, `delete`). The top-level shorthand
13
+ * `configure`, `delete`, `forget`, `add`, `backends`, `quotas`),
14
+ * `collection` (alias `coll`;
15
+ * `create`, `list`, `show`, `update`, `delete`, `backend`, `quota`),
16
+ * `resource` (alias
17
+ * `res`; `add`, `put`, `get`, `list`, `delete`), and `resource-meta`
18
+ * (alias `meta`; `get`, `put`). The top-level shorthand
16
19
  * verbs `ls`, `get`, `put`, and `rm` dispatch on the path depth, mirroring
17
20
  * the client's uniform-verbs-at-every-level design. Resource payloads come
18
21
  * from a file argument or stdin, with JSON-vs-binary detection in
@@ -21,1476 +24,18 @@
21
24
  * Data goes to stdout, diagnostics to stderr. Exit codes: 0 success, 1
22
25
  * operation error (typed WAS errors and not-found/not-visible reads), 2
23
26
  * input error (bad path syntax, unknown handle/DID, missing server URL).
24
- */
25
- import { readFile } from 'node:fs/promises';
26
- import { Command, Option } from 'commander';
27
- import { WasError } from '@interop/was-client';
28
- import { parseWasAddress } from '../was/address.js';
29
- import { buildWasClient, resolveWasTarget } from '../was/client.js';
30
- import { resolveCapabilityTarget } from '../was/capability.js';
31
- import { readInputBytes, readPayload, writeBytesOutput, writeResourceOutput } from '../was/io.js';
32
- import { listSpaceRecords, removeSpaceRecord, resolveSpaceRef, saveSpaceRecord } from '../was/registry.js';
33
- import { resolveDidRef } from '../meta.js';
34
- import { saveToCollection } from '../storage.js';
35
- import { encodeCapability } from '../zcap/encoding.js';
36
- import { expiresFromTtl } from '../zcap/ttl.js';
37
- import { storageIdFor, writeCreateMeta } from './zcap.js';
38
- import { renderTable } from '../table.js';
39
- /**
40
- * Awaits a command's run function and exits the process with its code when
41
- * non-zero -- the shared tail of every `was` command action.
42
- *
43
- * @param run {Promise<number>}
44
- * @returns {Promise<void>}
45
- */
46
- async function runAndExit(run) {
47
- const code = await run;
48
- if (code !== 0) {
49
- process.exit(code);
50
- }
51
- }
52
- /**
53
- * The shared command options, as factories (commander `Option` instances
54
- * are bound to the command they are added to, so they cannot be shared).
55
- * Centralizing them keeps the flag names and help text consistent across
56
- * the whole command tree.
57
- *
58
- * @returns {Option}
59
- */
60
- function serverOption() {
61
- return new Option('--server <url>', 'the WAS server base URL (or WAS_SERVER_URL)');
62
- }
63
- function didOption() {
64
- return new Option('--did <did>', 'DID or stored-DID handle to sign with (or WAS_DID)');
65
- }
66
- function capabilityOption() {
67
- return new Option('--capability <ref>', 'a received capability (encoded string, JSON file, or stored zcap ' +
68
- 'id/handle) instead of a path');
69
- }
70
- function contentTypeOption() {
71
- return new Option('--content-type <type>', 'send the payload as-is with this content type (skips JSON detection)');
72
- }
73
- /** The capability actions WAS servers match against (HTTP verbs). */
74
- const WAS_ACTIONS = ['GET', 'PUT', 'POST', 'DELETE'];
75
- /**
76
- * Normalizes grant action verbs to their canonical uppercase form,
77
- * rejecting anything that is not a WAS-supported HTTP verb.
78
- *
79
- * @param actions {string[]}
80
- * @returns {WasAction[]}
81
- */
82
- function normalizeActions(actions) {
83
- return actions.map(action => {
84
- const verb = action.toUpperCase();
85
- if (!WAS_ACTIONS.includes(verb)) {
86
- throw new Error(`Unknown action "${action}" (supported: GET, PUT, POST, DELETE).`);
87
- }
88
- return verb;
89
- });
90
- }
91
- /**
92
- * Guards a run function against receiving both path- and capability-based
93
- * addressing (or neither).
94
- *
95
- * @param options {object}
96
- * @param [options.address] {string}
97
- * @param [options.capability] {string}
98
- * @returns {void}
99
- */
100
- function assertOneAddressing({ address, capability }) {
101
- if (address && capability) {
102
- throw new Error('Provide either a path or --capability, not both.');
103
- }
104
- if (!address && !capability) {
105
- throw new Error('Provide a path or --capability.');
106
- }
107
- }
108
- /**
109
- * Builds the canonical URL of a space, collection, or resource on its
110
- * server, for messages and output.
111
- *
112
- * @param options {object}
113
- * @param options.server {string}
114
- * @param options.spaceId {string}
115
- * @param [options.collectionId] {string}
116
- * @param [options.resourceId] {string}
117
- * @returns {string}
118
- */
119
- function wasUrl({ server, spaceId, collectionId, resourceId }) {
120
- const segments = [spaceId, collectionId, resourceId]
121
- .filter((segment) => segment !== undefined)
122
- .map(encodeURIComponent);
123
- return `${server.replace(/\/$/, '')}/space/${segments.join('/')}`;
124
- }
125
- /**
126
- * Prints a one-line error for a failed command and classifies its exit code:
127
- * 1 for operation errors (typed WAS errors from the server exchange), 2 for
128
- * input errors (bad addresses, unknown handles/DIDs, missing server URL).
129
- *
130
- * @param options {object}
131
- * @param options.action {string} What failed, e.g. `create the space`.
132
- * @param options.err {unknown}
133
- * @returns {number} The process exit code.
134
- */
135
- function reportError({ action, err }) {
136
- const message = err instanceof Error ? err.message : String(err);
137
- console.error(`Could not ${action}: ${message}`);
138
- return err instanceof WasError ? 1 : 2;
139
- }
140
- /**
141
- * Parses a WAS address and asserts it addresses a space only (no
142
- * collection/resource segments), as the `space` subcommands require.
143
- *
144
- * @param address {string}
145
- * @returns {{server?: string, spaceRef: string}}
146
- */
147
- function parseSpaceAddress(address) {
148
- const parsed = parseWasAddress(address);
149
- if (parsed.collectionId !== undefined) {
150
- throw new Error(`"${address}" addresses a collection or resource; ` +
151
- 'space commands take a space address.');
152
- }
153
- return parsed;
154
- }
155
- /**
156
- * Asserts a resolved target addresses a collection (`SPACE/COLLECTION`,
157
- * no resource segment) and narrows its type accordingly.
158
- *
159
- * @param options {object}
160
- * @param options.target {ResolvedWasTarget}
161
- * @param options.address {string} The original address, for error messages.
162
- * @returns {ResolvedWasTarget & {collectionId: string}}
163
- */
164
- function requireCollectionTarget({ target, address }) {
165
- if (target.collectionId === undefined || target.resourceId !== undefined) {
166
- throw new Error(`"${address}" must address a collection (SPACE/COLLECTION).`);
167
- }
168
- return target;
169
- }
170
- /**
171
- * Asserts a resolved target addresses a resource
172
- * (`SPACE/COLLECTION/RESOURCE`) and narrows its type accordingly.
173
- *
174
- * @param options {object}
175
- * @param options.target {ResolvedWasTarget}
176
- * @param options.address {string} The original address, for error messages.
177
- * @returns {ResolvedWasTarget & {collectionId: string, resourceId: string}}
178
- */
179
- function requireResourceTarget({ target, address }) {
180
- if (target.collectionId === undefined || target.resourceId === undefined) {
181
- throw new Error(`"${address}" must address a resource (SPACE/COLLECTION/RESOURCE).`);
182
- }
183
- return target;
184
- }
185
- /**
186
- * Creates a space on the server and prints `{ id, url, name?, controller }`.
187
- *
188
- * @param options {object}
189
- * @param [options.name] {string} The space's display name.
190
- * @param [options.server] {string} The server base URL.
191
- * @param [options.did] {string} The signing DID or stored-DID handle.
192
- * @param [options.id] {string} A caller-chosen space id.
193
- * @param [options.save] {boolean} Register the space in the local wallet.
194
- * @param [options.handle] {string} Short tag for the registry entry.
195
- * @param [options.description] {string} Longer registry entry description.
196
- * @returns {Promise<number>} The process exit code.
197
- */
198
- export async function runSpaceCreate(options) {
199
- try {
200
- const { client, server, did } = await buildWasClient({
201
- server: options.server,
202
- did: options.did
203
- });
204
- const space = await client.createSpace({
205
- ...(options.name !== undefined && { name: options.name }),
206
- ...(options.id !== undefined && { id: options.id })
207
- });
208
- const url = wasUrl({ server, spaceId: space.id });
209
- if (options.save) {
210
- const filePath = await saveSpaceRecord({
211
- record: {
212
- id: space.id,
213
- ...(options.name !== undefined && { name: options.name }),
214
- server,
215
- controller: did
216
- },
217
- handle: options.handle,
218
- description: options.description
219
- });
220
- console.error(`Space registered in ${filePath}`);
221
- }
222
- console.log(JSON.stringify({
223
- id: space.id,
224
- url,
225
- ...(options.name !== undefined && { name: options.name }),
226
- controller: did
227
- }, null, 2));
228
- return 0;
229
- }
230
- catch (err) {
231
- return reportError({ action: 'create the space', err });
232
- }
233
- }
234
- /**
235
- * Lists the locally registered spaces (the working model while servers do
236
- * not implement List Spaces); `--remote` asks the server instead.
237
- *
238
- * @param options {object}
239
- * @param [options.json] {boolean} Output a JSON array with metadata.
240
- * @param [options.plain] {boolean} Output one space id per line.
241
- * @param [options.remote] {boolean} List spaces on the server instead.
242
- * @param [options.server] {string} The server base URL (with `--remote`).
243
- * @param [options.did] {string} The signing DID (with `--remote`).
244
- * @returns {Promise<number>} The process exit code.
245
- */
246
- export async function runSpaceList(options) {
247
- try {
248
- if (options.remote) {
249
- const { client } = await buildWasClient({
250
- server: options.server,
251
- did: options.did
252
- });
253
- const listing = await client.listSpaces();
254
- console.log(JSON.stringify(listing, null, 2));
255
- return 0;
256
- }
257
- const entries = await listSpaceRecords();
258
- if (options.plain) {
259
- const spaceIds = entries.map(entry => entry.record.id).sort();
260
- for (const spaceId of spaceIds) {
261
- console.log(spaceId);
262
- }
263
- return 0;
264
- }
265
- if (options.json) {
266
- const output = entries.map(entry => ({
267
- id: entry.record.id,
268
- server: entry.record.server,
269
- ...(entry.record.name && { name: entry.record.name }),
270
- ...(entry.record.controller && {
271
- controller: entry.record.controller
272
- }),
273
- ...(entry.meta?.created && { created: entry.meta.created }),
274
- ...(entry.meta?.handle && { handle: entry.meta.handle }),
275
- ...(entry.meta?.description && {
276
- description: entry.meta.description
277
- })
278
- }));
279
- console.log(JSON.stringify(output, null, 2));
280
- return 0;
281
- }
282
- if (entries.length === 0) {
283
- return 0;
284
- }
285
- const rows = entries.map(entry => [
286
- entry.meta?.handle ?? '',
287
- entry.record.name ?? '',
288
- entry.record.id,
289
- entry.record.server,
290
- entry.meta?.created?.slice(0, 10) ?? ''
291
- ]);
292
- console.log(renderTable({
293
- columns: [
294
- { header: 'HANDLE', maxWidth: 16 },
295
- { header: 'NAME', maxWidth: 20 },
296
- { header: 'SPACE ID', maxWidth: 40 },
297
- { header: 'SERVER', maxWidth: 32 },
298
- { header: 'CREATED' }
299
- ],
300
- rows
301
- }));
302
- return 0;
303
- }
304
- catch (err) {
305
- return reportError({ action: 'list spaces', err });
306
- }
307
- }
308
- /**
309
- * Shows a space: its Space Description from the server, or (`--meta`) its
310
- * local registry record and metadata.
311
- *
312
- * @param options {object}
313
- * @param options.address {string} The space address.
314
- * @param [options.meta] {boolean} Show the local registry metadata instead.
315
- * @param [options.json] {boolean} With `--meta`, output JSON.
316
- * @param [options.server] {string} The server base URL.
317
- * @param [options.did] {string} The signing DID or stored-DID handle.
318
- * @returns {Promise<number>} The process exit code.
319
- */
320
- export async function runSpaceShow(options) {
321
- try {
322
- const parsed = parseSpaceAddress(options.address);
323
- if (options.meta) {
324
- const entry = await resolveSpaceRef({ ref: parsed.spaceRef });
325
- if (!entry) {
326
- throw new Error(`No locally registered space found for "${parsed.spaceRef}".`);
327
- }
328
- if (options.json) {
329
- const output = {
330
- id: entry.record.id,
331
- server: entry.record.server,
332
- ...(entry.record.name && { name: entry.record.name }),
333
- ...(entry.record.controller && {
334
- controller: entry.record.controller
335
- }),
336
- ...(entry.meta?.created && { created: entry.meta.created }),
337
- ...(entry.meta?.handle && { handle: entry.meta.handle }),
338
- ...(entry.meta?.description && {
339
- description: entry.meta.description
340
- })
341
- };
342
- console.log(JSON.stringify(output, null, 2));
343
- return 0;
344
- }
345
- const rows = [
346
- ['ID', entry.record.id],
347
- ['Name', entry.record.name ?? ''],
348
- ['Server', entry.record.server],
349
- ['Controller', entry.record.controller ?? ''],
350
- ['Handle', entry.meta?.handle ?? ''],
351
- ['Created', entry.meta?.created ?? ''],
352
- ['Description', entry.meta?.description ?? '']
353
- ];
354
- console.log(renderTable({
355
- columns: [{ header: 'FIELD' }, { header: 'VALUE' }],
356
- rows
357
- }));
358
- return 0;
359
- }
360
- const target = await resolveWasTarget({
361
- address: options.address,
362
- server: options.server,
363
- did: options.did
364
- });
365
- const description = await target.client.space(target.spaceId).describe();
366
- if (description === null) {
367
- console.error('Not found (or not visible to you): ' +
368
- wasUrl({ server: target.server, spaceId: target.spaceId }));
369
- return 1;
370
- }
371
- console.log(JSON.stringify(description, null, 2));
372
- return 0;
373
- }
374
- catch (err) {
375
- return reportError({ action: 'show the space', err });
376
- }
377
- }
378
- /**
379
- * Updates a space's description fields on the server (upsert via
380
- * `configure()`), refreshing the name in the local registry entry when one
381
- * exists.
382
- *
383
- * @param options {object}
384
- * @param options.address {string} The space address.
385
- * @param [options.name] {string} The new display name.
386
- * @param [options.server] {string} The server base URL.
387
- * @param [options.did] {string} The signing DID or stored-DID handle.
388
- * @returns {Promise<number>} The process exit code.
389
- */
390
- export async function runSpaceUpdate(options) {
391
- try {
392
- parseSpaceAddress(options.address);
393
- const target = await resolveWasTarget({
394
- address: options.address,
395
- server: options.server,
396
- did: options.did
397
- });
398
- const description = await target.client.space(target.spaceId).configure({
399
- ...(options.name !== undefined && { name: options.name })
400
- });
401
- if (target.entry && options.name !== undefined) {
402
- await saveSpaceRecord({
403
- record: { ...target.entry.record, name: options.name }
404
- });
405
- }
406
- console.log(JSON.stringify(description, null, 2));
407
- return 0;
408
- }
409
- catch (err) {
410
- return reportError({ action: 'update the space', err });
411
- }
412
- }
413
- /**
414
- * Deletes a space on the server (idempotent) and removes its local registry
415
- * entry when one exists. The local-only counterpart is `forget`.
416
- *
417
- * @param options {object}
418
- * @param options.address {string} The space address.
419
- * @param [options.server] {string} The server base URL.
420
- * @param [options.did] {string} The signing DID or stored-DID handle.
421
- * @returns {Promise<number>} The process exit code.
422
- */
423
- export async function runSpaceDelete(options) {
424
- try {
425
- parseSpaceAddress(options.address);
426
- const target = await resolveWasTarget({
427
- address: options.address,
428
- server: options.server,
429
- did: options.did
430
- });
431
- await target.client.space(target.spaceId).delete();
432
- console.error('Deleted ' +
433
- wasUrl({ server: target.server, spaceId: target.spaceId }) +
434
- ' on the server.');
435
- if (target.entry) {
436
- const removed = await removeSpaceRecord({
437
- storageId: target.entry.storageId
438
- });
439
- for (const filePath of removed) {
440
- console.error(`Removed ${filePath}`);
441
- }
442
- }
443
- return 0;
444
- }
445
- catch (err) {
446
- return reportError({ action: 'delete the space', err });
447
- }
448
- }
449
- /**
450
- * Removes a space's local registry entry only; the server-side space is
451
- * untouched. The remote counterpart is `delete`.
452
- *
453
- * @param options {object}
454
- * @param options.address {string} The space address.
455
- * @returns {Promise<number>} The process exit code.
456
- */
457
- export async function runSpaceForget(options) {
458
- try {
459
- const parsed = parseSpaceAddress(options.address);
460
- const entry = await resolveSpaceRef({ ref: parsed.spaceRef });
461
- if (!entry) {
462
- throw new Error(`No locally registered space found for "${parsed.spaceRef}".`);
463
- }
464
- const removed = await removeSpaceRecord({ storageId: entry.storageId });
465
- for (const filePath of removed) {
466
- console.error(`Removed ${filePath}`);
467
- }
468
- return 0;
469
- }
470
- catch (err) {
471
- return reportError({ action: 'forget the space', err });
472
- }
473
- }
474
- /**
475
- * Registers an existing remote space (e.g. created elsewhere or received via
476
- * delegation) in the local registry, verifying it first with `describe()`.
477
- *
478
- * @param options {object}
479
- * @param options.address {string} A full space https URL or a bare space id.
480
- * @param [options.server] {string} The server base URL (with a bare id).
481
- * @param [options.did] {string} The signing DID or stored-DID handle.
482
- * @param [options.handle] {string} Short tag for the registry entry.
483
- * @param [options.description] {string} Longer registry entry description.
484
- * @returns {Promise<number>} The process exit code.
485
- */
486
- export async function runSpaceAdd(options) {
487
- try {
488
- parseSpaceAddress(options.address);
489
- const target = await resolveWasTarget({
490
- address: options.address,
491
- server: options.server,
492
- did: options.did
493
- });
494
- const description = await target.client.space(target.spaceId).describe();
495
- if (description === null) {
496
- console.error('Not found (or not visible to you): ' +
497
- wasUrl({ server: target.server, spaceId: target.spaceId }));
498
- return 1;
499
- }
500
- const filePath = await saveSpaceRecord({
501
- record: {
502
- id: target.spaceId,
503
- ...(description.name && { name: description.name }),
504
- server: target.server,
505
- controller: description.controller ?? target.did
506
- },
507
- handle: options.handle,
508
- description: options.description
509
- });
510
- console.error(`Space registered in ${filePath}`);
511
- console.log(JSON.stringify(description, null, 2));
512
- return 0;
513
- }
514
- catch (err) {
515
- return reportError({ action: 'add the space', err });
516
- }
517
- }
518
- /**
519
- * Creates a collection within a space and prints `{ id, url, name? }`.
520
- *
521
- * @param options {object}
522
- * @param options.address {string} The space address.
523
- * @param [options.name] {string} The collection's display name.
524
- * @param [options.id] {string} A caller-chosen collection id.
525
- * @param [options.server] {string} The server base URL.
526
- * @param [options.did] {string} The signing DID or stored-DID handle.
527
- * @returns {Promise<number>} The process exit code.
528
- */
529
- export async function runCollectionCreate(options) {
530
- try {
531
- parseSpaceAddress(options.address);
532
- const target = await resolveWasTarget({
533
- address: options.address,
534
- server: options.server,
535
- did: options.did
536
- });
537
- const collection = await target.client
538
- .space(target.spaceId)
539
- .createCollection({
540
- ...(options.name !== undefined && { name: options.name }),
541
- ...(options.id !== undefined && { id: options.id })
542
- });
543
- const url = wasUrl({
544
- server: target.server,
545
- spaceId: target.spaceId,
546
- collectionId: collection.id
547
- });
548
- console.log(JSON.stringify({
549
- id: collection.id,
550
- url,
551
- ...(options.name !== undefined && { name: options.name })
552
- }, null, 2));
553
- return 0;
554
- }
555
- catch (err) {
556
- return reportError({ action: 'create the collection', err });
557
- }
558
- }
559
- /**
560
- * Lists the collections in a space.
561
- *
562
- * @param options {object}
563
- * @param options.address {string} The space address.
564
- * @param [options.json] {boolean} Output the raw listing JSON.
565
- * @param [options.plain] {boolean} Output one collection id per line.
566
- * @param [options.server] {string} The server base URL.
567
- * @param [options.did] {string} The signing DID or stored-DID handle.
568
- * @returns {Promise<number>} The process exit code.
569
- */
570
- export async function runCollectionList(options) {
571
- try {
572
- parseSpaceAddress(options.address);
573
- const target = await resolveWasTarget({
574
- address: options.address,
575
- server: options.server,
576
- did: options.did
577
- });
578
- const listing = await target.client.space(target.spaceId).collections();
579
- if (listing === null) {
580
- console.error('Not found (or not visible to you): ' +
581
- wasUrl({ server: target.server, spaceId: target.spaceId }));
582
- return 1;
583
- }
584
- printCollectionListing({
585
- listing,
586
- json: options.json,
587
- plain: options.plain
588
- });
589
- return 0;
590
- }
591
- catch (err) {
592
- return reportError({ action: 'list collections', err });
593
- }
594
- }
595
- /**
596
- * Renders a listing as a table, raw JSON, or one item id per line -- the
597
- * shared output logic of `collection list`, `resource list`, and `ls`.
598
- *
599
- * @param options {object}
600
- * @param options.listing {{items: T[]}}
601
- * @param [options.json] {boolean}
602
- * @param [options.plain] {boolean}
603
- * @param options.columns {Column[]}
604
- * @param options.toRow {(item: T) => string[]}
605
- * @returns {void}
606
- */
607
- function printListing({ listing, json, plain, columns, toRow }) {
608
- if (json) {
609
- console.log(JSON.stringify(listing, null, 2));
610
- return;
611
- }
612
- if (plain) {
613
- const ids = listing.items.map(item => item.id).sort();
614
- for (const id of ids) {
615
- console.log(id);
616
- }
617
- return;
618
- }
619
- if (listing.items.length === 0) {
620
- return;
621
- }
622
- console.log(renderTable({ columns, rows: listing.items.map(toRow) }));
623
- }
624
- /**
625
- * Renders a collection listing as a table, raw JSON, or one id per line.
626
- *
627
- * @param options {object}
628
- * @param options.listing {CollectionListing}
629
- * @param [options.json] {boolean}
630
- * @param [options.plain] {boolean}
631
- * @returns {void}
632
- */
633
- function printCollectionListing({ listing, json, plain }) {
634
- printListing({
635
- listing,
636
- json,
637
- plain,
638
- columns: [
639
- { header: 'ID', maxWidth: 30 },
640
- { header: 'NAME', maxWidth: 20 },
641
- { header: 'URL' }
642
- ],
643
- toRow: item => [item.id, item.name, item.url]
644
- });
645
- }
646
- /**
647
- * Shows a collection's description from the server.
648
- *
649
- * @param options {object}
650
- * @param options.address {string} The collection address.
651
- * @param [options.server] {string} The server base URL.
652
- * @param [options.did] {string} The signing DID or stored-DID handle.
653
- * @returns {Promise<number>} The process exit code.
654
- */
655
- export async function runCollectionShow(options) {
656
- try {
657
- const target = requireCollectionTarget({
658
- target: await resolveWasTarget({
659
- address: options.address,
660
- server: options.server,
661
- did: options.did
662
- }),
663
- address: options.address
664
- });
665
- const description = await target.client
666
- .space(target.spaceId)
667
- .collection(target.collectionId)
668
- .describe();
669
- if (description === null) {
670
- console.error('Not found (or not visible to you): ' +
671
- wasUrl({
672
- server: target.server,
673
- spaceId: target.spaceId,
674
- collectionId: target.collectionId
675
- }));
676
- return 1;
677
- }
678
- console.log(JSON.stringify(description, null, 2));
679
- return 0;
680
- }
681
- catch (err) {
682
- return reportError({ action: 'show the collection', err });
683
- }
684
- }
685
- /**
686
- * Updates a collection's description fields on the server (upsert via
687
- * `configure()`).
688
- *
689
- * @param options {object}
690
- * @param options.address {string} The collection address.
691
- * @param options.name {string} The collection's new display name.
692
- * @param [options.server] {string} The server base URL.
693
- * @param [options.did] {string} The signing DID or stored-DID handle.
694
- * @returns {Promise<number>} The process exit code.
695
- */
696
- export async function runCollectionUpdate(options) {
697
- try {
698
- const target = requireCollectionTarget({
699
- target: await resolveWasTarget({
700
- address: options.address,
701
- server: options.server,
702
- did: options.did
703
- }),
704
- address: options.address
705
- });
706
- const description = await target.client
707
- .space(target.spaceId)
708
- .collection(target.collectionId)
709
- .configure({ name: options.name });
710
- console.log(JSON.stringify(description, null, 2));
711
- return 0;
712
- }
713
- catch (err) {
714
- return reportError({ action: 'update the collection', err });
715
- }
716
- }
717
- /**
718
- * Deletes a whole collection and its contents on the server. Idempotent.
719
- *
720
- * @param options {object}
721
- * @param options.address {string} The collection address.
722
- * @param [options.server] {string} The server base URL.
723
- * @param [options.did] {string} The signing DID or stored-DID handle.
724
- * @returns {Promise<number>} The process exit code.
725
- */
726
- export async function runCollectionDelete(options) {
727
- try {
728
- const target = requireCollectionTarget({
729
- target: await resolveWasTarget({
730
- address: options.address,
731
- server: options.server,
732
- did: options.did
733
- }),
734
- address: options.address
735
- });
736
- await target.client
737
- .space(target.spaceId)
738
- .collection(target.collectionId)
739
- .delete();
740
- console.error('Deleted ' +
741
- wasUrl({
742
- server: target.server,
743
- spaceId: target.spaceId,
744
- collectionId: target.collectionId
745
- }) +
746
- ' on the server.');
747
- return 0;
748
- }
749
- catch (err) {
750
- return reportError({ action: 'delete the collection', err });
751
- }
752
- }
753
- /**
754
- * Adds a resource to a collection (server-generated id) and prints the
755
- * `{ id, url, contentType }` add result. The collection comes from a path
756
- * or a `--capability` targeting one.
757
- *
758
- * @param options {object}
759
- * @param [options.address] {string} The collection address.
760
- * @param [options.capability] {string} A capability reference instead of
761
- * a path.
762
- * @param [options.file] {string} The payload file; stdin when omitted.
763
- * @param [options.contentType] {string} Explicit payload content type.
764
- * @param [options.server] {string} The server base URL.
765
- * @param [options.did] {string} The signing DID or stored-DID handle.
766
- * @returns {Promise<number>} The process exit code.
767
- */
768
- export async function runResourceAdd(options) {
769
- try {
770
- assertOneAddressing(options);
771
- let collection;
772
- if (options.capability) {
773
- const resolved = await resolveCapabilityTarget({
774
- ref: options.capability,
775
- did: options.did
776
- });
777
- if (resolved.depth !== 'collection') {
778
- throw new Error(`The capability targets a ${resolved.depth}; ` +
779
- 'add needs a collection capability.');
780
- }
781
- collection = resolved.handle;
782
- }
783
- else {
784
- const target = requireCollectionTarget({
785
- target: await resolveWasTarget({
786
- address: options.address,
787
- server: options.server,
788
- did: options.did
789
- }),
790
- address: options.address
791
- });
792
- collection = target.client
793
- .space(target.spaceId)
794
- .collection(target.collectionId);
795
- }
796
- const payload = await readPayload({
797
- file: options.file,
798
- contentType: options.contentType
799
- });
800
- const result = await collection.add(payload.data, {
801
- ...(payload.contentType !== undefined && {
802
- contentType: payload.contentType
803
- })
804
- });
805
- console.log(JSON.stringify(result, null, 2));
806
- return 0;
807
- }
808
- catch (err) {
809
- return reportError({ action: 'add the resource', err });
810
- }
811
- }
812
- /**
813
- * Creates or replaces a resource at a known id (upsert) and prints
814
- * `{ id, url }`. The resource comes from a path or a `--capability`
815
- * targeting one.
816
- *
817
- * @param options {object}
818
- * @param [options.address] {string} The resource address.
819
- * @param [options.capability] {string} A capability reference instead of
820
- * a path.
821
- * @param [options.file] {string} The payload file; stdin when omitted.
822
- * @param [options.contentType] {string} Explicit payload content type.
823
- * @param [options.server] {string} The server base URL.
824
- * @param [options.did] {string} The signing DID or stored-DID handle.
825
- * @returns {Promise<number>} The process exit code.
826
- */
827
- export async function runResourcePut(options) {
828
- try {
829
- assertOneAddressing(options);
830
- let resource;
831
- let url;
832
- if (options.capability) {
833
- const resolved = await resolveCapabilityTarget({
834
- ref: options.capability,
835
- did: options.did
836
- });
837
- if (resolved.depth !== 'resource') {
838
- throw new Error(`The capability targets a ${resolved.depth}; ` +
839
- 'put needs a resource capability.');
840
- }
841
- resource = resolved.handle;
842
- url = resolved.url;
843
- }
844
- else {
845
- const target = requireResourceTarget({
846
- target: await resolveWasTarget({
847
- address: options.address,
848
- server: options.server,
849
- did: options.did
850
- }),
851
- address: options.address
852
- });
853
- resource = target.client
854
- .space(target.spaceId)
855
- .collection(target.collectionId)
856
- .resource(target.resourceId);
857
- url = wasUrl({
858
- server: target.server,
859
- spaceId: target.spaceId,
860
- collectionId: target.collectionId,
861
- resourceId: target.resourceId
862
- });
863
- }
864
- const payload = await readPayload({
865
- file: options.file,
866
- contentType: options.contentType
867
- });
868
- await resource.put(payload.data, {
869
- ...(payload.contentType !== undefined && {
870
- contentType: payload.contentType
871
- })
872
- });
873
- console.log(JSON.stringify({ id: resource.id, url }, null, 2));
874
- return 0;
875
- }
876
- catch (err) {
877
- return reportError({ action: 'put the resource', err });
878
- }
879
- }
880
- /**
881
- * Reads a resource: JSON pretty-printed to stdout, binary written raw
882
- * (`--output` for files). The resource comes from a path or a
883
- * `--capability` targeting one.
884
- *
885
- * @param options {object}
886
- * @param [options.address] {string} The resource address.
887
- * @param [options.capability] {string} A capability reference instead of
888
- * a path.
889
- * @param [options.output] {string} The output file path; stdout when
890
- * omitted.
891
- * @param [options.server] {string} The server base URL.
892
- * @param [options.did] {string} The signing DID or stored-DID handle.
893
- * @returns {Promise<number>} The process exit code.
894
- */
895
- export async function runResourceGet(options) {
896
- try {
897
- assertOneAddressing(options);
898
- let resource;
899
- let url;
900
- if (options.capability) {
901
- const resolved = await resolveCapabilityTarget({
902
- ref: options.capability,
903
- did: options.did
904
- });
905
- if (resolved.depth !== 'resource') {
906
- throw new Error(`The capability targets a ${resolved.depth}; ` +
907
- 'get needs a resource capability.');
908
- }
909
- resource = resolved.handle;
910
- url = resolved.url;
911
- }
912
- else {
913
- const target = requireResourceTarget({
914
- target: await resolveWasTarget({
915
- address: options.address,
916
- server: options.server,
917
- did: options.did
918
- }),
919
- address: options.address
920
- });
921
- resource = target.client
922
- .space(target.spaceId)
923
- .collection(target.collectionId)
924
- .resource(target.resourceId);
925
- url = wasUrl({
926
- server: target.server,
927
- spaceId: target.spaceId,
928
- collectionId: target.collectionId,
929
- resourceId: target.resourceId
930
- });
931
- }
932
- const data = await resource.get();
933
- if (data === null) {
934
- console.error(`Not found (or not visible to you): ${url}`);
935
- return 1;
936
- }
937
- await writeResourceOutput({ data, output: options.output });
938
- return 0;
939
- }
940
- catch (err) {
941
- return reportError({ action: 'get the resource', err });
942
- }
943
- }
944
- /**
945
- * Lists the resources in a collection.
946
- *
947
- * @param options {object}
948
- * @param options.address {string} The collection address.
949
- * @param [options.json] {boolean} Output the raw listing JSON.
950
- * @param [options.plain] {boolean} Output one resource id per line.
951
- * @param [options.server] {string} The server base URL.
952
- * @param [options.did] {string} The signing DID or stored-DID handle.
953
- * @returns {Promise<number>} The process exit code.
954
- */
955
- export async function runResourceList(options) {
956
- try {
957
- const target = requireCollectionTarget({
958
- target: await resolveWasTarget({
959
- address: options.address,
960
- server: options.server,
961
- did: options.did
962
- }),
963
- address: options.address
964
- });
965
- const listing = await target.client
966
- .space(target.spaceId)
967
- .collection(target.collectionId)
968
- .list();
969
- if (listing === null) {
970
- console.error('Not found (or not visible to you): ' +
971
- wasUrl({
972
- server: target.server,
973
- spaceId: target.spaceId,
974
- collectionId: target.collectionId
975
- }));
976
- return 1;
977
- }
978
- printResourceListing({ listing, json: options.json, plain: options.plain });
979
- return 0;
980
- }
981
- catch (err) {
982
- return reportError({ action: 'list resources', err });
983
- }
984
- }
985
- /**
986
- * Renders a resource listing as a table, raw JSON, or one id per line.
987
- *
988
- * @param options {object}
989
- * @param options.listing {ResourceListing}
990
- * @param [options.json] {boolean}
991
- * @param [options.plain] {boolean}
992
- * @returns {void}
993
- */
994
- function printResourceListing({ listing, json, plain }) {
995
- printListing({
996
- listing,
997
- json,
998
- plain,
999
- columns: [
1000
- { header: 'ID', maxWidth: 30 },
1001
- { header: 'CONTENT TYPE', maxWidth: 24 },
1002
- { header: 'URL' }
1003
- ],
1004
- toRow: item => [item.id, item.contentType, item.url]
1005
- });
1006
- }
1007
- /**
1008
- * Deletes a resource on the server. Idempotent.
1009
- *
1010
- * @param options {object}
1011
- * @param options.address {string} The resource address.
1012
- * @param [options.server] {string} The server base URL.
1013
- * @param [options.did] {string} The signing DID or stored-DID handle.
1014
- * @returns {Promise<number>} The process exit code.
1015
- */
1016
- export async function runResourceDelete(options) {
1017
- try {
1018
- const target = requireResourceTarget({
1019
- target: await resolveWasTarget({
1020
- address: options.address,
1021
- server: options.server,
1022
- did: options.did
1023
- }),
1024
- address: options.address
1025
- });
1026
- await target.client
1027
- .space(target.spaceId)
1028
- .collection(target.collectionId)
1029
- .resource(target.resourceId)
1030
- .delete();
1031
- console.error('Deleted ' +
1032
- wasUrl({
1033
- server: target.server,
1034
- spaceId: target.spaceId,
1035
- collectionId: target.collectionId,
1036
- resourceId: target.resourceId
1037
- }) +
1038
- ' on the server.');
1039
- return 0;
1040
- }
1041
- catch (err) {
1042
- return reportError({ action: 'delete the resource', err });
1043
- }
1044
- }
1045
- /**
1046
- * The `was ls` shorthand: lists the collections of a space or the resources
1047
- * of a collection, depending on the depth of the path (or of a
1048
- * `--capability`'s invocation target).
1049
- *
1050
- * @param options {object}
1051
- * @param [options.address] {string} A space or collection address.
1052
- * @param [options.capability] {string} A capability reference instead of
1053
- * a path.
1054
- * @param [options.json] {boolean} Output the raw listing JSON.
1055
- * @param [options.plain] {boolean} Output one id per line.
1056
- * @param [options.server] {string} The server base URL.
1057
- * @param [options.did] {string} The signing DID or stored-DID handle.
1058
- * @returns {Promise<number>} The process exit code.
1059
- */
1060
- export async function runLs(options) {
1061
- if (options.capability) {
1062
- try {
1063
- assertOneAddressing(options);
1064
- const resolved = await resolveCapabilityTarget({
1065
- ref: options.capability,
1066
- did: options.did
1067
- });
1068
- if (resolved.depth === 'resource') {
1069
- throw new Error('the capability targets a resource; ' +
1070
- 'ls takes a space or collection capability.');
1071
- }
1072
- if (resolved.depth === 'space') {
1073
- const listing = await resolved.handle.collections();
1074
- if (listing === null) {
1075
- console.error(`Not found (or not visible to you): ${resolved.url}`);
1076
- return 1;
1077
- }
1078
- printCollectionListing({
1079
- listing,
1080
- json: options.json,
1081
- plain: options.plain
1082
- });
1083
- }
1084
- else {
1085
- const listing = await resolved.handle.list();
1086
- if (listing === null) {
1087
- console.error(`Not found (or not visible to you): ${resolved.url}`);
1088
- return 1;
1089
- }
1090
- printResourceListing({
1091
- listing,
1092
- json: options.json,
1093
- plain: options.plain
1094
- });
1095
- }
1096
- return 0;
1097
- }
1098
- catch (err) {
1099
- return reportError({ action: 'list the path', err });
1100
- }
1101
- }
1102
- let parsed;
1103
- try {
1104
- assertOneAddressing(options);
1105
- parsed = parseWasAddress(options.address);
1106
- }
1107
- catch (err) {
1108
- return reportError({ action: 'list the path', err });
1109
- }
1110
- if (parsed.resourceId !== undefined) {
1111
- console.error(`Could not list the path: "${options.address}" is a resource; ` +
1112
- 'ls takes a space or collection address.');
1113
- return 2;
1114
- }
1115
- if (parsed.collectionId !== undefined) {
1116
- return runResourceList({ ...options, address: options.address });
1117
- }
1118
- return runCollectionList({ ...options, address: options.address });
1119
- }
1120
- /**
1121
- * The `was rm` shorthand: deletes whatever the path (or a `--capability`'s
1122
- * invocation target) points at -- a space, a collection, or a resource (the
1123
- * client's uniform `delete()` design).
1124
- *
1125
- * @param options {object}
1126
- * @param [options.address] {string} A space, collection, or resource
1127
- * address.
1128
- * @param [options.capability] {string} A capability reference instead of
1129
- * a path.
1130
- * @param [options.server] {string} The server base URL.
1131
- * @param [options.did] {string} The signing DID or stored-DID handle.
1132
- * @returns {Promise<number>} The process exit code.
1133
- */
1134
- export async function runRm(options) {
1135
- if (options.capability) {
1136
- try {
1137
- assertOneAddressing(options);
1138
- const resolved = await resolveCapabilityTarget({
1139
- ref: options.capability,
1140
- did: options.did
1141
- });
1142
- await resolved.handle.delete();
1143
- console.error(`Deleted ${resolved.url} on the server.`);
1144
- return 0;
1145
- }
1146
- catch (err) {
1147
- return reportError({ action: 'delete the path', err });
1148
- }
1149
- }
1150
- let parsed;
1151
- try {
1152
- assertOneAddressing(options);
1153
- parsed = parseWasAddress(options.address);
1154
- }
1155
- catch (err) {
1156
- return reportError({ action: 'delete the path', err });
1157
- }
1158
- const pathOptions = { ...options, address: options.address };
1159
- if (parsed.resourceId !== undefined) {
1160
- return runResourceDelete(pathOptions);
1161
- }
1162
- if (parsed.collectionId !== undefined) {
1163
- return runCollectionDelete(pathOptions);
1164
- }
1165
- return runSpaceDelete(pathOptions);
1166
- }
1167
- /**
1168
- * Builds the access handle (space, collection, or resource) a resolved
1169
- * target addresses, together with its URL. Policies and publishing work at
1170
- * every depth, so these commands dispatch through this helper.
1171
- *
1172
- * @param target {ResolvedWasTarget}
1173
- * @returns {{handle: Space | Collection | Resource, url: string}}
1174
- */
1175
- function handleForTarget(target) {
1176
- const url = wasUrl({
1177
- server: target.server,
1178
- spaceId: target.spaceId,
1179
- collectionId: target.collectionId,
1180
- resourceId: target.resourceId
1181
- });
1182
- const space = target.client.space(target.spaceId);
1183
- if (target.collectionId === undefined) {
1184
- return { handle: space, url };
1185
- }
1186
- const collection = space.collection(target.collectionId);
1187
- if (target.resourceId === undefined) {
1188
- return { handle: collection, url };
1189
- }
1190
- return { handle: collection.resource(target.resourceId), url };
1191
- }
1192
- /**
1193
- * Shows the access-control policy of a space, collection, or resource.
1194
- *
1195
- * @param options {object}
1196
- * @param options.address {string} A space/collection/resource address.
1197
- * @param [options.server] {string} The server base URL.
1198
- * @param [options.did] {string} The signing DID or stored-DID handle.
1199
- * @returns {Promise<number>} The process exit code.
1200
- */
1201
- export async function runPolicyShow(options) {
1202
- try {
1203
- const target = await resolveWasTarget({
1204
- address: options.address,
1205
- server: options.server,
1206
- did: options.did
1207
- });
1208
- const { handle, url } = handleForTarget(target);
1209
- const policy = await handle.getPolicy();
1210
- if (policy === null) {
1211
- console.error(`No policy set (or not visible to you): ${url}`);
1212
- return 1;
1213
- }
1214
- console.log(JSON.stringify(policy, null, 2));
1215
- return 0;
1216
- }
1217
- catch (err) {
1218
- return reportError({ action: 'show the policy', err });
1219
- }
1220
- }
1221
- /**
1222
- * Parses the `policy set` arguments -- `--type <type>` for a simple
1223
- * type-only policy, or a JSON file for richer ones -- into a policy
1224
- * document.
1225
- *
1226
- * @param options {object}
1227
- * @param [options.type] {string}
1228
- * @param [options.file] {string}
1229
- * @returns {Promise<PolicyDocument>}
1230
- */
1231
- async function resolvePolicyInput({ type, file }) {
1232
- if (type && file) {
1233
- throw new Error('Provide either --type or a policy file, not both.');
1234
- }
1235
- if (type) {
1236
- return { type };
1237
- }
1238
- if (!file) {
1239
- throw new Error('Provide --type <type> or a policy JSON file.');
1240
- }
1241
- let parsed;
1242
- try {
1243
- parsed = JSON.parse(await readFile(file, 'utf8'));
1244
- }
1245
- catch (err) {
1246
- throw new Error(`${file} does not contain policy JSON: ` +
1247
- `${err instanceof Error ? err.message : String(err)}`, { cause: err });
1248
- }
1249
- if (parsed === null ||
1250
- typeof parsed !== 'object' ||
1251
- Array.isArray(parsed) ||
1252
- typeof parsed.type !== 'string') {
1253
- throw new Error(`${file} must hold a policy object with a "type" field.`);
1254
- }
1255
- return parsed;
1256
- }
1257
- /**
1258
- * Sets (creates or replaces) the access-control policy of a space,
1259
- * collection, or resource, and prints the policy document that was set.
1260
27
  *
1261
- * @param options {object}
1262
- * @param options.address {string} A space/collection/resource address.
1263
- * @param [options.file] {string} A policy JSON file.
1264
- * @param [options.type] {string} A simple type-only policy (e.g.
1265
- * PublicCanRead).
1266
- * @param [options.server] {string} The server base URL.
1267
- * @param [options.did] {string} The signing DID or stored-DID handle.
1268
- * @returns {Promise<number>} The process exit code.
1269
- */
1270
- export async function runPolicySet(options) {
1271
- try {
1272
- const policy = await resolvePolicyInput({
1273
- type: options.type,
1274
- file: options.file
1275
- });
1276
- const target = await resolveWasTarget({
1277
- address: options.address,
1278
- server: options.server,
1279
- did: options.did
1280
- });
1281
- const { handle, url } = handleForTarget(target);
1282
- await handle.setPolicy(policy);
1283
- console.error(`Policy set on ${url}`);
1284
- console.log(JSON.stringify(policy, null, 2));
1285
- return 0;
1286
- }
1287
- catch (err) {
1288
- return reportError({ action: 'set the policy', err });
1289
- }
1290
- }
1291
- /**
1292
- * Removes the access-control policy of a space, collection, or resource,
1293
- * reverting it to capability-only access. Idempotent.
1294
- *
1295
- * @param options {object}
1296
- * @param options.address {string} A space/collection/resource address.
1297
- * @param [options.server] {string} The server base URL.
1298
- * @param [options.did] {string} The signing DID or stored-DID handle.
1299
- * @returns {Promise<number>} The process exit code.
1300
- */
1301
- export async function runPolicyClear(options) {
1302
- try {
1303
- const target = await resolveWasTarget({
1304
- address: options.address,
1305
- server: options.server,
1306
- did: options.did
1307
- });
1308
- const { handle, url } = handleForTarget(target);
1309
- await handle.clearPolicy();
1310
- console.error(`Cleared the policy on ${url} (capability-only access).`);
1311
- return 0;
1312
- }
1313
- catch (err) {
1314
- return reportError({ action: 'clear the policy', err });
1315
- }
1316
- }
1317
- /**
1318
- * Makes a space, collection, or resource world-readable (the
1319
- * `PublicCanRead` policy) and prints its public URL -- the "share via
1320
- * public link" case.
1321
- *
1322
- * @param options {object}
1323
- * @param options.address {string} A space/collection/resource address.
1324
- * @param [options.server] {string} The server base URL.
1325
- * @param [options.did] {string} The signing DID or stored-DID handle.
1326
- * @returns {Promise<number>} The process exit code.
1327
- */
1328
- export async function runPublish(options) {
1329
- try {
1330
- const target = await resolveWasTarget({
1331
- address: options.address,
1332
- server: options.server,
1333
- did: options.did
1334
- });
1335
- const { handle, url } = handleForTarget(target);
1336
- await handle.setPublic();
1337
- console.error(`Published (world-readable): ${url}`);
1338
- console.log(url);
1339
- return 0;
1340
- }
1341
- catch (err) {
1342
- return reportError({ action: 'publish the path', err });
1343
- }
1344
- }
1345
- /**
1346
- * Reverts a published space, collection, or resource to capability-only
1347
- * access (clears its policy). Idempotent.
1348
- *
1349
- * @param options {object}
1350
- * @param options.address {string} A space/collection/resource address.
1351
- * @param [options.server] {string} The server base URL.
1352
- * @param [options.did] {string} The signing DID or stored-DID handle.
1353
- * @returns {Promise<number>} The process exit code.
1354
- */
1355
- export async function runUnpublish(options) {
1356
- try {
1357
- const target = await resolveWasTarget({
1358
- address: options.address,
1359
- server: options.server,
1360
- did: options.did
1361
- });
1362
- const { handle, url } = handleForTarget(target);
1363
- await handle.clearPolicy();
1364
- console.error(`Unpublished (capability-only access): ${url}`);
1365
- return 0;
1366
- }
1367
- catch (err) {
1368
- return reportError({ action: 'unpublish the path', err });
1369
- }
1370
- }
1371
- /**
1372
- * Exports a whole space as a tar archive, written to `--output` or raw to
1373
- * stdout.
1374
- *
1375
- * @param options {object}
1376
- * @param options.address {string} The space address.
1377
- * @param [options.output] {string} The output file path; stdout when
1378
- * omitted.
1379
- * @param [options.server] {string} The server base URL.
1380
- * @param [options.did] {string} The signing DID or stored-DID handle.
1381
- * @returns {Promise<number>} The process exit code.
1382
- */
1383
- export async function runSpaceExport(options) {
1384
- try {
1385
- parseSpaceAddress(options.address);
1386
- const target = await resolveWasTarget({
1387
- address: options.address,
1388
- server: options.server,
1389
- did: options.did
1390
- });
1391
- const bytes = await target.client.space(target.spaceId).export();
1392
- await writeBytesOutput({ bytes, output: options.output });
1393
- return 0;
1394
- }
1395
- catch (err) {
1396
- return reportError({ action: 'export the space', err });
1397
- }
1398
- }
1399
- /**
1400
- * Imports (merges) a tar archive into a space and prints the import stats
1401
- * summary.
1402
- *
1403
- * @param options {object}
1404
- * @param options.address {string} The space address.
1405
- * @param [options.file] {string} The tar file; stdin when omitted.
1406
- * @param [options.server] {string} The server base URL.
1407
- * @param [options.did] {string} The signing DID or stored-DID handle.
1408
- * @returns {Promise<number>} The process exit code.
1409
- */
1410
- export async function runSpaceImport(options) {
1411
- try {
1412
- parseSpaceAddress(options.address);
1413
- const target = await resolveWasTarget({
1414
- address: options.address,
1415
- server: options.server,
1416
- did: options.did
1417
- });
1418
- const tar = await readInputBytes({ file: options.file });
1419
- const stats = await target.client.space(target.spaceId).import(tar);
1420
- console.log(JSON.stringify(stats, null, 2));
1421
- return 0;
1422
- }
1423
- catch (err) {
1424
- return reportError({ action: 'import into the space', err });
1425
- }
1426
- }
1427
- /**
1428
- * Delegates access to a space, collection, or resource: signs a capability
1429
- * for the `--to` DID with the given actions and expiration, and prints
1430
- * `{ delegatedCapability, encoded }` (the same shape as `zcap delegate`).
1431
- * `--save` stores the capability in the local zcap store
1432
- * (`~/.config/did-cli-wallet/zcaps/`).
1433
- *
1434
- * @param options {object}
1435
- * @param options.address {string} The space/collection/resource address.
1436
- * @param options.to {string} The delegatee DID (or stored-DID handle).
1437
- * @param options.action {string[]} Allowed actions (HTTP verbs; lowercase
1438
- * accepted).
1439
- * @param [options.ttl] {string} Time-to-live for expiration (default 1y).
1440
- * @param [options.expires] {string} Explicit ISO 8601 expiration
1441
- * (overrides --ttl).
1442
- * @param [options.save] {boolean} Save the capability to the zcap store.
1443
- * @param [options.handle] {string} Short tag for the saved zcap.
1444
- * @param [options.description] {string} Longer description for the saved
1445
- * zcap.
1446
- * @param [options.server] {string} The server base URL.
1447
- * @param [options.did] {string} The signing DID or stored-DID handle.
1448
- * @returns {Promise<number>} The process exit code.
1449
- */
1450
- export async function runGrant(options) {
1451
- try {
1452
- const actions = normalizeActions(options.action);
1453
- const delegatee = await resolveDidRef({ ref: options.to });
1454
- if (!delegatee) {
1455
- throw new Error(`No locally stored DID found for "${options.to}".`);
1456
- }
1457
- const expires = options.expires ?? expiresFromTtl(options.ttl ?? '1y').toISOString();
1458
- const target = await resolveWasTarget({
1459
- address: options.address,
1460
- server: options.server,
1461
- did: options.did
1462
- });
1463
- const url = wasUrl({
1464
- server: target.server,
1465
- spaceId: target.spaceId,
1466
- collectionId: target.collectionId,
1467
- resourceId: target.resourceId
1468
- });
1469
- const delegatedCapability = await target.client.grant({
1470
- to: delegatee,
1471
- actions,
1472
- expires,
1473
- target: url
1474
- });
1475
- const encoded = encodeCapability(delegatedCapability);
1476
- if (options.save) {
1477
- const storageId = storageIdFor(delegatedCapability.id);
1478
- const filePath = await saveToCollection('zcaps', storageId, delegatedCapability);
1479
- await writeCreateMeta({
1480
- storageId,
1481
- created: new Date().toISOString(),
1482
- handle: options.handle,
1483
- description: options.description
1484
- });
1485
- console.error(`Capability saved to ${filePath}`);
1486
- }
1487
- console.log(JSON.stringify({ delegatedCapability, encoded }, null, 2));
1488
- return 0;
1489
- }
1490
- catch (err) {
1491
- return reportError({ action: 'grant access', err });
1492
- }
1493
- }
28
+ * The run functions for each subcommand group live in `src/commands/was/`;
29
+ * this module wires them onto the commander command tree.
30
+ */
31
+ import { Command } from 'commander';
32
+ import { capabilityOption, contentTypeOption, didOption, runAndExit, serverOption } from './was/shared.js';
33
+ import { runSpaceAdd, runSpaceBackends, runSpaceCreate, runSpaceDelete, runSpaceExport, runSpaceForget, runSpaceImport, runSpaceList, runSpaceQuotas, runSpaceShow, runSpaceUpdate } from './was/space.js';
34
+ import { runCollectionBackend, runCollectionCreate, runCollectionDelete, runCollectionList, runCollectionQuota, runCollectionShow, runCollectionUpdate } from './was/collection.js';
35
+ import { runResourceAdd, runResourceDelete, runResourceGet, runResourceList, runResourceMetaGet, runResourceMetaPut, runResourcePut } from './was/resource.js';
36
+ import { runLs, runRm } from './was/tree.js';
37
+ import { runPolicyClear, runPolicySet, runPolicyShow } from './was/policy.js';
38
+ import { runGrant, runPublish, runUnpublish } from './was/publish.js';
1494
39
  export function makeWasCommand() {
1495
40
  const was = new Command('was').description('Wallet Attached Storage (WAS) operations');
1496
41
  const space = new Command('space').description('Manage WAS spaces');
@@ -1591,6 +136,24 @@ export function makeWasCommand() {
1591
136
  .action(async (address, file, options) => {
1592
137
  await runAndExit(runSpaceImport({ address, file, ...options }));
1593
138
  });
139
+ space
140
+ .command('backends <space>')
141
+ .description('List the storage backends available within a space')
142
+ .option('--json', 'output the raw backends JSON')
143
+ .addOption(serverOption())
144
+ .addOption(didOption())
145
+ .action(async (address, options) => {
146
+ await runAndExit(runSpaceBackends({ address, ...options }));
147
+ });
148
+ space
149
+ .command('quotas <space>')
150
+ .description("Show a space's storage quota report, grouped by backend")
151
+ .option('--json', 'output the raw quota report JSON')
152
+ .addOption(serverOption())
153
+ .addOption(didOption())
154
+ .action(async (address, options) => {
155
+ await runAndExit(runSpaceQuotas({ address, ...options }));
156
+ });
1594
157
  was.addCommand(space);
1595
158
  const collection = new Command('collection')
1596
159
  .alias('coll')
@@ -1643,6 +206,25 @@ export function makeWasCommand() {
1643
206
  .action(async (address, options) => {
1644
207
  await runAndExit(runCollectionDelete({ address, ...options }));
1645
208
  });
209
+ collection
210
+ .command('backend <path>')
211
+ .description('Show the storage backend a collection is stored on (SPACE/COLLECTION)')
212
+ .option('--json', 'output the raw backend JSON')
213
+ .addOption(serverOption())
214
+ .addOption(didOption())
215
+ .action(async (address, options) => {
216
+ await runAndExit(runCollectionBackend({ address, ...options }));
217
+ });
218
+ collection
219
+ .command('quota <path>')
220
+ .description("Show a collection's storage usage, scoped to its backend " +
221
+ '(SPACE/COLLECTION)')
222
+ .option('--json', 'output the raw usage JSON')
223
+ .addOption(serverOption())
224
+ .addOption(didOption())
225
+ .action(async (address, options) => {
226
+ await runAndExit(runCollectionQuota({ address, ...options }));
227
+ });
1646
228
  was.addCommand(collection);
1647
229
  const resource = new Command('resource')
1648
230
  .alias('res')
@@ -1692,7 +274,7 @@ export function makeWasCommand() {
1692
274
  await runAndExit(runResourceGet({ address, ...options }));
1693
275
  });
1694
276
  resource
1695
- .command('list <path>')
277
+ .command('list <collection>')
1696
278
  .description('List the resources in a collection (SPACE/COLLECTION)')
1697
279
  .option('--json', 'output the raw listing JSON')
1698
280
  .option('--plain', 'output one resource id per line, sorted')
@@ -1711,6 +293,35 @@ export function makeWasCommand() {
1711
293
  await runAndExit(runResourceDelete({ address, ...options }));
1712
294
  });
1713
295
  was.addCommand(resource);
296
+ const resourceMeta = new Command('resource-meta')
297
+ .alias('meta')
298
+ .description("Read and update a resource's metadata (custom name and tags)");
299
+ resourceMeta
300
+ .command('get [path]')
301
+ .description("Show a resource's metadata: content type, size, timestamps, and " +
302
+ 'custom name/tags')
303
+ .addOption(capabilityOption())
304
+ .addOption(serverOption())
305
+ .addOption(didOption())
306
+ .action(async (address, options) => {
307
+ await runAndExit(runResourceMetaGet({ address, ...options }));
308
+ });
309
+ resourceMeta
310
+ .command('put [path]')
311
+ .description("Update a resource's custom metadata: --name and/or --tag key=value " +
312
+ '(both repeatable-friendly and non-destructive), or --json for a ' +
313
+ 'full replacement')
314
+ .option('--name <name>', "the resource's display name (preserves existing tags)")
315
+ .option('--tag <pair>', 'a custom tag as key=value; repeatable (preserves the existing name)', (value, previous) => previous.concat(value), [])
316
+ .option('--json <jsonOrFile>', 'full custom-metadata replacement as inline JSON or a JSON file path ' +
317
+ '(clears any omitted field)')
318
+ .addOption(capabilityOption())
319
+ .addOption(serverOption())
320
+ .addOption(didOption())
321
+ .action(async (address, options) => {
322
+ await runAndExit(runResourceMetaPut({ address, ...options }));
323
+ });
324
+ was.addCommand(resourceMeta);
1714
325
  was
1715
326
  .command('ls [path]')
1716
327
  .description('List the collections of a space, or the resources of a collection')