@interop/did-cli 0.6.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 (116) hide show
  1. package/CHANGELOG.md +341 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +1307 -0
  4. package/dist/commands/did.d.ts +3 -0
  5. package/dist/commands/did.d.ts.map +1 -0
  6. package/dist/commands/did.js +605 -0
  7. package/dist/commands/did.js.map +1 -0
  8. package/dist/commands/key.d.ts +3 -0
  9. package/dist/commands/key.d.ts.map +1 -0
  10. package/dist/commands/key.js +430 -0
  11. package/dist/commands/key.js.map +1 -0
  12. package/dist/commands/vc.d.ts +79 -0
  13. package/dist/commands/vc.d.ts.map +1 -0
  14. package/dist/commands/vc.js +528 -0
  15. package/dist/commands/vc.js.map +1 -0
  16. package/dist/commands/wallet.d.ts +14 -0
  17. package/dist/commands/wallet.d.ts.map +1 -0
  18. package/dist/commands/wallet.js +48 -0
  19. package/dist/commands/wallet.js.map +1 -0
  20. package/dist/commands/was.d.ts +500 -0
  21. package/dist/commands/was.d.ts.map +1 -0
  22. package/dist/commands/was.js +1833 -0
  23. package/dist/commands/was.js.map +1 -0
  24. package/dist/commands/zcap.d.ts +85 -0
  25. package/dist/commands/zcap.d.ts.map +1 -0
  26. package/dist/commands/zcap.js +447 -0
  27. package/dist/commands/zcap.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +20 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/keys/ecdsa.d.ts +40 -0
  33. package/dist/keys/ecdsa.d.ts.map +1 -0
  34. package/dist/keys/ecdsa.js +73 -0
  35. package/dist/keys/ecdsa.js.map +1 -0
  36. package/dist/meta.d.ts +164 -0
  37. package/dist/meta.d.ts.map +1 -0
  38. package/dist/meta.js +286 -0
  39. package/dist/meta.js.map +1 -0
  40. package/dist/storage.d.ts +191 -0
  41. package/dist/storage.d.ts.map +1 -0
  42. package/dist/storage.js +307 -0
  43. package/dist/storage.js.map +1 -0
  44. package/dist/table.d.ts +43 -0
  45. package/dist/table.d.ts.map +1 -0
  46. package/dist/table.js +61 -0
  47. package/dist/table.js.map +1 -0
  48. package/dist/vc/fixtures/welcomeCredential.d.ts +22 -0
  49. package/dist/vc/fixtures/welcomeCredential.d.ts.map +1 -0
  50. package/dist/vc/fixtures/welcomeCredential.js +25 -0
  51. package/dist/vc/fixtures/welcomeCredential.js.map +1 -0
  52. package/dist/vc/issue.d.ts +24 -0
  53. package/dist/vc/issue.d.ts.map +1 -0
  54. package/dist/vc/issue.js +211 -0
  55. package/dist/vc/issue.js.map +1 -0
  56. package/dist/vc/registries.d.ts +30 -0
  57. package/dist/vc/registries.d.ts.map +1 -0
  58. package/dist/vc/registries.js +53 -0
  59. package/dist/vc/registries.js.map +1 -0
  60. package/dist/vc/registryManager.d.ts +25 -0
  61. package/dist/vc/registryManager.d.ts.map +1 -0
  62. package/dist/vc/registryManager.js +29 -0
  63. package/dist/vc/registryManager.js.map +1 -0
  64. package/dist/vc/suites/expirationSuite.d.ts +23 -0
  65. package/dist/vc/suites/expirationSuite.d.ts.map +1 -0
  66. package/dist/vc/suites/expirationSuite.js +84 -0
  67. package/dist/vc/suites/expirationSuite.js.map +1 -0
  68. package/dist/vc/suites/issuerDetailsSuite.d.ts +22 -0
  69. package/dist/vc/suites/issuerDetailsSuite.d.ts.map +1 -0
  70. package/dist/vc/suites/issuerDetailsSuite.js +69 -0
  71. package/dist/vc/suites/issuerDetailsSuite.js.map +1 -0
  72. package/dist/vc/verify.d.ts +46 -0
  73. package/dist/vc/verify.d.ts.map +1 -0
  74. package/dist/vc/verify.js +147 -0
  75. package/dist/vc/verify.js.map +1 -0
  76. package/dist/was/address.d.ts +44 -0
  77. package/dist/was/address.d.ts.map +1 -0
  78. package/dist/was/address.js +98 -0
  79. package/dist/was/address.js.map +1 -0
  80. package/dist/was/capability.d.ts +65 -0
  81. package/dist/was/capability.d.ts.map +1 -0
  82. package/dist/was/capability.js +108 -0
  83. package/dist/was/capability.js.map +1 -0
  84. package/dist/was/client.d.ts +108 -0
  85. package/dist/was/client.d.ts.map +1 -0
  86. package/dist/was/client.js +142 -0
  87. package/dist/was/client.js.map +1 -0
  88. package/dist/was/io.d.ts +71 -0
  89. package/dist/was/io.d.ts.map +1 -0
  90. package/dist/was/io.js +146 -0
  91. package/dist/was/io.js.map +1 -0
  92. package/dist/was/registry.d.ts +79 -0
  93. package/dist/was/registry.d.ts.map +1 -0
  94. package/dist/was/registry.js +99 -0
  95. package/dist/was/registry.js.map +1 -0
  96. package/dist/zcap/create.d.ts +20 -0
  97. package/dist/zcap/create.d.ts.map +1 -0
  98. package/dist/zcap/create.js +29 -0
  99. package/dist/zcap/create.js.map +1 -0
  100. package/dist/zcap/delegate.d.ts +44 -0
  101. package/dist/zcap/delegate.d.ts.map +1 -0
  102. package/dist/zcap/delegate.js +77 -0
  103. package/dist/zcap/delegate.js.map +1 -0
  104. package/dist/zcap/encoding.d.ts +17 -0
  105. package/dist/zcap/encoding.d.ts.map +1 -0
  106. package/dist/zcap/encoding.js +37 -0
  107. package/dist/zcap/encoding.js.map +1 -0
  108. package/dist/zcap/signer.d.ts +20 -0
  109. package/dist/zcap/signer.d.ts.map +1 -0
  110. package/dist/zcap/signer.js +62 -0
  111. package/dist/zcap/signer.js.map +1 -0
  112. package/dist/zcap/ttl.d.ts +19 -0
  113. package/dist/zcap/ttl.d.ts.map +1 -0
  114. package/dist/zcap/ttl.js +40 -0
  115. package/dist/zcap/ttl.js.map +1 -0
  116. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,1307 @@
