@interop/did-cli
v0.15.0
Published
DID CLI tool for DIDs, Verifiable Credentials, key pairs, and zcaps
Downloads
194
Readme
DID CLI wallet (@interop/did-cli)
A command line client for managing DIDs, VCs, zCaps, and corresponding cryptographic key pairs, written in Typescript.
Table of Contents
Background
@interop/did-cli is a command-line wallet for the building blocks of
decentralized identity: Decentralized Identifiers
(DIDs), Verifiable Credentials (VCs),
Authorization Capabilities (zCaps), and the
cryptographic key pairs that underpin them.
It lets you generate and manage these objects locally -- minting DIDs, issuing and verifying credentials, delegating capabilities, and encrypting data -- and interact with remote Wallet Attached Storage servers, all from the terminal and backed by a local file-based wallet.
Features
- DIDs -- create, resolve, and manage
did:key,did:web, anddid:webvhDIDs (withdid:webvhkey pre-rotation, rotation, and history logs). - Keys -- generate and store Ed25519, ECDSA, X25519, and HMAC key pairs, with deterministic seed-based generation.
- Verifiable Credentials -- issue (sign), verify, and import VCs, including signature, expiration, revocation, and trusted-issuer checks.
- Authorization Capabilities (zCaps) -- create root capabilities and delegate (attenuate) authority down a signed capability chain.
- Encryption (EDV) -- encrypt and decrypt to X25519 recipients as raw JWE, EDV Documents, or chunked stream bundles.
- Wallet Attached Storage (WAS) -- a client for WAS servers: manage spaces, collections, and resources over zcap-authorized HTTP.
- Local wallet -- everything is stored in a local file-based wallet with searchable metadata (handles, descriptions, timestamps).
Install
pnpm install -g @interop/did-cliUsage
Help is available with the --help/-h command line option:
./di -h
./di COMMAND -hCommand Summary
di did create [method] create a DID (key | web | webvh; default key)
di did add-key <did> add a verification method to a stored DID
di did add-service <did> add a service entry to a stored did:web/webvh
di did remove-service <did> remove a service entry from a stored did:web/webvh
di did webvh rotate-keys <did> rotate a stored did:webvh update (auth) key
di did get|resolve <did> resolve a DID (or DID URL) via the document loader
di did show|view|cat <did> show a stored DID document
di did list list stored DIDs
di did meta <did> show/edit a DID's local metadata
di did remove|delete|rm <did> remove a stored DID
di key create generate a key pair
di key list list stored keys
di key show|view|cat <id> show a stored key
di key meta <id> show/edit a key's local metadata
di key remove|delete|rm <id> remove a stored key
di key export <id> export a key pair
di vc verify [file] verify a Verifiable Credential
di vc issue [file] issue (sign) a Verifiable Credential
di vc import [source] import a credential into the wallet
di vc list list stored credentials
di vc show|view|cat <id> show a stored credential
di vc meta <id> show/edit a credential's local metadata
di vc remove|delete|rm <id> remove a stored credential
di zcap create create a root capability
di zcap delegate delegate (attenuate) a capability
di zcap import <capability> validate and store a received capability
di zcap list list stored capabilities
di zcap show|view|cat <id> show a stored capability
di zcap meta <id> show/edit a capability's local metadata
di zcap remove|delete|rm <id> remove a stored capability
di zcap revoke <id> revoke a delegated capability
di edv encrypt [file] encrypt to X25519 recipients (raw JWE, an EDV Document with --document, or a chunked bundle with --stream)
di edv decrypt [file] decrypt a JWE, EDV Document, or stream bundle with a stored X25519 key
di wallet ls|list list all wallet collections and items
di was space <create|list|show|update|delete|forget|add|backends|quotas|export|import>
di was collection|coll <create|list|show|update|delete|backend|quota>
di was resource|res <add|put|get|list|delete>
di was ls|get|put|rm [path] depth-dispatching shorthands
di was policy <show|set|clear> manage access-control policies
di was publish|unpublish <path> toggle world-readable access
di was grant <path> delegate access via a signed capability
di was request-grant ask a wallet for a capability on a collectionEnvironment Variables
These environment variables configure storage locations and provide defaults or secret-key seeds for individual commands. Each is also documented inline in the relevant command section below.
| Variable | Used by | Purpose |
| -------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| WALLET_DIR | all | Wallet collections directory (keys/, zcaps/, credentials/, was-spaces/). Defaults to ~/.config/did-cli-wallet/ (honors XDG_CONFIG_HOME). |
| DIDS_DIR | did | DID-documents directory. Defaults to <WALLET_DIR>/dids/. |
| SECRET_KEY_SEED | key create, did create | Multibase-encoded seed for deterministic key/DID generation. Not supported with --type ecdsa or --type x25519. |
| WAS_DID | was | Default signing DID (or stored-DID handle) when --did is omitted. |
| WAS_SERVER_URL | was | Default WAS server base URL when --server (or --exchange for request-grant) is omitted. |
| ZCAP_CONTROLLER_KEY_SEED | zcap | Controller signing-key seed for delegating capabilities. |
File Permissions
Everything written under WALLET_DIR and DIDS_DIR -- keys, zcaps,
credentials, space records, DID documents, sidecars, and history logs -- is
created with mode 0600, owner read/write only. Directories are created with
mode 0700, since a DID document is named after its DID and the file names
alone reveal what you hold. Secret key material is stored as plaintext Multikey
documents, so the file mode is what keeps it off other local accounts.
Files written before this became the default are not migrated. To tighten an existing wallet directory:
chmod -R u=rwX,go= ~/.config/did-cli-walletThe capital X matters -- a plain chmod -R 600 would clear the traverse bit
on the directories and leave the wallet unreadable to you as well.
Key Management
Create a key pair
Generate a random Ed25519 key pair (ed25519 is the default type):
./di key createIf you'd like to also generate a secret key seed (to help deterministically
generate the same key pair in the future), pass in the --with-seed flag:
./di key create --with-seed
{
"secretKeySeed": "z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv",
"keyPair": {
"@context": "https://w3id.org/security/multikey/v1",
"type": "Multikey",
"publicKeyMultibase": "z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
"secretKeyMultibase": "zruzykbtvWUgV8Tp1LKVEuTmywLEa75qHsvWRVarVhdgHiCgiMYTSDXTavJVh47Cwes4mKgdAY5PTizbRvHXcA7XcLF"
}
}Generate a deterministic key pair by setting the SECRET_KEY_SEED environment
variable to a multibase-encoded seed (e.g. from @digitalcredentials/bnid):
SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv ./di key create
{
"@context": "https://w3id.org/security/multikey/v1",
"type": "Multikey",
"publicKeyMultibase": "z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
"secretKeyMultibase": "zruzykbtvWUgV8Tp1LKVEuTmywLEa75qHsvWRVarVhdgHiCgiMYTSDXTavJVh47Cwes4mKgdAY5PTizbRvHXcA7XcLF"
}Specify an explicit key type with --type (defaults to ed25519; supported:
ed25519, ecdsa, x25519, hmac, aes256):
SECRET_KEY_SEED=z1Aaj5A4UCsd... ./di key create --type ed25519Output is a JSON-LD Multikey document with both the public and secret key in multibase encoding:
{
"@context": "https://w3id.org/security/multikey/v1",
"type": "Multikey",
"publicKeyMultibase": "z6Mk...",
"secretKeyMultibase": "zrv..."
}Generate an ECDSA key with --type ecdsa. The curve is chosen with --curve
(defaults to p256; supported: p256, p384, p521, each also accepted in
hyphenated p-256 and SECG secp256r1 spellings, case-insensitively):
./di key create --type ecdsa --curve p384ECDSA keys are serialized as Multikey, the same as Ed25519. Note that ECDSA key
generation is non-deterministic (it cannot be derived from a seed), so
--with-seed and SECRET_KEY_SEED are not supported with --type ecdsa.
Generate an X25519 (Curve25519) key agreement key -- for Diffie-Hellman key
exchange / encryption, not signing -- with --type x25519:
./di key create --type x25519It is serialized as an X25519KeyAgreementKey2020 document with the public and
private key in multibase encoding:
{
"type": "X25519KeyAgreementKey2020",
"publicKeyMultibase": "z6LS...",
"privateKeyMultibase": "z3we..."
}Like ECDSA, X25519 key generation is non-deterministic, so --with-seed and
SECRET_KEY_SEED are not supported with --type x25519.
Generate a Sha256HmacKey2019 HMAC key -- a 32-byte symmetric secret used to
HMAC-blind EDV index attributes (
see Blinded indexing)
-- with --type hmac:
./di key create --type hmacIt is serialized with the secret carried as an oct JWK (it has no public
half), identified by a random urn:uuid: id:
{
"id": "urn:uuid:...",
"type": "Sha256HmacKey2019",
"secretKeyJwk": {
"kty": "oct",
"alg": "HS256",
"k": "..."
}
}HMAC key generation is non-deterministic, so --with-seed and SECRET_KEY_SEED
are not supported with --type hmac.
Generate a symmetric AES-256 key-encryption key (KEK) -- 32 random bytes carried
as a Multikey secretKeyMultibase -- with --type aes256:
./di key create --type aes256It is serialized as a Multikey with the secret carried as a base58btc
secretKeyMultibase (the AES-256 multicodec header 0xa2 0x01 followed by the
32 raw key bytes, no public half), identified by a urn:kek:sha256: id derived
from the SHA-256 digest of the raw key bytes:
{
"id": "urn:kek:sha256:...",
"type": "Multikey",
"secretKeyMultibase": "z..."
}This is the form Wallet Attached Storage servers accept for their at-rest
key-encryption-key configuration (KMS_RECORD_KEK / KMS_RECORD_KEKS), where
each KEK is identified by the same derived urn:kek:sha256: id. Generation is
always fresh and random, so --with-seed and SECRET_KEY_SEED are not
supported with --type aes256.
Save the key to local wallet storage (~/.config/did-cli-wallet/keys/ by
default, or
$WALLET_DIR/keys/ if set) with --save. A .meta.json metadata sidecar is
written next to the key, recording the creation timestamp; --handle (a short
tag for telling keys apart) and --description add user-defined metadata to
it (both require --save):
./di key create --save --handle issuer-signing --description 'Demo issuer signing key'
Key saved to /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....jsonList key pairs
List the key pairs saved in local wallet storage (via key create --save) as
a table of their metadata. The DIDS column shows the locally stored DIDs whose
documents reference the key, derived by scanning the saved DID documents:
./di key list
HANDLE TYPE CREATED FINGERPRINT DIDS DESCRIPTION
-------------- ------- ---------- ---------------------------- ------------------- -----------------------
issuer-signing ed25519 2026-06-10 z6MkrLBubwzwEv...MnCsdX2hM did:key:z6MkrL... Demo issuer signing keyIf no keys are stored, nothing is printed. Pass --json to output the list as
a JSON array of objects with metadata:
./di key list --json
[
{
"fingerprint": "z6Mkr...",
"storageId": "2026-06-10-ed25519-z6Mkr...",
"type": "ed25519",
"created": "2026-06-10T17:22:31.123Z",
"handle": "issuer-signing",
"description": "Demo issuer signing key",
"dids": ["did:key:z6Mkr..."]
}
]Or pass --plain to print just the fingerprints (multibase-encoded public
keys), one per line, sorted:
./di key list --plain
z6Mkr...
z6Mks...Show a key pair
Display a key saved in local wallet storage, looked up by its fingerprint
(publicKeyMultibase, as printed by key list) or by its metadata handle.
Only the public key object is shown -- the stored secret key is never included
in the output:
./di key show z6Mkr...
{
"@context": "https://w3id.org/security/multikey/v1",
"id": "...",
"type": "Multikey",
"controller": "...",
"publicKeyMultibase": "z6Mkr..."
}Aliases: view, cat.
Pass --meta to show the key's metadata instead of the public key object,
including the DIDs the key participates in (derived from the locally stored
DID documents):
./di key show issuer-signing --meta
FIELD VALUE
----------- ------------------------------------------------
Fingerprint z6Mkr...
Type ed25519
Created 2026-06-10T17:22:31.123Z
Handle issuer-signing
Description Demo issuer signing key
DIDs did:key:z6Mkr...--meta --json prints the same metadata as a JSON object.
Edit key metadata
Show or edit the metadata of a stored key with key meta (looked up by
fingerprint or handle). With no options it prints the current metadata; with
--handle / --description it updates the metadata sidecar (the key file
itself is never rewritten). Passing an empty string clears a field:
./di key meta z6Mkr... --handle issuer-signing --description 'Demo issuer signing key'
Metadata saved to /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....meta.json
{
"created": "2026-06-10T17:22:31.123Z",
"handle": "issuer-signing",
"description": "Demo issuer signing key"
}
./di key meta issuer-signing --description ''Keys saved before metadata support get a sidecar created on first edit, with
created backfilled from the date prefix of the key's file name.
Remove a key pair
Remove a stored key with key remove (aliases: delete, rm), looked up by
fingerprint or handle. Both the key file and its .meta.json metadata sidecar
are deleted:
./di key remove issuer-signing
Removed /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....json
Removed /home/user/.config/did-cli-wallet/keys/2026-06-10-ed25519-z6Mkr....meta.jsonDID Management
Create a DID
Generate a random Ed25519 did:key DID (method defaults to key):
./di did create
{
"id": "did:key:z6Mkr...",
"didDocument": { ... }
}Or pass the method explicitly:
./di did create keyBy default the DID's verification key is Ed25519. Pass --type ecdsa (with an
optional --curve, defaulting to p256) to mint a DID backed by an ECDSA key
instead. This works for both did:key and did:web:
./di did create key --type ecdsa --curve p384
./di did create web --type ecdsa --url https://example.comECDSA works for did create web --type ecdsa and did add-key --type ecdsa
too. Because ECDSA keys are not seed-derivable, --with-seed and
SECRET_KEY_SEED are not supported with --type ecdsa.
To also include the secret key seed in the output (useful for re-deriving the
same DID later), pass --with-seed:
./di did create --with-seed
{
"id": "did:key:z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
"secretKeySeed": "z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv",
"didDocument": {
"@context": [ ... ],
"id": "did:key:z6MkrLBubwzwEvwmsyEKd2kJ6pt91E6MHdf3EeQMnCsdX2hM",
"verificationMethod": [ ... ],
...
}
}Generate a deterministic DID by setting the SECRET_KEY_SEED environment
variable to a multibase-encoded seed (e.g. from @digitalcredentials/bnid):
SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv ./di did createSave the DID document and key material to local storage with --save
(written to ~/.config/did-cli-wallet/dids/ by default, or $DIDS_DIR if set).
A .meta.json
metadata sidecar is written next to the DID document, recording the creation
timestamp; --handle and --description add user-defined metadata to it
(both require --save):
./di did create --save --handle demo-issuer
DID saved to /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....json
{
"id": "did:key:z6Mkr...",
"didDocument": { ... }
}If the DID's verification key also exists in the local wallet (e.g. both were derived from the same seed), saving the DID records the association in that key's metadata sidecar as well.
Create a did:web DID
Generate a did:web DID. Unlike did:key, a did:web DID is tied to a domain,
so --url (the HTTPS url of the DID document) is required:
./di did create web --url https://example.com
{
"id": "did:web:example.com",
"didDocument": { ... }
}This generates a single Ed25519 verification key, wired into the
authentication, assertionMethod, capabilityDelegation, and
capabilityInvocation relationships. Additional keys can be added later.
As with did:key, pass --with-seed to include the secret key seed in the
output (useful for re-deriving the same DID later):
./di did create web --url https://example.com --with-seed
{
"id": "did:web:example.com",
"secretKeySeed": "z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv",
"didDocument": { ... }
}Or set the SECRET_KEY_SEED environment variable to a multibase-encoded seed to
generate the DID deterministically:
SECRET_KEY_SEED=z1AXVyT6G1Qk3E9cMPkDYY6wVRpZjVGWAZ3TfrAgFZkX6bv \
./di did create web --url https://example.comSave the DID document and key material to local storage with --save (written
to ~/.config/did-cli-wallet/dids/web/ by default, or $DIDS_DIR if set). The
key file is an
object keyed by verification method id, so further keys can be appended later:
./di did create web --url https://example.com --save
DID saved to /home/user/.config/did-cli-wallet/dids/web/did:web:example.com.json
{
"id": "did:web:example.com",
"didDocument": { ... }
}Add a key to a did:web DID
Add another verification key to an existing, locally stored did:web DID (the
DID must have been saved with did create web --save). The new key is
generated,
added to the DID document, and both the document and key file in storage are
updated in place:
./di did add-key did:web:example.com
DID saved to /home/user/.config/did-cli-wallet/dids/web/did:web:example.com.json
{
"id": "did:web:example.com",
"didDocument": { ... }
}By default the new key is wired into the authentication, assertionMethod,
capabilityDelegation, and capabilityInvocation relationships. Pass
--purpose (repeatable) to choose specific relationships:
./di did add-key did:web:example.com --purpose authentication --purpose assertionMethodBy default the new key is Ed25519; pass --type ecdsa (with an optional
--curve, defaulting to p256) to add an ECDSA key instead:
./di did add-key did:web:example.com --type ecdsa --curve p384Pass --type x25519 to add an X25519 (Curve25519) key agreement key. X25519
keys are encryption/key-exchange keys, not signing keys, so they are wired into
the keyAgreement relationship only -- a --purpose other than keyAgreement
is rejected:
./di did add-key did:web:example.com --type x25519For Ed25519 keys, the new key is derived from a seed (as with did create):
pass --with-seed to generate (and print) a fresh seed, or setSECRET_KEY_SEED
to derive the key deterministically. ECDSA and X25519 keys are not
seed-derivable, so --with-seed is not supported with --type ecdsa or
--type x25519:
./di did add-key did:web:example.com --with-seedCreate a did:webvh DID
Generate a did:webvh DID. Like did:web it is tied to a domain, so --url
(the HTTPS url that will host the DID's history log) is required:
./di did create webvh --url https://example.com
{
"id": "did:webvh:Qm...:example.com",
"didDocument": { ... }
}did:webvh separates two key roles: an update (authorization) key that
signs entries in the DID's append-only history log, and the document
verification key wired into the authentication, assertionMethod,
capabilityDelegation, and capabilityInvocation relationships. They are
distinct keys, so the update key can be rotated without ever disturbing the
document.
By default did:webvh arms key pre-rotation: the DID commits, in advance,
to the hash of the key allowed to perform the next update. A compromise of the
currently active update key cannot be used to seize the DID, because the
attacker still does not hold the pre-committed next key. So create generates
three keys: the active update key, a staged next update key (whose hash is
committed as nextKeyHashes), and the document key. Pass --no-prerotation to
create the DID without pre-rotation (no next key is staged):
./di did create webvh --url https://example.com --no-prerotationSave the DID document, history log, and key material to local storage with
--save (written to ~/.config/did-cli-wallet/dids/webvh/ by default, or
$DIDS_DIR if set). The document key is stored in <did>.keys.json; the update
keys (active, and the staged next key when pre-rotation is on) are stored in a
separate <did>.update-keys.json sidecar, and the signed history log in
<did>.jsonl:
./di did create webvh --url https://example.com --save
DID saved to /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:example.com.json
DID history log saved to /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:example.com.jsonl
Update keys saved to /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:example.com.update-keys.json
{
"id": "did:webvh:Qm...:example.com",
"didDocument": { ... }
}Only Ed25519 update keys are supported (the eddsa-jcs-2022 cryptosuite the
method uses requires them), so --type ecdsa is rejected.
A few more create-time options are recorded in the signed history-log
parameters:
--portable/--no-portable-- a portable DID (the default) can later be moved to a different domain;--no-portablepins it to its origin.--witness <did...>-- declare one or more witnessdid:keyDIDs authorized to co-sign the DID's log entries (repeatable).--witness-threshold <n>sets how many witness approvals are required (defaults to the number of witnesses; it requires--witness). This only declares the witnesses; actually generating witness proofs is not yet supported.--watcher <url...>-- declare one or more watcher URLs that monitor the DID's log (repeatable;https://, orhttp://localhostfor local testing).
./di did create webvh --url https://example.com \
--witness did:key:z6Mk... --witness did:key:z6Mk... --witness-threshold 1 \
--watcher https://watcher.example.com --saveRotate a did:webvh update key
Rotate the update (authorization) key of a locally stored did:webvh DID with
did webvh rotate-keys. This appends a new entry to the DID's history log and
never touches the document's verification methods (those are separate keys).
With no flags it advances the pre-rotation ratchet in one step: it reveals and activates the previously staged next key (signing the new entry with it), and stages a fresh next key for the following rotation. The retired update key's secret is deleted by default -- a retired key is only ever needed to verify historic log entries, which uses the public key from the log, not the secret:
./di did webvh rotate-keys did:webvh:Qm...:example.com
DID document saved to /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:example.com.json
DID history log saved to /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:example.com.jsonl
Update keys saved to /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:example.com.update-keys.json
Pre-rotation is armed: a next update key is staged.
{
"id": "did:webvh:Qm...:example.com",
"didDocument": { ... }
}Pre-rotation requires the staged key to sign its own activation, so the staged
secret in <did>.update-keys.json is what makes the next rotation possible --
losing it means the DID can never be updated again. Keep that sidecar backed up.
Flags:
--stop-prerotation-- rotate but commit no next-key hash; pre-rotation turns off after this entry.--enable-prerotation-- for a DID with pre-rotation currently off, turn it on by staging a next key. Alone it stages only (the active key is unchanged, signed by the current key).--update-key <multibase...>-- in ordinary (non-pre-rotation) mode, rotate to specific update key(s) by public key instead of generating a fresh one. Rejected while pre-rotation is armed, where the next keys are fixed by the prior commitment.--keep-old-key-- retain the retired update key's secret in the sidecar instead of dropping it.--with-seed-- emit the secret key seed of the new/next update key this rotation generates (the staged next key when pre-rotation stays armed, or the freshly generated active key in an ordinary rotation). HonorsSECRET_KEY_SEEDif set, otherwise generates a seed. Rejected for a rotation that generates no new key (a bare--stop-prerotationreveal, or rotating to an external--update-key).-y,--yes-- skip the confirmation prompt (rotation is hard to undo). Required when stdin is not interactive (scripts, cron), where the prompt cannot be asked.
Add or remove a service entry
Add or remove a service entry on a
locally stored did:web or did:webvh DID. The DID may be given as a full DID
or a metadata handle. For did:web this edits the stored document in place; for
did:webvh it appends a new entry to the history log.
./di did add-service did:web:example.com \
--id files --type LinkedDomains --endpoint https://example.com
{
"id": "did:web:example.com",
"didDocument": { ..., "service": [ ... ] }
}--id accepts a bare fragment (files), which is expanded to <did>#files, or
a full service id. The service type comes from --type (repeat for multiple
types). The endpoint comes from either --endpoint (repeat for multiple values;
a single value stays a string, several become an array) or --endpoint-json (a
raw JSON value, e.g. an object); exactly one of the two is required.
./di did add-service did:web:example.com \
--id dwn --type DecentralizedWebNode \
--endpoint-json '{"nodes":["https://dwn.example"]}'Remove a service by its id (a bare fragment is expanded the same way):
./di did remove-service did:web:example.com --id filesFor did:webvh, both commands sign and append a log entry (and so accept -y /
--yes to skip the confirmation prompt). The update keys and document
verification methods are carried forward unchanged -- with one exception: when
pre-rotation is armed the library requires the staged key to sign the entry, so
the update-key ratchet is advanced as part of the change (the staged key is
revealed and activated, and a fresh next key is staged), exactly as in
webvh rotate-keys. Pass --keep-old-key to retain the retired update key's
secret in that case.
List DIDs
List the DIDs saved in local storage (via did create --save) as a table of
their metadata:
./di did list
HANDLE METHOD CREATED DID DESCRIPTION
----------- ------ ---------- -------------------------------------------- -----------
demo-issuer key 2026-06-10 did:key:z6MkrLBubwzwEvwms...6MHdf3EeQMnCsdX2hMIf no DIDs are stored, nothing is printed. Pass --json to output the list as
a JSON array of objects with metadata:
./di did list --json
[
{
"did": "did:key:z6Mkr...",
"method": "key",
"created": "2026-06-10T17:22:31.123Z",
"handle": "demo-issuer"
}
]Or pass --plain to print just the DIDs, one per line, sorted:
./di did list --plain
did:key:z6Mkr...
did:key:z6Mks...Resolve a DID
Resolve a DID to its DID document through the security document loader. Unlike
did show (which reads local storage), did get resolves live: did:key is
resolved offline, did:web is fetched over HTTPS, and did:webvh is resolved by
fetching and verifying its history log over HTTPS. Pass a DID URL (a
did#fragment key id) to dereference straight to its verification method:
./di did get did:key:z6Mkr...
{
"@context": [ ... ],
"id": "did:key:z6Mkr...",
"verificationMethod": [ ... ],
...
}
./di did get did:key:z6Mkr...#z6Mkr...
{
"id": "did:key:z6Mkr...#z6Mkr...",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6Mkr...",
"publicKeyMultibase": "z6Mkr..."
}Alias: resolve.
Show a DID
Display the DID document saved in local storage (via did create --save),
looked up by DID or by its metadata handle. The stored DID document holds no
secret key material -- signing keys live in a separate key file -- so it is
printed as-is:
./di did show did:key:z6Mkr...
{
"@context": [ ... ],
"id": "did:key:z6Mkr...",
"verificationMethod": [ ... ],
...
}Aliases: view, cat.
For a did:webvh DID the document is resolved from its stored history log
(<did>.jsonl) -- the source of truth -- rather than the saved snapshot, so the
output reflects every appended log entry. If no log is stored the saved snapshot
is shown instead.
Pass --meta to show the DID's metadata instead of the DID document:
./di did show demo-issuer --meta
FIELD VALUE
----------- ----------------------------------------------
DID did:key:z6Mkr...
Method key
Handle demo-issuer
Created 2026-06-10T17:22:31.123Z
Description
Keys 1For a did:webvh DID, --meta also reports the parameters resolved from the
log -- the current version id, last-updated time, portability, pre-rotation,
deactivation status, and the number of update keys, witnesses, and watchers:
./di did show my-webvh --meta
FIELD VALUE
----------- ----------------------------------------------
DID did:webvh:Qm...:example.com
Method webvh
...
Version 1-Qm...
Updated 2026-06-10T17:22:31Z
Portable yes
Prerotation yes
Deactivated no
Update keys 1
Witnesses 0
Watchers 0--meta --json prints the same metadata as a JSON object (the did:webvh
fields are included there too).
Edit DID metadata
Show or edit the metadata of a stored DID with did meta (looked up by DID or
handle). With no options it prints the current metadata (to stdout) and lists
where the DID's artifacts live on disk (to stderr) -- the DID document, keys
file, metadata sidecar, and for did:webvh the history log and update-keys
sidecar:
./di did meta did:webvh:Qm...:example.com:space:abc:did
Location:
document: /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:did.json
keys: /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:did.keys.json
metadata: /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:did.meta.json
log: /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:did.jsonl
update-keys: /home/user/.config/did-cli-wallet/dids/webvh/did:webvh:Qm...:did.update-keys.json
{
"created": "2026-06-10T17:22:31.123Z"
}Add --json for a machine-readable object that folds the locations in as a
files map alongside the metadata:
./di did meta did:webvh:Qm...:did --json
{
"metadata": { "created": "..." },
"files": {
"document": ".../did:webvh:Qm...:did.json",
"keys": ".../did:webvh:Qm...:did.keys.json",
"metadata": ".../did:webvh:Qm...:did.meta.json",
"log": ".../did:webvh:Qm...:did.jsonl",
"updateKeys": ".../did:webvh:Qm...:did.update-keys.json"
}
}With --handle / --description it updates the metadata sidecar (the DID
document itself is never rewritten). Passing an empty string clears a field:
./di did meta did:key:z6Mkr... --handle demo-issuer --description 'Issuer DID for the demo'
Metadata saved to /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....meta.json
{
"created": "2026-06-10T17:22:31.123Z",
"handle": "demo-issuer",
"description": "Issuer DID for the demo"
}Remove a DID
Remove a stored DID with did remove (aliases: delete, rm), looked up by
DID or handle. The DID document, its .keys.json key file, and its
.meta.json metadata sidecar are all deleted, and the DID is scrubbed from
the cached dids associations of any matching wallet keys:
./di did remove demo-issuer
Removed /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....json
Removed /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....keys.json
Removed /home/user/.config/did-cli-wallet/dids/key/did:key:z6Mkr....meta.jsonVerifiable Credentials
Verify a credential
Run full verification on a Verifiable Credential (JSON). Beyond the
cryptographic signature check, this also verifies expiration, revocation /
status, and whether the issuer DID is recognized in any trusted registry
(via @interop/verifier-core and @digitalcredentials/issuer-registry-client).
The credential is read from a file argument, an http(s) URL, or, if neither is given, from stdin:
./di vc verify credential.json
./di vc verify https://example.com/credentials/123.json
cat credential.json | ./di vc verifyBy default it prints the full @interop/verifier-core verification result
(top-level verified, a per-suite summary, and the flat results of every
check). Pass --summary for a compact, human-friendly object instead:
./di vc verify credential.json --summary
{
"verified": true,
"checks": {
"signature": true,
"revoked": false,
"issuerRecognized": true
},
"matchingIssuers": [ ... ]
}A check is omitted from checks when it was skipped (for example expired is
absent when the credential has no expiration date).
The exit code is scriptable: 0 when the credential verified, 1 when it did
not, and 2 on a read/parse error or a structurally malformed credential.
The trusted registry list is fetched from the DCC known-did-registries at runtime, falling back to a bundled list of DCC registries when the network is unavailable.
Issue a credential
Issue (sign) an unsigned Verifiable Credential with a locally-stored DID, acting as a command-line wallet and issuer. The credential is read from a file argument, an http(s) URL, or, if neither is given, from stdin, and the issued credential is printed to stdout. If the input already carries a proof, issuing appends an additional one.
The DID to issue with is required (--did); it must have been saved locally (
see
di did create --save):
./di vc issue credential.json --did did:key:z6Mk...
cat credential.json | ./di vc issue --did did:key:z6Mk...The credential's issuer is set to the signing DID when the input has none.
When the input already names an issuer, it must match the signing DID,
otherwise issuance is aborted -- a credential cannot be issued by a DID other
than the one named as its issuer.
By default the first key in the DID's assertionMethod relationship is used.
Pass --key to choose a specific verification method; it must be authorized by
the DID's assertionMethod array, otherwise issuance fails:
./di vc issue credential.json --did did:key:z6Mk... --key did:key:z6Mk...#z6Mk...The signature suite defaults to the signing key's type. An Ed25519 DID signs
with eddsa-rdfc-2022 (a W3C Data Integrity proof) by default; pass
--suite Ed25519Signature2020 for the classic Ed25519Signature2020 proof:
./di vc issue credential.json --did did:key:z6Mk... --suite Ed25519Signature2020An ECDSA DID (see did create --type ecdsa) signs with ecdsa-rdfc-2019. The
suite is selected automatically from the key, so no --suite flag is needed:
./di vc issue credential.json --did did:key:zDna...Only the P-256 and P-384 curves can issue credentials -- the ecdsa-rdfc-2019
cryptosuite does not support P-521 (key creation warns about this). A suite that
does not match the key type (e.g. --suite eddsa-rdfc-2022 for an ECDSA key) is
rejected. ECDSA credentials round-trip through vc verify (below).
Pass --save to also store the issued credential in local wallet storage
(~/.config/did-cli-wallet/credentials/ by default, or $WALLET_DIR if set);
--save
records the creation timestamp in a .meta.json metadata sidecar, and
--handle / --description (which require --save) tag the saved credential
the same way zcap create --save does:
./di vc issue credential.json --did did:key:z6Mk... --save --handle alumni
Credential saved to /home/user/.config/did-cli-wallet/credentials/sha256-1f4a....jsonThe exit code is scriptable: 0 when the credential was issued, 1 on an
issuance error (an unauthorized key, an unknown suite, a missing DID / key
file, or an issuer that does not match the signing DID), and 2 on a
read/parse error.
Import a credential
Store an existing Verifiable Credential in local wallet storage with
vc import. The credential is read from a file argument, an http(s) URL, or,
if neither is given, from stdin. The input must structurally look like a
credential (its type must include VerifiableCredential); it is stored
as-is and is not verified on import (run vc verify for that). --handle /
--description tag the saved credential:
./di vc import credential.json --handle alumni --description 'Alumni credential'
./di vc import https://example.com/credentials/123.json
cat credential.json | ./di vc import
Credential saved to /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d....jsonThe credential file is named after the credential's id; a credential
without an id (the property is optional) is stored under a digest of its
content, so re-importing it overwrites rather than duplicates. Re-importing a
credential preserves the metadata its sidecar already carries.
The exit code is scriptable: 0 when the credential was imported, 1 when
the input is not a Verifiable Credential, and 2 on a fetch/read/parse error.
List credentials
List the credentials saved in local wallet storage (via vc import or
vc issue --save) as a table of their metadata. The TYPE column shows the
credential's most specific type (its first type entry other than the
generic VerifiableCredential):
./di vc list
HANDLE TYPE ISSUER CREATED ID DESCRIPTION
------ ------------------- ------------------------------ ---------- --------------------------- -----------------
alumni OpenBadgeCredential did:key:z6MkExa...ampleIssuer 2026-06-11 urn:uuid:9b1deb4d-3b7d-4ba8 Alumni credentialIf no credentials are stored, nothing is printed. Pass --json to output the
list as a JSON array of objects with metadata:
./di vc list --json
[
{
"id": "urn:uuid:9b1deb4d-3b7d-4ba8",
"type": "OpenBadgeCredential",
"issuer": "did:key:z6MkExampleIssuer",
"created": "2026-06-11T17:22:31.123Z",
"handle": "alumni",
"description": "Alumni credential"
}
]Or pass --plain to print just the credential ids, one per line, sorted. A
credential without an id is listed by its storage id (the sha256-... file
name), which show / meta / remove accept in place of a credential id.
Show a credential
Display a credential saved in local wallet storage, looked up by its
credential id (as printed by vc list), its storage id, or its metadata
handle:
./di vc show alumni
{
"@context": ["https://www.w3.org/ns/credentials/v2"],
"id": "urn:uuid:9b1deb4d-3b7d-4ba8",
"type": ["VerifiableCredential", "OpenBadgeCredential"],
...
}Aliases: view, cat.
Pass --meta to show the credential's metadata instead, along with its
issuer, validity start, and expiration:
./di vc show alumni --meta
FIELD VALUE
----------- ----------------------------
ID urn:uuid:9b1deb4d-3b7d-4ba8
Type OpenBadgeCredential
Handle alumni
Created 2026-06-11T17:22:31.123Z
Description Alumni credential
Issuer did:key:z6MkExampleIssuer
Valid From 2026-01-01T00:00:00Z
Expires--meta --json prints the same metadata as a JSON object.
Edit credential metadata
Show or edit the metadata of a stored credential with vc meta (looked up by
credential id, storage id, or handle). With no options it prints the current
metadata; with --handle / --description it updates the metadata sidecar
(the stored credential itself is never rewritten). Passing an empty string
clears a field:
./di vc meta urn:uuid:9b1deb4d-3b7d-4ba8 \
--handle alumni --description 'Alumni credential'
Metadata saved to /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d-3b7d-4ba8.meta.json
{
"created": "2026-06-11T17:22:31.123Z",
"handle": "alumni",
"description": "Alumni credential"
}
./di vc meta alumni --description ''Remove a credential
Remove a stored credential with vc remove (aliases: delete, rm), looked
up by credential id, storage id, or handle. Both the credential file and its
.meta.json metadata sidecar are deleted:
./di vc remove alumni
Removed /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d-3b7d-4ba8.json
Removed /home/user/.config/did-cli-wallet/credentials/urn_uuid_9b1deb4d-3b7d-4ba8.meta.jsonAuthorization Capabilities (zCaps)
An Authorization Capability (zCap) grants its
controller permission to invoke an action against a resource (the
invocationTarget). Authority starts at an unsigned root capability and is
handed down a chain of signed delegated capabilities, each one optionally
narrowing the allowed actions or the target.
Both commands print the capability as JSON together with an encoded field --
the capability serialized and base64url-encoded with a multibase u prefix --
which is the compact form you pass to zcap delegate --capability to delegate
it
further. Pass --save to also write the capability to local wallet storage
(~/.config/did-cli-wallet/zcaps/ by default, or $WALLET_DIR if set);--save
records the
creation timestamp in a .meta.json metadata sidecar, and --handle /
--description (which require --save) tag the saved capability the same way
key create --save and did create --save do. The exit code is 0 on
success and 1 on a creation / delegation or input error.
Create a root capability
Build the root capability for an invocation target. The --controller is the
DID
that holds root authority over the target, and --url is theinvocationTarget.
Root capabilities are unsigned, so no key is needed:
./di zcap create \
--controller did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR \
--url https://example.com/api
{
"rootCapability": {
"@context": "https://w3id.org/zcap/v1",
"id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi",
"controller": "did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR",
"invocationTarget": "https://example.com/api"
},
"encoded": "ueyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvemNhcC92MSIsImlkIjoi..."
}The root capability's id is always
urn:zcap:root:<url-encoded invocationTarget>,
and a root capability grants all actions (it has no allowedAction).
Note about the encoded field
The multibase- (that's the u prefix) and base64url-encoded JSON of the zcap
is returned, for convenience, in the encoded field.
This is done for easier "double-click to copy" and pasting into other tools, such as password managers, server env secrets, etc.
Delegate a capability
Delegate authority to another DID (--delegatee, which becomes the delegated
capability's controller). The delegation is signed with the delegator's
capabilityDelegation key, sourced one of two ways:
- A locally-stored DID (
--did) -- the DID must have been saved withdi did create --save; this is the preferred mode and mirrorsvc issue. - A secret key seed (
ZCAP_CONTROLLER_KEY_SEED+--controller) -- thedid:keyis re-derived from the seed and checked against--controller.
To delegate from the root capability for a target, pass --url (the same
invocationTarget the root was created for) and the action(s) to allow with
--allow (repeatable; if omitted the delegatee inherits the parent's actions):
./di zcap delegate \
--did did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR \
--delegatee did:key:z6MknBxrctS4KsfiBsEaXsfnrnfNYTvDjVpLYYUAN6PX2EfG \
--url https://example.com/documents \
--allow read
{
"delegatedCapability": {
"@context": [
"https://w3id.org/zcap/v1",
"https://w3id.org/security/suites/ed25519-2020/v1"
],
"id": "urn:uuid:e03d4f97-2e70-42e8-ae5d-51e92e903afa",
"controller": "did:key:z6MknBxrctS4KsfiBsEaXsfnrnfNYTvDjVpLYYUAN6PX2EfG",
"parentCapability": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fdocuments",
"invocationTarget": "https://example.com/documents",
"expires": "2027-06-07T17:30:00Z",
"allowedAction": ["read"],
"proof": {
"type": "Ed25519Signature2020",
"created": "2026-06-07T17:30:00Z",
"verificationMethod": "did:key:z6Mkfeco...#z6Mkfeco...",
"proofPurpose": "capabilityDelegation",
"capabilityChain": ["urn:zcap:root:https%3A%2F%2Fexample.com%2Fdocuments"],
"proofValue": "z5tuwwdJE6VXLhf1v8SNAquBmMcJCD7zJ4bXDi6rh1Fk..."
}
},
"encoded": "ueyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3pjYXAvdjEi..."
}The same delegation, signed via a secret key seed instead of a stored DID:
ZCAP_CONTROLLER_KEY_SEED=z1AZK4h5w5YZkKYEgqtcFfvSbWQ3tZ3ZFgmLsXMZsTVoeK7 \
./di zcap delegate \
--controller did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR \
--delegatee did:key:z6MknBxrctS4KsfiBsEaXsfnrnfNYTvDjVpLYYUAN6PX2EfG \
--url https://example.com/documents \
--allow readTo delegate an existing capability further down the chain, pass it as
--capability instead of --url -- the encoded string from a previous
delegation, a path to a JSON file containing the capability, or the id or
metadata handle of a zcap saved in local wallet storage. Use
--invocation-target to attenuate (narrow) the parent's target to a sub-path:
./di zcap delegate \
--did did:key:z6MknBxr... \
--delegatee did:key:z6Mks... \
--capability ueyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3pjYXAvdjEi... \
--invocation-target https://example.com/documents/reports \
--allow readThe delegated capability expires after --ttl (a duration such as 1y, 30d,
24h, 15m; default 1y). Pass --expires with an explicit ISO 8601 date to
override it:
./di zcap delegate --did did:key:z6Mk... --delegatee did:key:z6Mkn... \
--url https://example.com/documents --allow read --ttl 30d
./di zcap delegate --did did:key:z6Mk... --delegatee did:key:z6Mkn... \
--url https://example.com/documents --allow read --expires 2027-01-01T00:00:00ZList capabilities
List the capabilities saved in local wallet storage (via zcap create --save
or zcap delegate --save) as a table of their metadata. The TYPE column shows
whether the capability is a root or a delegated one:
./di zcap list
HANDLE TYPE CREATED ID DESCRIPTION
-------- ---- ---------- -------------------------------------------- -------------
api-root root 2026-06-11 urn:zcap:root:https%3...%2Fexample.com%2Fapi Demo API rootIf no capabilities are stored, nothing is printed. Pass --json to output the
list as a JSON array of objects with metadata:
./di zcap list --json
[
{
"id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi",
"type": "root",
"created": "2026-06-11T17:22:31.123Z",
"handle": "api-root",
"description": "Demo API root"
}
]Or pass --plain to print just the capability ids, one per line, sorted:
./di zcap list --plain
urn:zcap:root:https%3A%2F%2Fexample.com%2Fa
urn:zcap:root:https%3A%2F%2Fexample.com%2FbShow a capability
Display a capability saved in local wallet storage, looked up by its
capability id (as printed by zcap list) or by its metadata handle:
./di zcap show api-root
{
"@context": "https://w3id.org/zcap/v1",
"id": "urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi",
"controller": "did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR",
"invocationTarget": "https://example.com/api"
}Aliases: view, cat.
Pass --meta to show the capability's metadata instead, along with its
controller, invocation target, and (for delegated capabilities) expiration:
./di zcap show api-root --meta
FIELD VALUE
----------- --------------------------------------------------------
ID urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi
Type root
Handle api-root
Created 2026-06-11T17:22:31.123Z
Description Demo API root
Controller did:key:z6Mkfeco2NSEPeFV3DkjNSabaCza1EoS3CmqLb1eJ5BriiaR
Target https://example.com/api
Expires--meta --json prints the same metadata as a JSON object.
Edit capability metadata
Show or edit the metadata of a stored capability with zcap meta (looked up
by capability id or handle). With no options it prints the current metadata;
with --handle / --description it updates the metadata sidecar (the stored
capability itself is never rewritten). Passing an empty string clears a field:
./di zcap meta urn:zcap:root:https%3A%2F%2Fexample.com%2Fapi \
--handle api-root --description 'Demo API root'
Metadata saved to /home/user/.config/did-cli-wallet/zcaps/urn_zcap_root_https_3A_2F_2Fexample.com_2Fapi.meta.json
{
"created": "2026-06-11T17:22:31.123Z",
"handle": "api-root",
"description": "Demo API root"
}
./di zcap meta api-root --description ''Remove a capability
Remove a stored capability with zcap remove (aliases: delete, rm),
looked up by capability id or handle. Both the capability file and its
.meta.json metadata sidecar are deleted:
./di zcap remove api-root
Removed /home/user/.config/did-cli-wallet/zcaps/urn_zcap_root_https_3A_2F_2Fexample.com_2Fapi.json
Removed /home/user/.config/did-cli-wallet/zcaps/urn_zcap_root_https_3A_2F_2Fexample.com_2Fapi.meta.jsonNote that removing a capability from local storage does not revoke it -- a
delegated capability that has already been handed to its delegatee remains
valid until it expires (see --ttl / --expires).
Wallet Attached Storage (WAS)
The was command group is a client for
Wallet Attached Storage
servers, which organize content as Space > Collection > Resource behind
zcap-authorized HTTP. Every request is signed with a did:key DID stored in
the local wallet (saved with did create --save; Ed25519 keys only for now).
Commands address content with a single positional WAS path:
SPACE[/COLLECTION[/RESOURCE]]where SPACE is one of:
- a registry handle (e.g.
home) of a space registered in the local wallet (~/.config/did-cli-wallet/was-spaces/), which also supplies the server URL and signing DID defaults; - a bare space id (a server-generated uuid or urn), combined with
--server/WAS_SERVER_URL; - a full space URL (e.g.
https://was.example/space/8124...cf2e), which is self-contained -- the server URL is its origin. Collection and resource segments can be appended to any of the three forms.
The signing DID resolves from --did (a DID or stored-DID handle), the
WAS_DID environment variable, or the controller recorded in the registry
entry. Exit codes: 0 success, 1 operation error (a typed WAS error or a
not-found/not-visible read -- the spec returns 404 for both), 2 input error
(bad path, unknown handle/DID, missing server URL).
Create a space
Create a space on a WAS server (--name, a display name, is optional). Pass
--save to register it in the local wallet, with the usual --handle /
--description metadata; the handle is what makes every later command short:
./di was space create --name 'Home space' \
--server http://localhost:3002 --did did:key:z6Mkfeco... \
--save --handle home
Space registered in /home/user/.config/did-cli-wallet/was-spaces/81246131-69a4-45ab-9bff-9c946b59cf2e.json
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"url": "http://localhost:3002/space/81246131-69a4-45ab-9bff-9c946b59cf2e",
"name": "Home space",
"controller": "did:key:z6Mkfeco..."
}Without --save, address the space later by its full URL (or register it
afterwards with was space add).
List, show, and manage spaces
was space list lists the locally registered spaces (WAS servers do not
implement server-side space listing yet; --remote asks anyway and surfaces
the 501). --json and --plain work as in the other list commands:
./di was space list
HANDLE NAME SPACE ID SERVER CREATED
------ ---------- ------------------------------------ --------------------- ----------
home Home space 81246131-69a4-45ab-9bff-9c946b59cf2e http://localhost:3002 2026-06-11was space show (aliases: view, cat) prints the Space Description from
the server, or the local registry record with --meta:
./di was space show home
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"type": ["Space"],
"name": "Home space",
"controller": "did:key:z6Mkfeco..."
}was space update (alias: configure) upserts description fields
(--name), also refreshing the registry entry. was space add registers an
existing remote space (a full space URL, or a bare id plus --server) in
the local registry, verifying it with a describe first. was space meta
<space> updates only a registered space's local metadata (--handle and/or
--description); the server-side space is untouched, and passing an empty
string (--handle '') clears that field. The local/remote delete pair:
was space delete <space>(alias:rm) deletes the space on the server (idempotent) and removes the registry entry;was space forget <space>removes only the local registry entry.
was space backends lists the storage backends available within a space, and
was space quotas shows the space's storage report grouped by backend (usage,
limit, and any restricted actions). Both render a table by default and take
--json for the raw response:
./di was space backends home
ID NAME MANAGED BY STORAGE MODE PERSISTENCE
default Filesystem server document, blob durable
./di was space quotas home
BACKEND STATE USAGE (B) LIMIT (B) RESTRICTED
default (Filesystem) ok 2048 1048576A server that does not implement these endpoints (a 501) is reported as an error.
Manage collections
The collection group (alias: coll) manages collections within a space.
create takes a space address plus an optional --name and --id (the id
is server-generated otherwise); show/update/delete take a
SPACE/COLLECTION path:
./di was collection create home --name Credentials --id credentials
{
"id": "credentials",
"url": "http://localhost:3002/space/8124...cf2e/credentials",
"name": "Credentials"
}
./di was collection list home
ID NAME URL
----------- ----------- ---------------------------------------------------
credentials Credentials http://localhost:3002/space/8124...cf2e/credentials
./di was collection delete home/credentials
Deleted http://localhost:3002/space/8124...cf2e/credentials on the server.was collection backend shows the storage backend a collection is stored on,
and was collection quota shows the collection's storage usage scoped to that
backend (state, usage, limit, and any restricted actions). Both render a table
by default and take --json for the raw response:
./di was collection backend home/credentials
FIELD VALUE
------------ --------------
ID default
Name Filesystem
Managed By server
Storage Mode document, blob
Persistence durable
./di was collection quota home/credentials
FIELD VALUE
----------- --------------------
Backend default (Filesystem)
Managed By server
State ok
Usage (B) 2048
Limit (B) 1048576
Restricted
Measured At 2026-06-13T00:00:00ZA missing or not-visible collection is reported as not-found; a server (or backend) that does not implement these endpoints (a 501) is reported as an error.
Add and read resources
The resource group (alias: res) manages the content itself. Payloads come
from a file argument or stdin: *.json files (and any input that parses to a
JSON object or array) are sent as JSON, anything else as binary
application/octet-stream, and an explicit --content-type sends the bytes
as-is with that type (useful for e.g. application/ld+json or images).
add posts to a collection and lets the server pick the resource id; put
creates or replaces at a known id:
./di was resource add home/credentials vc.json
{
"id": "d3c9...",
"url": "http://localhost:3002/space/8124...cf2e/credentials/d3c9...",
"contentType": "application/json"
}
./di was resource put home/credentials/vc-1 vc.json
cat vc.json | ./di was resource put home/credentials/vc-1
./di was resource put home/photos/pic-1 photo.png --content-type image/pngget pretty-prints JSON to stdout and writes binary raw (use --output for
files); a missing or not-visible resource prints
Not found (or not visible to you): <url> and exits 1:
./di was resource get home/credentials/vc-1
{
"name": "Alice"
}
./di was resource get home/photos/pic-1 --output photo.pnglist renders the resources of a collection (ID | CONTENT TYPE | URL), and
delete (alias: rm) removes one (idempotent).
Resource metadata (name and tags)
The resource-meta group (alias: meta) reads and updates a resource's
metadata. get prints the whole metadata object -- the server-managed
contentType, size, and timestamps plus the user-writable custom (its
name and tags):
./di was resource-meta get home/credentials/vc-1put updates the user-writable custom. --name sets the display name shown
in collection listings and --tag key=value (repeatable) sets annotations;
used on their own each is non-destructive -- --name preserves existing tags
and --tag preserves the existing name:
./di was resource-meta put home/credentials/vc-1 --name 'Diploma'
./di was resource-meta put home/credentials/vc-1 --tag year=2026 --tag status=verifiedGiving both --name and --tag together replaces custom wholesale (any
field you do not pass is cleared). The --json escape hatch takes the full
custom object as inline JSON or a JSON file path, for the same full
replacement (pass --json '{}' to clear everything):
./di was resource-meta put home/credentials/vc-1 \
--json '{"name":"Diploma","tags":{"year":"2026"}}'
./di was resource-meta put home/credentials/vc-1 --json custom.jsonAfter a successful update the command prints the resulting metadata.
Shorthand verbs
For day-to-day use, the top-level verbs dispatch on the path depth:
./di was ls home # collections of a space
./di was ls home/credentials # resources of a collection
./di was get home/credentials/vc-1 # = resource get
./di was put home/credentials/vc-1 vc.json # = resource put
./di was rm home/credentials/vc-1 # delete whatever the path points at
./di was rm home # ... including a whole spaceDelegate access (grant)
was grant delegates access to a space, collection, or resource. Actions are
HTTP verbs (GET, PUT, POST, DELETE; lowercase accepted), expiration
comes from --ttl (default 1y) or an explicit --expires, and the output
is the signed capability plus its encoded multibase form -- the same shape
as zcap delegate. --save (with --handle / --description) stores it in
the zcap store (~/.config/did-cli-wallet/zcaps/):
./di was grant home/credentials --to did:key:z6MkBob... --action GET PUT
{
"delegatedCapability": {
"@context": [...],
"id": "urn:uuid:e03d4f97-...",
"controller": "did:key:z6MkBob...",
"invocationTarget": "http://localhost:3002/space/8124...cf2e/credentials",
"allowedAction": ["GET", "PUT"],
"expires": "2027-06-11T17:30:00Z",
"proof": { ... }
},
"encoded": "ueyJAY29udGV4dCI6..."
}Hand the encoded string (or the JSON) to the delegatee out-of-band.
Request a grant
was request-grant is the inverse of was grant, for the case where you are
the one who needs access. It asks a user's wallet to delegate a capability on
one of its public collections, and is what lets a program publish into someone
else's storage without ever logging into their wallet.
The grantee key is minted by the command rather than passed in, so a script (or
an LLM agent) driving the CLI never handles key material: the secret goes
straight into local DID storage, and stdout carries only the capability. The
approval link and the progress notes go to stderr; --json puts the DID, the
handle, the link, and the capability into one object on stdout instead.
./di was request-grant --exchange https://freewallet.cloud/spaces/ \
--reason "Publish a demo page" --name "demo-publisher"
Requesting "web" access for did:key:z6MkAgent...
Open this in your wallet to approve:
https://freewallet.cloud/workflows/ephemeral/exchanges/abc-123/protocols?iuv=1
Waiting for approval...If you know which wallet the user runs, --wallet <url> also prints a link
that opens it straight onto the approval page for this request:
./di was request-grant --exchange https://freewallet.cloud/spaces/ --wallet https://freewallet.me
...
Or open your wallet directly:
https://freewallet.me/#/external/request?url=https%3A%2F%2Ffreewallet.cloud%2F...The interaction URL is still printed beside it, since that is what a wallet on another device can be pointed at.
The user opens that link in their wallet and approves. The command then prints
the capability it received (the same u... form --capability accepts) and
files it in the zcap store under --handle (default agent), together with the
minted key:
Capability saved to ~/.config/did-cli-wallet/zcaps/urn_uuid_....json
Granted. Use it with --capability agent, for example:
di was put ./index.html --capability agent --did agent --resource index.html --content-type text/html
ueyJAY29udGV4dCI6...Saving is what makes the grant usable: --capability resolves its signing key
out of the local DID store, so a key held only for the run cannot sign the write
that follows. --no-save prints the capability without keeping either half, for
inspecting a grant you do not intend to use.
The signing DID is taken from --did first, then WAS_DID, and only then the
capability's own controller. Naming the minted key explicitly
(--did agent, matching the handle the grant was filed under) is therefore
worth doing whenever WAS_DID is set in the environment, or the write is signed
by your own DID and the server rejects it.
A run files the minted key an
