Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
# Generated files from CLI commands
keypair.json
didKeyPairs.json
wellknown.json
credentialStatus.json
signed_vc.json
raw_vc.json
# Generated files from CLI commands, dropped in the working directory by their default
# output paths. Anchored to the repo root: unanchored, these names match at ANY depth and
# would also swallow identically-named files elsewhere in the tree.
/keypair.json
/didKeyPairs.json
/wellknown.json
/credentialStatus.json
/signed_vc.json
/signed_vp.json
/raw_vc.json

# Verifiable Presentation manual-test fixtures — generated, deliberately not committed.
# They carry throwaway private keys, and every credential and presentation is bound to the
# one holder key pair, so the set is only coherent as a whole; there is no regenerating
# part of it. Only generate-fixtures.cjs and README.md are tracked. Recreate with:
# node tests/fixtures/vp/generate-fixtures.cjs
# npx prettier --write "tests/fixtures/vp/**/*.json"
tests/fixtures/vp/*.json
tests/fixtures/vp/credentials/
tests/fixtures/vp/invalid-credentials/
tests/fixtures/vp/invalid-keypairs/
tests/fixtures/vp/presentations/

# Dependencies
node_modules/
Expand Down
170 changes: 170 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# CLAUDE.md

Guidance for working in this repo — for human developers and for Claude Code.

> **Keep this file alive.** It's only useful if it stays true. Treat it as part of the
> code: when a change makes something here wrong or incomplete, update it *in the same
> commit/PR*. See [Maintaining this file](#maintaining-this-file).

## What this repo is

`@trustvc/trustvc-cli` is the **interactive command-line front end** to
[`@trustvc/trustvc`](https://github.com/TrustVC/trustvc). It owns no cryptography and no
chain logic — every command is prompts + file I/O around a library call. When something is
wrong with a signature, a proof or a verification result, the bug is almost always upstream
in `trustvc` (or `@trustvc/w3c-vc` below it), not here.

```text
src/main.ts yargs entry point; auto-registers every command under src/commands/
src/commands/ one file per command — see "Adding a command"
src/utils/ file I/O, networks, wallets, formatting, prompts (barrel: utils/index.ts)
src/types.ts shared input types for command handlers
tests/fixtures/ documents to run commands against
```

Each command file exports `command`, `describe` and `handler`; `main.ts` picks them up with
`commandDir(..., { recurse: true })`. A nested folder becomes a **command group** only if it
has an `index.ts` declaring one (`wallet <method>`, `document-store <method>`); otherwise the
files register flat, which is why `src/commands/w3c/sign.ts` is `trustvc w3c-sign`.

## Commands

Node **≥ 22** — enforced at runtime in `main.ts`, and the install fails below it.
Use `nvm use 22`.

```bash
npm run build # tsup -> dist/ (run before testing the real CLI)
node dist/main.js <cmd> # run a command; `npm link` if you want a global `trustvc`
npm test # vitest --run
npm run lint # eslint, --max-warnings=0
npm run format:check # prettier, same set CI checks

npx vitest --run tests/commands/w3c/vp-sign.test.ts # one file
npx vitest --run <file> -t "does not match the holder" # one test
```

**Before "done": `npm run lint` AND `npm run format:check`.** CI runs lint → format:check →
test → build, and `lint` is `--max-warnings=0`, so a single warning is a red build.

**`tsc --noEmit` is NOT a gate and will never be clean** — `node_modules/@tradetrust-tt/
token-registry-v4` ships `.ts` sources that don't compile against ethers v6, and several
`src/commands/**` files have pre-existing ethers v5/v6 signature mismatches. To check your
own work, filter: `npx tsc --noEmit -p tsconfig.json 2>&1 | grep <your-file>`.

**Some tests hit the network** (did:web resolution, StatusList fetches, RPC). They're real
integration checks — don't mock them away.

## Testing an interactive command for real

Every command is prompt-driven, so piping stdin does **not** work — inquirer needs a TTY and
exits with `User force closed the prompt`. Use `expect`:

```tcl
set timeout 90
spawn node dist/main.js vp-sign
expect "path(s) to individual JSON file"
send "tests/fixtures/vp/credentials\r"
expect "key-pair JSON file"
send "tests/fixtures/vp/didKeyPairs.json\r"
expect eof
```

Strip the ANSI redraw noise from the output: `| perl -pe 's/\e\[[0-9;?]*[a-zA-Z]//g; s/\r//g'`.
Keep answers short — long absolute paths make the prompt line-wrap, and an `expect` pattern
that straddles the wrap never matches, which looks like a hang.

## Verifiable Presentations

`vp-sign` creates and signs a presentation; `verify` verifies it, along with every other
document type. There is deliberately **no `vp-verify`** — one verify command for everything.

- **The credentials prompt takes a directory**, a file, or comma-separated files. A directory
presents **every file in it**, unfiltered: anything that isn't a presentable credential is
reported by the signing step, which names the file (`nameFailingCredential` rewrites
trustvc's "credential at index 2" using the paths). Dot-files and sub-directories are
skipped — OS noise, never a credential.
- **The holder DID is not prompted.** trustvc enforces that the signing key's DID *is* the
holder, so any other answer could only fail. It's read from the key pair and printed. A key
pair with no `controller` (the bare `keypair.json` from `key-pair-generation`) is rejected
up front — presentations need the `didKeyPairs.json` that `did-web` writes.
- **No challenge support.** An anti-replay challenge can only be checked by the verifier that
issued it, and `verify` has no way to take one, so every presentation gets an
`assertionMethod` proof.
- A valid presentation prints one extra line — `N embedded credentials verified.` — because
the three fragment lines read identically over one credential or five. Failures keep the
plain three-line output.

`verify` routes on shape via `isVerifiablePresentation()` (type includes
`VerifiablePresentation` **and** a `verifiableCredential` field). It deliberately ignores
`proof`, so an unsigned presentation is routed in and reported INVALID rather than skipped.

## Gotchas (hard-won — add to this list)

- **`.gitignore` entries for command output are anchored (`/didKeyPairs.json`) on purpose.**
Unanchored, those names match at *any* depth and silently swallow the identically-named
files under `tests/fixtures/`. Adding a bare `signed_vp.json` would quietly drop a fixture
from the next commit. If you add a command that writes a default filename, anchor it.
- **`tests/commands/verify.test.ts` walks `tests/fixtures/verify/` recursively** and verifies
every JSON it finds. Anything you drop in there becomes a test case. Never put a
presentation there — a VP always carries an expiry and would start failing on its own.
- **VP tests mint their own presentations at runtime** for the same reason. Nothing
automated reads `tests/fixtures/vp/`.
- **`tests/fixtures/vp/` is generated and gitignored** — only `generate-fixtures.cjs` and
`README.md` are tracked, so the folder is empty on a fresh clone. Run
`node tests/fixtures/vp/generate-fixtures.cjs` before testing a command by hand. The set is
all-or-nothing: every credential and presentation is bound to the one holder key pair it
mints, so you cannot regenerate part of it, and each run produces a new holder DID.
- **1–3 `bbs2023`/`ecdsa` fixtures in `verify.test.ts` time out under full-suite load.**
Pre-existing: BBS verification is slow and vitest's default timeout is 5s. It varies run to
run. `npx vitest --run tests/commands/verify.test.ts -t bbs2023` passes in isolation.
- **`main.ts` sets `process.noDeprecation = true`.** Transitive deps (`node-fetch@2` →
`whatwg-url` → `tr46`, and `jsonld@4` → `request` → `tough-cookie`) still require Node's
deprecated `punycode`, and the warning printed mid-prompt garbled the interactive display.
- **`@trustvc/trustvc` does not export the presentation types.** It exports
`signW3CPresentation`/`verifyW3CPresentation` but not `SignedVerifiablePresentation`, so
`src/types.ts` derives it from the function signature. Delete that alias and import
directly once the library exports it.
- **`w3c-sign` output changed in trustvc 2.15.1.** `@trustvc/w3c-vc` 2.4.2 made
`/credentialStatus`, `/validUntil` and `/expirationDate` mandatory pointers, so a holder can
no longer selectively disclose a revocation entry or an expiry away. Credentials signed by
older versions are still strippable and must be reissued. Regenerate the VP fixtures after
bumping trustvc — they're signed artifacts and keep whatever rules produced them.
- **`Cannot read properties of null (reading 'verificationMethod')`** always means a DID could
not be resolved — nearly always a did:web whose document isn't published yet.

## Adding a command

1. Create `src/commands/<area>/<name>.ts` exporting `command`, `describe`, `handler`.
2. Keep the prompt flow in an exported `promptForInputs()` and the work in a second exported
function. Tests mock `@inquirer/prompts` and call the two separately — a handler that does
both inline can't be tested.
3. Add the input type to `src/types.ts`.
4. Wrap the handler body in try/catch and report via `signale.error`.
5. Document it in `README.md`: the Quick Start block, the command table, and a
`<details>` section in the Detailed Command Reference.

## Conventions

- Conventional commits (semantic-release drives versioning and the CHANGELOG).
- Prompts use `@inquirer/prompts`; user-facing output goes through `signale`, never
`console.log` (except deliberate blank spacer lines).
- Read and write files through `src/utils/file-io.ts` — it handles path validation and
parent-directory creation.
- Don't add a prompt whose only correct answer is the default. See the holder-DID note above.

## Maintaining this file

**Documentation-as-code. Keep it in sync in the same change that makes it stale — not
"later".** Update this file when your change touches:

- **A command's prompts, name, or output shape** — including anything the README documents.
- **A gotcha you just spent time on** — new gotchas are the highest-value additions.
- **The dependency on `@trustvc/trustvc`**, when its behaviour changes what commands produce.
- **Tooling, CI gates, or the Node requirement** — keep the Commands section runnable.

Small-and-true beats big-and-stale; delete guidance that no longer holds. Keep it
repo-specific: anything true of every Node CLI doesn't belong here.

**For Claude Code specifically:** at the end of a task that changed any of the above, check
whether this file is now inaccurate and propose the edit as part of the same work — don't
wait to be asked.
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials,
- ✅ **Key Pair Generation**: Generate cryptographic key pairs with Multikey format
- ✅ **DID Management**: Create and manage did:web identifiers
- ✅ **W3C Verifiable Credentials**: Sign, verify and manage W3C verifiable credentials
- ✅ **W3C Verifiable Presentations**: Present credentials as a holder and verify presentations
- ✅ **OpenAttestation**: Sign, verify, wrap/unwrap, and encrypt/decrypt OpenAttestation v2/v3 documents
- ✅ **Token Registry**: Mint tokens to blockchain-based token registries
- ✅ **Document Store**: Deploy and manage document store contracts
Expand Down Expand Up @@ -76,7 +77,10 @@ trustvc did-web
# Sign a W3C verifiable credential
trustvc w3c-sign

# Verify a W3C document
# Present credential(s) you hold as a Verifiable Presentation
trustvc vp-sign

# Verify a W3C credential or presentation
trustvc verify

# Create a credential status list
Expand Down Expand Up @@ -202,6 +206,15 @@ trustvc title-escrow reject-transfer-owner-holder

- **Credential Status**: Provides commands to create and update W3C credential status lists for managing credential revocation and suspension.

- **Verifiable Presentations**: Uses `signW3CPresentation` to let a holder bundle and present their own credentials; presentations are verified by the same `verify` command as every other document. TrustVC enforces the presentation policies, so the CLI cannot disable them:
- **Holder binding** — the signing key's DID must equal the presentation `holder` and every `credentialSubject.id`. The issuer is independent: a credential issued by another party is fine.
- **Mandatory expiry** — every presentation carries a `validUntil`; `vp-sign` always asks for one.
- **Full disclosure** — a selective-disclosure credential that has not been derived is auto-derived.
- **v2 envelope** — the presentation envelope is always VC Data Model 2.0; embedded credentials keep their own version.
- Credentials with a `TransferableRecords` status cannot be presented — ownership of a transferable record lives on-chain.

The holder proof is an `assertionMethod` proof: the CLI does not issue challenges, since an anti-replay nonce can only be checked by the verifier that issued it.

### OpenAttestation

- **Document Signing**: Uses `signOA` to cryptographically sign OpenAttestation v2 and v3 documents with private keys.
Expand Down Expand Up @@ -233,7 +246,8 @@ trustvc title-escrow reject-transfer-owner-holder
| **W3C Credentials** | [`key-pair-generation`](#key-pair-generation) | Generate cryptographic key pairs (ECDSA-SD-2023, BBS-2023) |
| | [`did-web`](#did-web) | Create did:web identifiers from key pairs |
| | [`w3c-sign`](#w3c-sign) | Sign W3C verifiable credentials |
| | [`verify`](#verify) | Verify W3C verifiable credentials |
| | [`verify`](#verify) | Verify W3C verifiable credentials and presentations |
| | [`vp-sign`](#vp-sign) | Create and sign a W3C verifiable presentation |
| | [`credential-status-create`](#credential-status-create) | Create credential status lists |
| | [`credential-status-update`](#credential-status-update) | Update credential status (revoke/suspend) |
| **OpenAttestation** | [`oa-sign`](#oa-sign) | Sign OpenAttestation v2/v3 documents |
Expand Down Expand Up @@ -442,9 +456,50 @@ Verifies the document integrity, status, and issuer identity.
**Supported Formats:**

- W3C Verifiable Credential
- W3C Verifiable Presentation
- OpenAttestation v2
- OpenAttestation v3

**Verifiable Presentations:**
For a presentation, the three results cover the presentation as a whole — `DOCUMENT_INTEGRITY` is the holder proof plus holder binding (an unsigned presentation is invalid), `DOCUMENT_STATUS` is the presentation expiry plus each embedded credential's revocation, and `ISSUER_IDENTITY` resolves each embedded issuer. Freshness of an `authentication` proof (challenge/domain) is not checked — only the verifier that issued the challenge can do that.

A valid presentation adds one line stating how many credentials it covered, since the three results read identically over one credential or five:

```text
✔ success DOCUMENT_INTEGRITY: VALID
✔ success DOCUMENT_STATUS: VALID
✔ success ISSUER_IDENTITY: VALID
ℹ info 2 embedded credentials verified.
```

</details>

<details>
<summary><h4 id="vp-sign">vp-sign</h4></summary>

Creates **and** signs a W3C Verifiable Presentation from one or more signed credentials, so a holder can present credentials they own.

**Usage:**

```sh
trustvc vp-sign
```

**Interactive Prompts:**

- A directory of signed verifiable credentials, **or** the path(s) to individual JSON files (comma-separated). Given a directory, **every file in it is presented** — nothing is filtered by extension or content, so anything that is not a presentable credential is reported by the signing step, naming the file it came from. Sub-directories and dot-files (`.DS_Store` and the like) are skipped.
- Path to the holder did key-pair JSON file (defaults to `./didKeyPairs.json`). The holder DID is taken from this file and is **not** asked for — trustvc requires the signing key's DID to *be* the holder, so there is nothing to choose. A key pair with no DID (the bare `keypair.json` from [`key-pair-generation`](#key-pair-generation)) is rejected up front; use the `didKeyPairs.json` that [`did-web`](#did-web) writes.
- Presentation expiry — either a lifetime in seconds or an explicit `validUntil` timestamp
- Output directory

**Output:**
Creates `signed_vp.json`, holding an `assertionMethod` holder proof. Verify it with [`verify`](#verify).

**Requirements:**

- The holder key pair must be an ECDSA (P-256) Multikey bound to a DID — the `didKeyPairs.json` produced by [`did-web`](#did-web) is one.
- Every credential must be about the holder: each `credentialSubject.id` must equal the holder DID. Credentials with a `TransferableRecords` status cannot be presented.

</details>

<details>
Expand Down Expand Up @@ -1438,6 +1493,7 @@ src/commands/
├── did.ts # Generate DID
├── key-pair.ts # Generate key pairs
├── sign.ts # Sign W3C credentials
├── vp-sign.ts # Create and sign a verifiable presentation
└── credentialStatus/
├── create.ts # Create credential status list
└── update.ts # Update credential status list
Expand Down
Loading
Loading