1
+ # DID CLI wallet _(@interop/did-cli)_
2
+
3
+ > A command line client for managing DIDs, VCs, zCaps, and corresponding cryptographic key pairs, written in Typescript.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Background](#background)
8
+ - [Install](#install)
9
+ - [Usage](#usage)
10
+ - [Contribute](#contribute)
11
+ - [License](#license)
12
+
13
+ ## Background
14
+
15
+ ## Install
16
+
17
+ ## Usage
18
+
19
+ Help is available with the `--help/-h` command line option:
20
+
21
+ ```
22
+ ./di -h
23
+ ./di COMMAND -h
24
+ ```
25
+
26
+ ### Key Management
27
+
28
+ #### Create a key pair
29
+
30
+ Generate a random Ed25519 key pair (ed25519 is the default type):
31
+
32
+ ```
33
+ ./di key create
34
+ ```
35
+
36
+ If you'd like to also generate a secret key seed (to help deterministically
37
+ generate the same key pair in the future), pass in the `--with-seed` flag:
38
+
39
+ ```
40
+ ./di key create --with-seed
41
+ {
42
+ "secretKeySeed": "z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv",
43
+ "keyPair": {
44
+ "@context": "https://w3id.org/security/multikey/v1",
45
+ "type": "Multikey",
46
+ "publicKeyMultibase": "z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
47
+ "secretKeyMultibase": "zruzykbtvWUgV8Tp1LKVEuTmywLEa75qHsvWRVarVhdgHiCgiMYTSDXTavJVh47Cwes4mKgdAY5PTizbRvHXcA7XcLF"
48
+ }
49
+ }
50
+ ```
51
+
52
+ Generate a deterministic key pair by setting the `SECRET_KEY_SEED` environment
53
+ variable to a multibase-encoded seed (e.g. from `@digitalcredentials/bnid`):
54
+
55
+ ```
56
+ SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv ./di key create
57
+ {
58
+ "@context": "https://w3id.org/security/multikey/v1",
59
+ "type": "Multikey",
60
+ "publicKeyMultibase": "z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
61
+ "secretKeyMultibase": "zruzykbtvWUgV8Tp1LKVEuTmywLEa75qHsvWRVarVhdgHiCgiMYTSDXTavJVh47Cwes4mKgdAY5PTizbRvHXcA7XcLF"
62
+ }
63
+ ```
64
+
65
+ Specify an explicit key type with `--type` (defaults to `ed25519`; supported:
66
+ `ed25519`, `ecdsa`):
67
+
68
+ ```
69
+ SECRET_KEY_SEED=z1Aaj5A4UCsd... ./di key create --type ed25519
70
+ ```
71
+
72
+ Output is a JSON-LD Multikey document with both the public and secret key in
73
+ multibase encoding:
74
+
75
+ ```json
76
+ {
77
+ "@context": "https://w3id.org/security/multikey/v1",
78
+ "type": "Multikey",
79
+ "publicKeyMultibase": "z6Mk...",
80
+ "secretKeyMultibase": "zrv..."
81
+ }
82
+ ```
83
+
84
+ Generate an ECDSA key with `--type ecdsa`. The curve is chosen with `--curve`
85
+ (defaults to `p256`; supported: `p256`, `p384`, `p521`, each also accepted in
86
+ hyphenated `p-256` and SECG `secp256r1` spellings, case-insensitively):
87
+
88
+ ```
89
+ ./di key create --type ecdsa --curve p384
90
+ ```
91
+
92
+ ECDSA keys are serialized as Multikey, the same as Ed25519. Note that ECDSA key
93
+ generation is non-deterministic (it cannot be derived from a seed), so
94
+ `--with-seed` and `SECRET_KEY_SEED` are not supported with `--type ecdsa`.
95
+
96
+ Save the key to local wallet storage (`~/.config/did-cli-wallet/keys/` by default, or
97
+ `$WALLET_DIR/keys/` if set) with `--save`. A `.meta.json` metadata sidecar is
98
+ written next to the key, recording the creation timestamp; `--handle` (a short
99
+ tag for telling keys apart) and `--description` add user-defined metadata to
100
+ it (both require `--save`):
101
+
102
+ ```
103
+ ./di key create --save --handle issuer-signing --description 'Demo issuer signing key'
104
+ Key saved to /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....json
105
+ ```
106
+
107
+ #### List key pairs
108
+
109
+ List the key pairs saved in local wallet storage (via `key create --save`) as
110
+ a table of their metadata. The DIDS column shows the locally stored DIDs whose
111
+ documents reference the key, derived by scanning the saved DID documents:
112
+
113
+ ```
114
+ ./di key list
115
+ HANDLE TYPE CREATED FINGERPRINT DIDS DESCRIPTION
116
+ -------------- ------- ---------- ---------------------------- ------------------- -----------------------
117
+ issuer-signing ed25519 2026-06-10 z6MkrLBubwzwEv...MnCsdX2hM did:key:z6MkrL... Demo issuer signing key
118
+ ```
119
+
120
+ If no keys are stored, nothing is printed. Pass `--json` to output the list as
121
+ a JSON array of objects with metadata:
122
+
123
+ ```
124
+ ./di key list --json
125
+ [
126
+ {
127
+ "fingerprint": "z6Mkr...",
128
+ "storageId": "2026-06-10-ed25519-z6Mkr...",
129
+ "type": "ed25519",
130
+ "created": "2026-06-10T17:22:31.123Z",
131
+ "handle": "issuer-signing",
132
+ "description": "Demo issuer signing key",
133
+ "dids": ["did:key:z6Mkr..."]
134
+ }
135
+ ]
136
+ ```
137
+
138
+ Or pass `--plain` to print just the fingerprints (multibase-encoded public
139
+ keys), one per line, sorted:
140
+
141
+ ```
142
+ ./di key list --plain
143
+ z6Mkr...
144
+ z6Mks...
145
+ ```
146
+
147
+ #### Show a key pair
148
+
149
+ Display a key saved in local wallet storage, looked up by its fingerprint
150
+ (`publicKeyMultibase`, as printed by `key list`) or by its metadata handle.
151
+ Only the public key object is shown -- the stored secret key is never included
152
+ in the output:
153
+
154
+ ```
155
+ ./di key show z6Mkr...
156
+ {
157
+ "@context": "https://w3id.org/security/multikey/v1",
158
+ "id": "...",
159
+ "type": "Multikey",
160
+ "controller": "...",
161
+ "publicKeyMultibase": "z6Mkr..."
162
+ }
163
+ ```
164
+
165
+ Aliases: `view`, `cat`.
166
+
167
+ Pass `--meta` to show the key's metadata instead of the public key object,
168
+ including the DIDs the key participates in (derived from the locally stored
169
+ DID documents):
170
+
171
+ ```
172
+ ./di key show issuer-signing --meta
173
+ FIELD VALUE
174
+ ----------- ------------------------------------------------
175
+ Fingerprint z6Mkr...
176
+ Type ed25519
177
+ Created 2026-06-10T17:22:31.123Z
178
+ Handle issuer-signing
179
+ Description Demo issuer signing key
180
+ DIDs did:key:z6Mkr...
181
+ ```
182
+
183
+ `--meta --json` prints the same metadata as a JSON object.
184
+
185
+ #### Edit key metadata
186
+
187
+ Show or edit the metadata of a stored key with `key meta` (looked up by
188
+ fingerprint or handle). With no options it prints the current metadata; with
189
+ `--handle` / `--description` it updates the metadata sidecar (the key file
190
+ itself is never rewritten). Passing an empty string clears a field:
191
+
192
+ ```
193
+ ./di key meta z6Mkr... --handle issuer-signing --description 'Demo issuer signing key'
194
+ Metadata saved to /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....meta.json
195
+ {
196
+ "created": "2026-06-10T17:22:31.123Z",
197
+ "handle": "issuer-signing",
198
+ "description": "Demo issuer signing key"
199
+ }
200
+
201
+ ./di key meta issuer-signing --description ''
202
+ ```
203
+
204
+ Keys saved before metadata support get a sidecar created on first edit, with
205
+ `created` backfilled from the date prefix of the key's file name.
206
+
207
+ #### Remove a key pair
208
+
209
+ Remove a stored key with `key remove` (aliases: `delete`, `rm`), looked up by
210
+ fingerprint or handle. Both the key file and its `.meta.json` metadata sidecar
211
+ are deleted:
212
+
213
+ ```
214
+ ./di key remove issuer-signing
215
+ Removed /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....json
216
+ Removed /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....meta.json
217
+ ```
218
+
219
+ ### DID Management
220
+
221
+ #### Create a DID
222
+
223
+ Generate a random Ed25519 `did:key` DID (method defaults to `key`):
224
+
225
+ ```
226
+ ./di did create
227
+ {
228
+ "id": "did:key:z6Mkr...",
229
+ "didDocument": { ... }
230
+ }
231
+ ```
232
+
233
+ Or pass the method explicitly:
234
+
235
+ ```
236
+ ./di did create key
237
+ ```
238
+
239
+ By default the DID's verification key is Ed25519. Pass `--type ecdsa` (with an
240
+ optional `--curve`, defaulting to `p256`) to mint a DID backed by an ECDSA key
241
+ instead. This works for both `did:key` and `did:web`:
242
+
243
+ ```
244
+ ./di did create key --type ecdsa --curve p384
245
+ ./di did create web --type ecdsa --url https://example.com
246
+ ```
247
+
248
+ ECDSA works for `did create web --type ecdsa` and `did add-key --type ecdsa`
249
+ too. Because ECDSA keys are not seed-derivable, `--with-seed` and
250
+ `SECRET_KEY_SEED` are not supported with `--type ecdsa`.
251
+
252
+ To also include the secret key seed in the output (useful for re-deriving the
253
+ same DID later), pass `--with-seed`:
254
+
255
+ ```
256
+ ./di did create --with-seed
257
+ {
258
+ "id": "did:key:z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
259
+ "secretKeySeed": "z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv",
260
+ "didDocument": {
261
+ "@context": [ ... ],
262
+ "id": "did:key:z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
263
+ "verificationMethod": [ ... ],
264
+ ...
265
+ }
266
+ }
267
+ ```
268
+
269
+ Generate a deterministic DID by setting the `SECRET_KEY_SEED` environment
270
+ variable to a multibase-encoded seed (e.g. from `@digitalcredentials/bnid`):
271
+
272
+ ```
273
+ SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv ./di did create
274
+ ```
275
+
276
+ Save the DID document and key material to local storage with `--save`
277
+ (written to `~/.config/did-cli-wallet/dids/` by default, or `$DIDS_DIR` if set). A `.meta.json`
278
+ metadata sidecar is written next to the DID document, recording the creation
279
+ timestamp; `--handle` and `--description` add user-defined metadata to it
280
+ (both require `--save`):
281
+
282
+ ```
283
+ ./di did create --save --handle demo-issuer
284
+ DID saved to /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....json
285
+ {
286
+ "id": "did:key:z6Mkr...",
287
+ "didDocument": { ... }
288
+ }
289
+ ```
290
+
291
+ If the DID's verification key also exists in the local wallet (e.g. both were
292
+ derived from the same seed), saving the DID records the association in that
293
+ key's metadata sidecar as well.
294
+
295
+ #### Create a did:web DID
296
+
297
+ Generate a `did:web` DID. Unlike `did:key`, a `did:web` DID is tied to a domain,
298
+ so `--url` (the HTTPS url of the DID document) is required:
299
+
300
+ ```
301
+ ./di did create web --url https://example.com
302
+ {
303
+ "id": "did:web:example.com",
304
+ "didDocument": { ... }
305
+ }
306
+ ```
307
+
308
+ This generates a single Ed25519 verification key, wired into the
309
+ `authentication`, `assertionMethod`, `capabilityDelegation`, and
310
+ `capabilityInvocation` relationships. Additional keys can be added later.
311
+
312
+ As with `did:key`, pass `--with-seed` to include the secret key seed in the
313
+ output (useful for re-deriving the same DID later):
314
+
315
+ ```
316
+ ./di did create web --url https://example.com --with-seed
317
+ {
318
+ "id": "did:web:example.com",
319
+ "secretKeySeed": "z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv",
320
+ "didDocument": { ... }
321
+ }
322
+ ```
323
+
324
+ Or set the `SECRET_KEY_SEED` environment variable to a multibase-encoded seed to
325
+ generate the DID deterministically:
326
+
327
+ ```
328
+ SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv \
329
+ ./di did create web --url https://example.com
330
+ ```
331
+
332
+ Save the DID document and key material to local storage with `--save` (written
333
+ to `~/.config/did-cli-wallet/dids/web/` by default, or `$DIDS_DIR` if set). The key file is an
334
+ object keyed by verification method id, so further keys can be appended later:
335
+
336
+ ```
337
+ ./di did create web --url https://example.com --save
338
+ DID saved to /home/user/.config/did-cli-wallet/dids/web/did:web:example.com.json
339
+ {
340
+ "id": "did:web:example.com",
341
+ "didDocument": { ... }
342
+ }
343
+ ```
344
+
345
+ #### Add a key to a did:web DID
346
+
347
+ Add another verification key to an existing, locally stored `did:web` DID (the
348
+ DID must have been saved with `did create web --save`). The new key is generated,
349
+ added to the DID document, and both the document and key file in storage are
350
+ updated in place:
351
+
352
+ ```
353
+ ./di did add-key did:web:example.com
354
+ DID saved to /home/user/.config/did-cli-wallet/dids/web/did:web:example.com.json
355
+ {
356
+ "id": "did:web:example.com",
357
+ "didDocument": { ... }
358
+ }
359
+ ```
360
+
361
+ By default the new key is wired into the `authentication`, `assertionMethod`,
362
+ `capabilityDelegation`, and `capabilityInvocation` relationships. Pass
363
+ `--purpose` (repeatable) to choose specific relationships:
364
+
365
+ ```
366
+ ./di did add-key did:web:example.com --purpose authentication --purpose assertionMethod
367
+ ```
368
+
369
+ By default the new key is Ed25519; pass `--type ecdsa` (with an optional
370
+ `--curve`, defaulting to `p256`) to add an ECDSA key instead:
371
+
372
+ ```
373
+ ./di did add-key did:web:example.com --type ecdsa --curve p384
374
+ ```
375
+
376
+ For Ed25519 keys, the new key is derived from a seed (as with `did create`):
377
+ pass `--with-seed` to generate (and print) a fresh seed, or set `SECRET_KEY_SEED`
378
+ to derive the key deterministically. ECDSA keys are not seed-derivable, so
379
+ `--with-seed` is not supported with `--type ecdsa`:
380
+
381
+ ```
382
+ ./di did add-key did:web:example.com --with-seed
383
+ ```
384
+
385
+ #### List DIDs
386
+
387
+ List the DIDs saved in local storage (via `did create --save`) as a table of
388
+ their metadata:
389
+
390
+ ```
391
+ ./di did list
392
+ HANDLE METHOD CREATED DID DESCRIPTION
393
+ ----------- ------ ---------- -------------------------------------------- -----------
394
+ demo-issuer key 2026-06-10 did:key:z6MkrLBubwzwEvwms...6MHdf3EeQMnCsdX2hM
395
+ ```
396
+
397
+ If no DIDs are stored, nothing is printed. Pass `--json` to output the list as
398
+ a JSON array of objects with metadata:
399
+
400
+ ```
401
+ ./di did list --json
402
+ [
403
+ {
404
+ "did": "did:key:z6Mkr...",
405
+ "method": "key",
406
+ "created": "2026-06-10T17:22:31.123Z",
407
+ "handle": "demo-issuer"
408
+ }
409
+ ]
410
+ ```
411
+
412
+ Or pass `--plain` to print just the DIDs, one per line, sorted:
413
+
414
+ ```
415
+ ./di did list --plain
416
+ did:key:z6Mkr...
417
+ did:key:z6Mks...
418
+ ```
419
+
420
+ #### Show a DID
421
+
422
+ Display the DID document saved in local storage (via `did create --save`),
423
+ looked up by DID or by its metadata handle. The stored DID document holds no
424
+ secret key material -- signing keys live in a separate key file -- so it is
425
+ printed as-is:
426
+
427
+ ```
428
+ ./di did show did:key:z6Mkr...
429
+ {
430
+ "@context": [ ... ],
431
+ "id": "did:key:z6Mkr...",
432
+ "verificationMethod": [ ... ],
433
+ ...
434
+ }
435
+ ```
436
+
437
+ Aliases: `view`, `cat`.
438
+
439
+ Pass `--meta` to show the DID's metadata instead of the DID document:
440
+
441
+ ```
442
+ ./di did show demo-issuer --meta
443
+ FIELD VALUE
444
+ ----------- ----------------------------------------------
445
+ DID did:key:z6Mkr...
446
+ Method key
447
+ Handle demo-issuer
448
+ Created 2026-06-10T17:22:31.123Z
449
+ Description
450
+ Keys 1
451
+ ```
452
+
453
+ `--meta --json` prints the same metadata as a JSON object.
454
+
455
+ #### Edit DID metadata
456
+
457
+ Show or edit the metadata of a stored DID with `did meta` (looked up by DID or
458
+ handle). With no options it prints the current metadata; with `--handle` /
459
+ `--description` it updates the metadata sidecar (the DID document itself is
460
+ never rewritten). Passing an empty string clears a field:
461
+
462
+ ```
463
+ ./di did meta did:key:z6Mkr... --handle demo-issuer --description 'Issuer DID for the demo'
464
+ Metadata saved to /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....meta.json
465
+ {
466
+ "created": "2026-06-10T17:22:31.123Z",
467
+ "handle": "demo-issuer",
468
+ "description": "Issuer DID for the demo"
469
+ }
470
+ ```
471
+
472
+ #### Remove a DID
473
+
474
+ Remove a stored DID with `did remove` (aliases: `delete`, `rm`), looked up by
475
+ DID or handle. The DID document, its `.keys.json` key file, and its
476
+ `.meta.json` metadata sidecar are all deleted, and the DID is scrubbed from
477
+ the cached `dids` associations of any matching wallet keys:
478
+
479
+ ```
480
+ ./di did remove demo-issuer
481
+ Removed /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....json
482
+ Removed /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....keys.json
483
+ Removed /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....meta.json
484
+ ```
485
+
486
+ ### Verifiable Credentials
487
+
488
+ #### Verify a credential
489
+
490
+ Run full verification on a Verifiable Credential (JSON). Beyond the
491
+ cryptographic signature check, this also verifies expiration, revocation /
492
+ status, and whether the issuer DID is recognized in any trusted registry
493
+ (via `@interop/verifier-core` and `@digitalcredentials/issuer-registry-client`).
494
+
495
+ The credential is read from a file argument, an http(s) URL, or, if neither
496
+ is given, from stdin:
497
+
498
+ ```
499
+ ./di vc verify credential.json
500
+ ./di vc verify https://example.com/credentials/123.json
501
+ cat credential.json | ./di vc verify
502
+ ```
503
+
504
+ By default it prints the full `@interop/verifier-core` verification result
505
+ (top-level `verified`, a per-suite `summary`, and the flat `results` of every
506
+ check). Pass `--summary` for a compact, human-friendly object instead:
507
+
508
+ ```
509
+ ./di vc verify credential.json --summary
510
+ {
511
+ "verified": true,
512
+ "checks": {
513
+ "signature": true,
514
+ "revoked": false,
515
+ "issuerRecognized": true
516
+ },
517
+ "matchingIssuers": [ ... ]
518
+ }
519
+ ```
520
+
521
+ A check is omitted from `checks` when it was skipped (for example `expired` is
522
+ absent when the credential has no expiration date).
523
+
524
+ The exit code is scriptable: `0` when the credential verified, `1` when it did
525
+ not, and `2` on a read/parse error or a structurally malformed credential.
526
+
527
+ The trusted registry list is fetched from the DCC
528
+ [known-did-registries](https://digitalcredentials.github.io/dcc-known-registries/known-did-registries.json)
529
+ at runtime, falling back to a bundled list of DCC registries when the network
530
+ is unavailable.
531
+
532
+ #### Issue a credential
533
+
534
+ Issue (sign) an unsigned Verifiable Credential with a locally-stored DID, acting
535
+ as a command-line wallet and issuer. The credential is read from a file
536
+ argument, an http(s) URL, or, if neither is given, from stdin, and the issued
537
+ credential is printed to stdout. If the input already carries a proof, issuing
538
+ appends an additional one.
539
+
540
+ The DID to issue with is required (`--did`); it must have been saved locally (see
541
+ `di did create --save`):
542
+
543
+ ```
544
+ ./di vc issue credential.json --did did:key:z6Mk...
545
+ cat credential.json | ./di vc issue --did did:key:z6Mk...
546
+ ```
547
+
548
+ The credential's `issuer` is set to the signing DID when the input has none.
549
+ When the input already names an `issuer`, it must match the signing DID,
550
+ otherwise issuance is aborted -- a credential cannot be issued by a DID other
551
+ than the one named as its issuer.
552
+
553
+ By default the first key in the DID's `assertionMethod` relationship is used.
554
+ Pass `--key` to choose a specific verification method; it must be authorized by
555
+ the DID's `assertionMethod` array, otherwise issuance fails:
556
+
557
+ ```
558
+ ./di vc issue credential.json --did did:key:z6Mk... --key did:key:z6Mk...#z6Mk...
559
+ ```
560
+
561
+ The signature suite defaults to the signing key's type. An Ed25519 DID signs
562
+ with `eddsa-rdfc-2022` (a W3C Data Integrity proof) by default; pass
563
+ `--suite Ed25519Signature2020` for the classic Ed25519Signature2020 proof:
564
+
565
+ ```
566
+ ./di vc issue credential.json --did did:key:z6Mk... --suite Ed25519Signature2020
567
+ ```
568
+
569
+ An ECDSA DID (see `did create --type ecdsa`) signs with `ecdsa-rdfc-2019`. The
570
+ suite is selected automatically from the key, so no `--suite` flag is needed:
571
+
572
+ ```
573
+ ./di vc issue credential.json --did did:key:zDna...
574
+ ```
575
+
576
+ Only the P-256 and P-384 curves can issue credentials -- the `ecdsa-rdfc-2019`
577
+ cryptosuite does not support P-521 (key creation warns about this). A suite that
578
+ does not match the key type (e.g. `--suite eddsa-rdfc-2022` for an ECDSA key) is
579
+ rejected. ECDSA credentials round-trip through `vc verify` (below).
580
+
581
+ Pass `--save` to also store the issued credential in local wallet storage
582
+ (`~/.config/did-cli-wallet/credentials/` by default, or `$WALLET_DIR` if set); `--save`
583
+ records the creation timestamp in a `.meta.json` metadata sidecar, and
584
+ `--handle` / `--description` (which require `--save`) tag the saved credential
585
+ the same way `zcap create --save` does:
586
+
587
+ ```
588
+ ./di vc issue credential.json --did did:key:z6Mk... --save --handle alumni
589
+ Credential saved to /home/user/.config/did-cli-wallet/credentials/sha256-1f4a....json
590
+ ```
591
+
592
+ The exit code is scriptable: `0` when the credential was issued, `1` on an
593
+ issuance error (an unauthorized key, an unknown suite, a missing DID / key
594
+ file, or an issuer that does not match the signing DID), and `2` on a
595
+ read/parse error.
596
+
597
+ #### Import a credential
598
+
599
+ Store an existing Verifiable Credential in local wallet storage with
600
+ `vc import`. The credential is read from a file argument, an http(s) URL, or,
601
+ if neither is given, from stdin. The input must structurally look like a
602
+ credential (its `type` must include `VerifiableCredential`); it is stored
603
+ as-is and is *not* verified on import (run `vc verify` for that). `--handle` /
604
+ `--description` tag the saved credential:
605
+
606
+ ```
607
+ ./di vc import credential.json --handle alumni --description 'Alumni credential'
608
+ ./di vc import https://example.com/credentials/123.json
609
+ cat credential.json | ./di vc import
610
+ Credential saved to /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d....json
611
+ ```
612
+
613
+ The credential file is named after the credential's `id`; a credential
614
+ without an `id` (the property is optional) is stored under a digest of its
615
+ content, so re-importing it overwrites rather than duplicates. Re-importing a
616
+ credential preserves the metadata its sidecar already carries.
617
+
618
+ The exit code is scriptable: `0` when the credential was imported, `1` when
619
+ the input is not a Verifiable Credential, and `2` on a fetch/read/parse error.
620
+
621
+ #### List credentials
622
+
623
+ List the credentials saved in local wallet storage (via `vc import` or
624
+ `vc issue --save`) as a table of their metadata. The TYPE column shows the
625
+ credential's most specific type (its first `type` entry other than the
626
+ generic `VerifiableCredential`):
627
+
628
+ ```
629
+ ./di vc list
630
+ HANDLE TYPE ISSUER CREATED ID DESCRIPTION
631
+ ------ ------------------- ------------------------------ ---------- --------------------------- -----------------
632
+ alumni OpenBadgeCredential did:key:z6MkExa...ampleIssuer 2026-06-11 urn:uuid:9b1deb4d-3b7d-4ba8 Alumni credential
633
+ ```
634
+
635
+ If no credentials are stored, nothing is printed. Pass `--json` to output the
636
+ list as a JSON array of objects with metadata:
637
+
638
+ ```
639
+ ./di vc list --json
640
+ [
641
+ {
642
+ "id": "urn:uuid:9b1deb4d-3b7d-4ba8",
643
+ "type": "OpenBadgeCredential",
644
+ "issuer": "did:key:z6MkExampleIssuer",
645
+ "created": "2026-06-11T17:22:31.123Z",
646
+ "handle": "alumni",
647
+ "description": "Alumni credential"
648
+ }
649
+ ]
650
+ ```
651
+
652
+ Or pass `--plain` to print just the credential ids, one per line, sorted. A
653
+ credential without an `id` is listed by its storage id (the `sha256-...` file
654
+ name), which `show` / `meta` / `remove` accept in place of a credential id.
655
+
656
+ #### Show a credential
657
+
658
+ Display a credential saved in local wallet storage, looked up by its
659
+ credential id (as printed by `vc list`), its storage id, or its metadata
660
+ handle:
661
+
662
+ ```
663
+ ./di vc show alumni
664
+ {
665
+ "@context": ["https://www.w3.org/ns/credentials/v2"],
666
+ "id": "urn:uuid:9b1deb4d-3b7d-4ba8",
667
+ "type": ["VerifiableCredential", "OpenBadgeCredential"],
668
+ ...
669
+ }
670
+ ```
671
+
672
+ Aliases: `view`, `cat`.
673
+
674
+ Pass `--meta` to show the credential's metadata instead, along with its
675
+ issuer, validity start, and expiration:
676
+
677
+ ```
678
+ ./di vc show alumni --meta
679
+ FIELD VALUE
680
+ ----------- ----------------------------
681
+ ID urn:uuid:9b1deb4d-3b7d-4ba8
682
+ Type OpenBadgeCredential
683
+ Handle alumni
684
+ Created 2026-06-11T17:22:31.123Z
685
+ Description Alumni credential
686
+ Issuer did:key:z6MkExampleIssuer
687
+ Valid From 2026-01-01T00:00:00Z
688
+ Expires
689
+ ```
690
+
691
+ `--meta --json` prints the same metadata as a JSON object.
692
+
693
+ #### Edit credential metadata
694
+
695
+ Show or edit the metadata of a stored credential with `vc meta` (looked up by
696
+ credential id, storage id, or handle). With no options it prints the current
697
+ metadata; with `--handle` / `--description` it updates the metadata sidecar
698
+ (the stored credential itself is never rewritten). Passing an empty string
699
+ clears a field:
700
+
701
+ ```
702
+ ./di vc meta urn:uuid:9b1deb4d-3b7d-4ba8 \
703
+ --handle alumni --description 'Alumni credential'
704
+ Metadata saved to /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d-3b7d-4ba8.meta.json
705
+ {
706
+ "created": "2026-06-11T17:22:31.123Z",
707
+ "handle": "alumni",
708
+ "description": "Alumni credential"
709
+ }
710
+
711
+ ./di vc meta alumni --description ''
712
+ ```
713
+
714
+ #### Remove a credential
715
+
716
+ Remove a stored credential with `vc remove` (aliases: `delete`, `rm`), looked
717
+ up by credential id, storage id, or handle. Both the credential file and its
718
+ `.meta.json` metadata sidecar are deleted:
719
+
720
+ ```
721
+ ./di vc remove alumni
722
+ Removed /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d-3b7d-4ba8.json
723
+ Removed /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d-3b7d-4ba8.meta.json
724
+ ```
725
+
726
+ ### Authorization Capabilities (zCaps)
727
+
728
+ An Authorization Capability (zCap) grants its
729
+ controller permission to invoke an action against a resource (the
730
+ `invocationTarget`). Authority starts at an unsigned _root_ capability and is
731
+ handed down a chain of signed _delegated_ capabilities, each one optionally
732
+ narrowing the allowed actions or the target.
733
+
734
+ Both commands print the capability as JSON together with an `encoded` field --
735
+ the capability serialized and `base58btc`-encoded with a multibase `z` prefix --
736
+ which is the compact form you pass to `zcap delegate --capability` to delegate it
737
+ further. Pass `--save` to also write the capability to local wallet storage
738
+ (`~/.config/did-cli-wallet/zcaps/` by default, or `$WALLET_DIR` if set); `--save` records the
739
+ creation timestamp in a `.meta.json` metadata sidecar, and `--handle` /
740
+ `--description` (which require `--save`) tag the saved capability the same way
741
+ `key create --save` and `did create --save` do. The exit code is `0` on
742
+ success and `1` on a creation / delegation or input error.
743
+
744
+ #### Create a root capability
745
+
746
+ Build the root capability for an invocation target. The `--controller` is the DID
747
+ that holds root authority over the target, and `--url` is the `invocationTarget`.
748
+ Root capabilities are unsigned, so no key is needed:
749
+
750
+ ```
751
+ ./di zcap create \
752
+ --controller did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR \
753
+ --url https://example.com/api
754
+ {
755
+ "rootCapability": {
756
+ "@context": "https://w3id.org/zcap/v1",
757
+ "id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi",
758
+ "controller": "did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR",
759
+ "invocationTarget": "https://example.com/api"
760
+ },
761
+ "encoded": "z3g9TJBrQTdKemE9BC43N9WsT8snKvQzwCpCWs8o..."
762
+ }
763
+ ```
764
+
765
+ The root capability's `id` is always `urn:zcap:root:<url-encoded invocationTarget>`,
766
+ and a root capability grants all actions (it has no `allowedAction`).
767
+
768
+ #### Note about the `encoded` field
769
+
770
+ The multibase- (that's the `z` prefix) and base58btc-encoded JSON of the zcap
771
+ is returned, for convenience, in the `encoded` field.
772
+
773
+ This is done for easier "double-click to copy" and pasting into other tools,
774
+ such as password managers, server env secrets, etc.
775
+
776
+ #### Delegate a capability
777
+
778
+ Delegate authority to another DID (`--delegatee`, which becomes the delegated
779
+ capability's controller). The delegation is signed with the delegator's
780
+ `capabilityDelegation` key, sourced one of two ways:
781
+
782
+ - **A locally-stored DID (`--did`)** -- the DID must have been saved with
783
+ `di did create --save`; this is the preferred mode and mirrors `vc issue`.
784
+ - **A secret key seed (`ZCAP_CONTROLLER_KEY_SEED` + `--controller`)** -- the
785
+ `did:key` is re-derived from the seed and checked against `--controller`.
786
+
787
+ To delegate from the root capability for a target, pass `--url` (the same
788
+ `invocationTarget` the root was created for) and the action(s) to allow with
789
+ `--allow` (repeatable; if omitted the delegatee inherits the parent's actions):
790
+
791
+ ```
792
+ ./di zcap delegate \
793
+ --did did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR \
794
+ --delegatee did:key:z6MknBxrctS4KsfiBsEaXsfnrnfNYTvDjVpLYYUAN6PX2EfG \
795
+ --url https://example.com/documents \
796
+ --allow read
797
+ {
798
+ "delegatedCapability": {
799
+ "@context": [
800
+ "https://w3id.org/zcap/v1",
801
+ "https://w3id.org/security/suites/ed25519-2020/v1"
802
+ ],
803
+ "id": "urn:uuid:e03d4f97-2e70-42e8-ae5d-51e92e903afa",
804
+ "controller": "did:key:z6MknBxrctS4KsfiBsEaXsfnrnfNYTvDjVpLYYUAN6PX2EfG",
805
+ "parentCapability": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fdocuments",
806
+ "invocationTarget": "https://example.com/documents",
807
+ "expires": "2027-06-07T17:30:00Z",
808
+ "allowedAction": ["read"],
809
+ "proof": {
810
+ "type": "Ed25519Signature2020",
811
+ "created": "2026-06-07T17:30:00Z",
812
+ "verificationMethod": "did:key:z6Mkfeco...#z6Mkfeco...",
813
+ "proofPurpose": "capabilityDelegation",
814
+ "capabilityChain": ["urn:zcap:root:https%3A%2F%2Fexample.com%2Fdocuments"],
815
+ "proofValue": "z5tuwwdJE6VXLhf1v8SNAquBmMcJCD7zJ4bXDi6rh1Fk..."
816
+ }
817
+ },
818
+ "encoded": "zkL8vet8M2mn7akSpHEVvgFUCTVq4VSGs1s8Zsq9bYba..."
819
+ }
820
+ ```
821
+
822
+ The same delegation, signed via a secret key seed instead of a stored DID:
823
+
824
+ ```
825
+ ZCAP_CONTROLLER_KEY_SEED=z1AZK4h5w5YZkKYEgqtcFfvSbWQ3tZ3ZFgmLsXMZsTVoeK7 \
826
+ ./di zcap delegate \
827
+ --controller did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR \
828
+ --delegatee did:key:z6MknBxrctS4KsfiBsEaXsfnrnfNYTvDjVpLYYUAN6PX2EfG \
829
+ --url https://example.com/documents \
830
+ --allow read
831
+ ```
832
+
833
+ To delegate an _existing_ capability further down the chain, pass it as
834
+ `--capability` instead of `--url` -- either the `encoded` string from a previous
835
+ delegation or a path to a JSON file containing the capability. Use
836
+ `--invocation-target` to attenuate (narrow) the parent's target to a sub-path:
837
+
838
+ ```
839
+ ./di zcap delegate \
840
+ --did did:key:z6MknBxr... \
841
+ --delegatee did:key:z6Mks... \
842
+ --capability zkL8vet8M2mn7akSpHEVvgFUCTVq4VSGs1s8Zsq9bYba... \
843
+ --invocation-target https://example.com/documents/reports \
844
+ --allow read
845
+ ```
846
+
847
+ The delegated capability expires after `--ttl` (a duration such as `1y`, `30d`,
848
+ `24h`, `15m`; default `1y`). Pass `--expires` with an explicit ISO 8601 date to
849
+ override it:
850
+
851
+ ```
852
+ ./di zcap delegate --did did:key:z6Mk... --delegatee did:key:z6Mkn... \
853
+ --url https://example.com/documents --allow read --ttl 30d
854
+
855
+ ./di zcap delegate --did did:key:z6Mk... --delegatee did:key:z6Mkn... \
856
+ --url https://example.com/documents --allow read --expires 2027-01-01T00:00:00Z
857
+ ```
858
+
859
+ #### List capabilities
860
+
861
+ List the capabilities saved in local wallet storage (via `zcap create --save`
862
+ or `zcap delegate --save`) as a table of their metadata. The TYPE column shows
863
+ whether the capability is a `root` or a `delegated` one:
864
+
865
+ ```
866
+ ./di zcap list
867
+ HANDLE TYPE CREATED ID DESCRIPTION
868
+ -------- ---- ---------- -------------------------------------------- -------------
869
+ api-root root 2026-06-11 urn:zcap:root:https%3...%2Fexample.com%2Fapi Demo API root
870
+ ```
871
+
872
+ If no capabilities are stored, nothing is printed. Pass `--json` to output the
873
+ list as a JSON array of objects with metadata:
874
+
875
+ ```
876
+ ./di zcap list --json
877
+ [
878
+ {
879
+ "id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi",
880
+ "type": "root",
881
+ "created": "2026-06-11T17:22:31.123Z",
882
+ "handle": "api-root",
883
+ "description": "Demo API root"
884
+ }
885
+ ]
886
+ ```
887
+
888
+ Or pass `--plain` to print just the capability ids, one per line, sorted:
889
+
890
+ ```
891
+ ./di zcap list --plain
892
+ urn:zcap:root:https%3A%2F%2Fexample.com%2Fa
893
+ urn:zcap:root:https%3A%2F%2Fexample.com%2Fb
894
+ ```
895
+
896
+ #### Show a capability
897
+
898
+ Display a capability saved in local wallet storage, looked up by its
899
+ capability id (as printed by `zcap list`) or by its metadata handle:
900
+
901
+ ```
902
+ ./di zcap show api-root
903
+ {
904
+ "@context": "https://w3id.org/zcap/v1",
905
+ "id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi",
906
+ "controller": "did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR",
907
+ "invocationTarget": "https://example.com/api"
908
+ }
909
+ ```
910
+
911
+ Aliases: `view`, `cat`.
912
+
913
+ Pass `--meta` to show the capability's metadata instead, along with its
914
+ controller, invocation target, and (for delegated capabilities) expiration:
915
+
916
+ ```
917
+ ./di zcap show api-root --meta
918
+ FIELD VALUE
919
+ ----------- --------------------------------------------------------
920
+ ID urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi
921
+ Type root
922
+ Handle api-root
923
+ Created 2026-06-11T17:22:31.123Z
924
+ Description Demo API root
925
+ Controller did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR
926
+ Target https://example.com/api
927
+ Expires
928
+ ```
929
+
930
+ `--meta --json` prints the same metadata as a JSON object.
931
+
932
+ #### Edit capability metadata
933
+
934
+ Show or edit the metadata of a stored capability with `zcap meta` (looked up
935
+ by capability id or handle). With no options it prints the current metadata;
936
+ with `--handle` / `--description` it updates the metadata sidecar (the stored
937
+ capability itself is never rewritten). Passing an empty string clears a field:
938
+
939
+ ```
940
+ ./di zcap meta urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi \
941
+ --handle api-root --description 'Demo API root'
942
+ Metadata saved to /home/user/.config/did-cli-wallet/zcaps/urn_zcap_root_https_3A_2F_2Fexample.com_2Fapi.meta.json
943
+ {
944
+ "created": "2026-06-11T17:22:31.123Z",
945
+ "handle": "api-root",
946
+ "description": "Demo API root"
947
+ }
948
+
949
+ ./di zcap meta api-root --description ''
950
+ ```
951
+
952
+ #### Remove a capability
953
+
954
+ Remove a stored capability with `zcap remove` (aliases: `delete`, `rm`),
955
+ looked up by capability id or handle. Both the capability file and its
956
+ `.meta.json` metadata sidecar are deleted:
957
+
958
+ ```
959
+ ./di zcap remove api-root
960
+ Removed /home/user/.config/did-cli-wallet/zcaps/urn_zcap_root_https_3A_2F_2Fexample.com_2Fapi.json
961
+ Removed /home/user/.config/did-cli-wallet/zcaps/urn_zcap_root_https_3A_2F_2Fexample.com_2Fapi.meta.json
962
+ ```
963
+
964
+ Note that removing a capability from local storage does not revoke it -- a
965
+ delegated capability that has already been handed to its delegatee remains
966
+ valid until it expires (see `--ttl` / `--expires`).
967
+
968
+ ### Wallet Attached Storage (WAS)
969
+
970
+ The `was` command group is a client for
971
+ [Wallet Attached Storage](https://digitalcredentials.github.io/wallet-attached-storage-spec/)
972
+ servers, which organize content as `Space > Collection > Resource` behind
973
+ zcap-authorized HTTP. Every request is signed with a `did:key` DID stored in
974
+ the local wallet (saved with `did create --save`; Ed25519 keys only for now).
975
+
976
+ Commands address content with a single positional **WAS path**:
977
+
978
+ ```
979
+ SPACE[/COLLECTION[/RESOURCE]]
980
+ ```
981
+
982
+ where `SPACE` is one of:
983
+
984
+ - a **registry handle** (e.g. `home`) of a space registered in the local
985
+ wallet (`~/.config/did-cli-wallet/was-spaces/`), which also supplies the server URL and
986
+ signing DID defaults;
987
+ - a **bare space id** (a server-generated uuid or urn), combined with
988
+ `--server` / `WAS_SERVER_URL`;
989
+ - a **full space URL** (e.g. `https://was.example/space/8124...cf2e`), which
990
+ is self-contained -- the server URL is its origin. Collection and resource
991
+ segments can be appended to any of the three forms.
992
+
993
+ The signing DID resolves from `--did` (a DID or stored-DID handle), the
994
+ `WAS_DID` environment variable, or the controller recorded in the registry
995
+ entry. Exit codes: `0` success, `1` operation error (a typed WAS error or a
996
+ not-found/not-visible read -- the spec returns 404 for both), `2` input error
997
+ (bad path, unknown handle/DID, missing server URL).
998
+
999
+ #### Create a space
1000
+
1001
+ Create a space on a WAS server (`--name`, a display name, is optional). Pass
1002
+ `--save` to register it in the local wallet, with the usual `--handle` /
1003
+ `--description` metadata; the handle is what makes every later command short:
1004
+
1005
+ ```
1006
+ ./di was space create --name 'Home space' \
1007
+ --server http://localhost:3002 --did did:key:z6Mkfeco... \
1008
+ --save --handle home
1009
+ Space registered in /home/user/.config/did-cli-wallet/was-spaces/81246131-69a4-45ab-9bff-9c946b59cf2e.json
1010
+ {
1011
+ "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
1012
+ "url": "http://localhost:3002/space/81246131-69a4-45ab-9bff-9c946b59cf2e",
1013
+ "name": "Home space",
1014
+ "controller": "did:key:z6Mkfeco..."
1015
+ }
1016
+ ```
1017
+
1018
+ Without `--save`, address the space later by its full URL (or register it
1019
+ afterwards with `was space add`).
1020
+
1021
+ #### List, show, and manage spaces
1022
+
1023
+ `was space list` lists the locally registered spaces (WAS servers do not
1024
+ implement server-side space listing yet; `--remote` asks anyway and surfaces
1025
+ the 501). `--json` and `--plain` work as in the other list commands:
1026
+
1027
+ ```
1028
+ ./di was space list
1029
+ HANDLE NAME SPACE ID SERVER CREATED
1030
+ ------ ---------- ------------------------------------ --------------------- ----------
1031
+ home Home space 81246131-69a4-45ab-9bff-9c946b59cf2e http://localhost:3002 2026-06-11
1032
+ ```
1033
+
1034
+ `was space show` (aliases: `view`, `cat`) prints the Space Description from
1035
+ the server, or the local registry record with `--meta`:
1036
+
1037
+ ```
1038
+ ./di was space show home
1039
+ {
1040
+ "id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
1041
+ "type": ["Space"],
1042
+ "name": "Home space",
1043
+ "controller": "did:key:z6Mkfeco..."
1044
+ }
1045
+ ```
1046
+
1047
+ `was space update` (alias: `configure`) upserts description fields
1048
+ (`--name`), also refreshing the registry entry. `was space add` registers an
1049
+ *existing* remote space (a full space URL, or a bare id plus `--server`) in
1050
+ the local registry, verifying it with a describe first. The local/remote
1051
+ delete pair:
1052
+
1053
+ - `was space delete <space>` (alias: `rm`) deletes the space **on the
1054
+ server** (idempotent) and removes the registry entry;
1055
+ - `was space forget <space>` removes only the local registry entry.
1056
+
1057
+ #### Manage collections
1058
+
1059
+ The `collection` group (alias: `coll`) manages collections within a space.
1060
+ `create` takes a space address plus an optional `--name` and `--id` (the id
1061
+ is server-generated otherwise); `show`/`update`/`delete` take a
1062
+ `SPACE/COLLECTION` path:
1063
+
1064
+ ```
1065
+ ./di was collection create home --name Credentials --id credentials
1066
+ {
1067
+ "id": "credentials",
1068
+ "url": "http://localhost:3002/space/8124...cf2e/credentials",
1069
+ "name": "Credentials"
1070
+ }
1071
+
1072
+ ./di was collection list home
1073
+ ID NAME URL
1074
+ ----------- ----------- ---------------------------------------------------
1075
+ credentials Credentials http://localhost:3002/space/8124...cf2e/credentials
1076
+
1077
+ ./di was collection delete home/credentials
1078
+ Deleted http://localhost:3002/space/8124...cf2e/credentials on the server.
1079
+ ```
1080
+
1081
+ #### Add and read resources
1082
+
1083
+ The `resource` group (alias: `res`) manages the content itself. Payloads come
1084
+ from a file argument or stdin: `*.json` files (and any input that parses to a
1085
+ JSON object or array) are sent as JSON, anything else as binary
1086
+ `application/octet-stream`, and an explicit `--content-type` sends the bytes
1087
+ as-is with that type (useful for e.g. `application/ld+json` or images).
1088
+
1089
+ `add` posts to a collection and lets the server pick the resource id; `put`
1090
+ creates or replaces at a known id:
1091
+
1092
+ ```
1093
+ ./di was resource add home/credentials vc.json
1094
+ {
1095
+ "id": "d3c9...",
1096
+ "url": "http://localhost:3002/space/8124...cf2e/credentials/d3c9...",
1097
+ "contentType": "application/json"
1098
+ }
1099
+
1100
+ ./di was resource put home/credentials/vc-1 vc.json
1101
+ cat vc.json | ./di was resource put home/credentials/vc-1
1102
+ ./di was resource put home/photos/pic-1 photo.png --content-type image/png
1103
+ ```
1104
+
1105
+ `get` pretty-prints JSON to stdout and writes binary raw (use `--output` for
1106
+ files); a missing or not-visible resource prints
1107
+ `Not found (or not visible to you): <url>` and exits `1`:
1108
+
1109
+ ```
1110
+ ./di was resource get home/credentials/vc-1
1111
+ {
1112
+ "name": "Alice"
1113
+ }
1114
+
1115
+ ./di was resource get home/photos/pic-1 --output photo.png
1116
+ ```
1117
+
1118
+ `list` renders the resources of a collection (`ID | CONTENT TYPE | URL`), and
1119
+ `delete` (alias: `rm`) removes one (idempotent).
1120
+
1121
+ #### Shorthand verbs
1122
+
1123
+ For day-to-day use, the top-level verbs dispatch on the path depth:
1124
+
1125
+ ```
1126
+ ./di was ls home # collections of a space
1127
+ ./di was ls home/credentials # resources of a collection
1128
+ ./di was get home/credentials/vc-1 # = resource get
1129
+ ./di was put home/credentials/vc-1 vc.json # = resource put
1130
+ ./di was rm home/credentials/vc-1 # delete whatever the path points at
1131
+ ./di was rm home # ... including a whole space
1132
+ ```
1133
+
1134
+ #### Delegate access (grant)
1135
+
1136
+ `was grant` delegates access to a space, collection, or resource. Actions are
1137
+ HTTP verbs (`GET`, `PUT`, `POST`, `DELETE`; lowercase accepted), expiration
1138
+ comes from `--ttl` (default `1y`) or an explicit `--expires`, and the output
1139
+ is the signed capability plus its `encoded` multibase form -- the same shape
1140
+ as `zcap delegate`. `--save` (with `--handle` / `--description`) stores it in
1141
+ the zcap store (`~/.config/did-cli-wallet/zcaps/`):
1142
+
1143
+ ```
1144
+ ./di was grant home/credentials --to did:key:z6MkBob... --action GET PUT
1145
+ {
1146
+ "delegatedCapability": {
1147
+ "@context": [...],
1148
+ "id": "urn:uuid:e03d4f97-...",
1149
+ "controller": "did:key:z6MkBob...",
1150
+ "invocationTarget": "http://localhost:3002/space/8124...cf2e/credentials",
1151
+ "allowedAction": ["GET", "PUT"],
1152
+ "expires": "2027-06-11T17:30:00Z",
1153
+ "proof": { ... }
1154
+ },
1155
+ "encoded": "zkL8vet8M2mn..."
1156
+ }
1157
+ ```
1158
+
1159
+ Hand the `encoded` string (or the JSON) to the delegatee out-of-band.
1160
+
1161
+ #### Use a received capability
1162
+
1163
+ On the receiving side, `ls` / `get` / `put` / `rm` (and `resource
1164
+ add/get/put`) accept `--capability` instead of a path. The reference is one
1165
+ of:
1166
+
1167
+ - the `encoded` multibase string from the grant output (`zkL8vet...`),
1168
+ - a path to a JSON file holding the capability, or
1169
+ - the capability id or metadata **handle of a zcap** stored in
1170
+ `~/.config/did-cli-wallet/zcaps/`.
1171
+
1172
+ Note that a `--capability` reference is *not* a WAS path -- no space,
1173
+ collection, or resource address is given (or needed). The capability itself
1174
+ records what it grants access to in its `invocationTarget`, and that is what
1175
+ the command operates on:
1176
+
1177
+ - a capability granted on a **resource** drives `get` / `put` / `rm`;
1178
+ - one granted on a **collection** drives `ls`, `resource add`, and `rm`;
1179
+ - one granted on a whole **space** drives `ls` and `rm`.
1180
+
1181
+ A depth mismatch (e.g. `get` with a collection-scoped capability) is an
1182
+ input error. The server URL is taken from the invocation target's origin,
1183
+ and the signing DID defaults to the capability's controller (the delegatee)
1184
+ when that DID is stored locally -- so usually no flags are needed at all.
1185
+
1186
+ In the examples below, `bob-share` is the metadata handle of a *stored zcap*
1187
+ (not a space or collection handle): say Alice granted Bob `GET`/`PUT` on the
1188
+ single resource `home/credentials/vc-1`, and the capability was saved with
1189
+ `--save --handle bob-share` (on Alice's machine via `was grant --save`; on
1190
+ Bob's machine he can pass the encoded string or a JSON file directly):
1191
+
1192
+ ```
1193
+ # Alice delegates one resource to Bob, keeping a tagged copy:
1194
+ ./di was grant home/credentials/vc-1 --to did:key:z6MkBob... \
1195
+ --action GET PUT --save --handle bob-share
1196
+
1197
+ # Bob, with the encoded string he received out-of-band -- this reads the
1198
+ # resource the capability targets (home/credentials/vc-1):
1199
+ ./di was get --capability zkL8vet8M2mn...
1200
+ {
1201
+ "name": "Alice"
1202
+ }
1203
+
1204
+ # The same via a capability JSON file, or a stored zcap's handle:
1205
+ ./di was get --capability ./bob-share.json
1206
+ ./di was get --capability bob-share
1207
+
1208
+ # A resource-scoped capability also allows writing (it grants PUT):
1209
+ ./di was put data.json --capability bob-share
1210
+
1211
+ # Had the grant been on the whole collection (home/credentials), ls would
1212
+ # list it and `resource add` could post new resources into it:
1213
+ ./di was ls --capability zkL8vet8M2mn...
1214
+ ```
1215
+
1216
+ #### Policies and public sharing
1217
+
1218
+ By default all operations on a space requires a capability invocation. A
1219
+ **policy** can override this per space, collection, or resource; the common
1220
+ case is `PublicCanRead` -- the "share via public link" model. `was publish`
1221
+ is the sugar for it and prints the public URL; `was unpublish` reverts to
1222
+ capability-only access:
1223
+
1224
+ ```
1225
+ ./di was publish home/credentials/vc-1
1226
+ Published (world-readable): http://localhost:3002/space/8124...cf2e/credentials/vc-1
1227
+ http://localhost:3002/space/8124...cf2e/credentials/vc-1
1228
+
1229
+ curl http://localhost:3002/space/8124...cf2e/credentials/vc-1 # no auth needed
1230
+ ```
1231
+
1232
+ The generic primitives work at any depth: `was policy show <path>` prints the
1233
+ policy document (or `No policy set (or not visible to you)` with exit `1`),
1234
+ `was policy set <path> --type PublicCanRead` (or a policy JSON file for
1235
+ richer, server-defined policies), and `was policy clear <path>`.
1236
+
1237
+ #### Export and import a space
1238
+
1239
+ `was space export` downloads a whole space as a tar archive (to `--output`
1240
+ or raw to stdout); `was space import` merges a tar into a space (from a file
1241
+ or stdin) and prints the import stats:
1242
+
1243
+ ```
1244
+ ./di was space export home --output home.tar
1245
+ Wrote 10240 bytes to home.tar
1246
+
1247
+ ./di was space import other-space home.tar
1248
+ {
1249
+ "collectionsCreated": 1,
1250
+ "collectionsSkipped": 0,
1251
+ "resourcesCreated": 2,
1252
+ "resourcesSkipped": 0,
1253
+ "policiesCreated": 0,
1254
+ "policiesSkipped": 0
1255
+ }
1256
+ ```
1257
+
1258
+ #### End-to-end smoke test
1259
+
1260
+ To exercise the whole flow against a local server, start the reference
1261
+ [was-teaching-server](https://github.com/digitalcredentials/was-teaching-server)
1262
+ (the server URL must match byte-for-byte, including the port, since zcap
1263
+ invocation targets embed it):
1264
+
1265
+ ```
1266
+ SERVER_URL='http://localhost:3002' PORT=3002 pnpm dev
1267
+ ```
1268
+
1269
+ Then, in another shell:
1270
+
1271
+ ```
1272
+ export WAS_SERVER_URL=http://localhost:3002
1273
+
1274
+ ./di did create --save --handle alice
1275
+ ./di was space create --name Demo --did alice --save --handle demo
1276
+ ./di was collection create demo --name Docs --id docs
1277
+ echo '{"hello": "world"}' | ./di was put demo/docs/doc-1
1278
+ ./di was get demo/docs/doc-1
1279
+
1280
+ ./di did create --save --handle bob # the delegatee
1281
+ ./di was grant demo/docs/doc-1 --to <bob's did> --action GET --did alice \
1282
+ --save --handle bob-share
1283
+ ./di was get --capability bob-share --did bob
1284
+
1285
+ ./di was publish demo/docs/doc-1 # prints the public URL
1286
+ curl <public url> # readable without auth
1287
+
1288
+ ./di was rm demo # clean up (deletes the space)
1289
+ ```
1290
+
1291
+ The same flow runs as an env-gated integration test: `npm test` skips it
1292
+ unless `WAS_TEST_SERVER_URL` points at a running server:
1293
+
1294
+ ```
1295
+ WAS_TEST_SERVER_URL=http://localhost:3002 npm run test:node
1296
+ ```
1297
+
1298
+ ## Contribute
1299
+
1300
+ PRs accepted.
1301
+
1302
+ If editing the Readme, please conform to the
1303
+ [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
1304
+
1305
+ ## License
1306
+
1307
+ [MIT](LICENSE.md) © 2026 Interop Alliance