@vidos-id/openid4vc-wallet-cli 0.0.0-test1
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.
- package/README.md +158 -0
- package/dist/index.d.mts +197 -0
- package/dist/index.mjs +936 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @vidos-id/openid4vc-wallet-cli
|
|
2
|
+
|
|
3
|
+
CLI for OpenID4VCI credential receipt, `dc+sd-jwt` wallet storage, IETF credential status resolution, and OpenID4VP presentation. Wraps the [`@vidos-id/openid4vc-wallet`](../wallet/) library.
|
|
4
|
+
|
|
5
|
+
For the full issue-hold-present flow, see the [root README](../../).
|
|
6
|
+
|
|
7
|
+
Prefer using this CLI for wallet tasks instead of re-implementing protocol steps in agent code. In particular, `openid4vc-wallet receive` already handles the supported OID4VCI offer redemption flow end to end.
|
|
8
|
+
|
|
9
|
+
Running `openid4vc-wallet` with no subcommand starts the interactive mode by default.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Download the latest GitHub Release artifact and make it executable:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -L -o openid4vc-wallet https://github.com/vidos-id/openid4vc-tools/releases/latest/download/openid4vc-wallet.js
|
|
17
|
+
chmod +x openid4vc-wallet
|
|
18
|
+
./openid4vc-wallet --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Release artifacts do not bundle the repo's `examples/` directory. For remote example requests, pass raw GitHub content via `--request` or raw offer content via `--offer`.
|
|
22
|
+
|
|
23
|
+
For development in this repo, you can run the bin entry directly with Bun:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun packages/wallet-cli/src/index.ts --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Files in `--wallet-dir`
|
|
30
|
+
|
|
31
|
+
- `holder-key.json` - Holder key pair (private + public JWK)
|
|
32
|
+
- `wallet.json` - Wallet manifest with credential index
|
|
33
|
+
- `credentials/<credential-id>.json` - Stored credentials
|
|
34
|
+
|
|
35
|
+
## Interactive Mode
|
|
36
|
+
|
|
37
|
+
Run the CLI with no subcommand:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
openid4vc-wallet
|
|
41
|
+
openid4vc-wallet --wallet-dir ./my-wallet
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Interactive mode can:
|
|
45
|
+
|
|
46
|
+
- initialize a wallet if one does not exist yet
|
|
47
|
+
- receive credentials from OpenID4VCI offers
|
|
48
|
+
- browse stored credentials
|
|
49
|
+
- show credential details and status
|
|
50
|
+
- present credentials to verifiers
|
|
51
|
+
- import raw credentials as a fallback
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### `init`
|
|
56
|
+
|
|
57
|
+
Initialize a wallet directory and create (or import) a holder key.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
openid4vc-wallet init --wallet-dir ./my-wallet
|
|
61
|
+
openid4vc-wallet init --wallet-dir ./my-wallet --alg EdDSA
|
|
62
|
+
openid4vc-wallet init --wallet-dir ./my-wallet --holder-key-file ./existing-key.jwk.json
|
|
63
|
+
openid4vc-wallet init --wallet-dir ./my-wallet --output json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `receive`
|
|
67
|
+
|
|
68
|
+
Receive and store a credential from a minimal OpenID4VCI credential offer.
|
|
69
|
+
|
|
70
|
+
This is the primary way to add credentials into the wallet.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# From an openid-credential-offer URI
|
|
74
|
+
openid4vc-wallet receive \
|
|
75
|
+
--wallet-dir ./my-wallet \
|
|
76
|
+
--offer 'openid-credential-offer://?credential_offer=...'
|
|
77
|
+
|
|
78
|
+
# From inline credential-offer JSON
|
|
79
|
+
openid4vc-wallet receive \
|
|
80
|
+
--wallet-dir ./my-wallet \
|
|
81
|
+
--offer '{"credential_issuer":"https://issuer.example",...}'
|
|
82
|
+
|
|
83
|
+
# From a by-reference offer URI
|
|
84
|
+
openid4vc-wallet receive \
|
|
85
|
+
--wallet-dir ./my-wallet \
|
|
86
|
+
--offer 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.example%2Foffers%2Fperson-1'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Notes:
|
|
90
|
+
- default output is a concise text summary; use `--output json` for full details
|
|
91
|
+
- supports by-value `credential_offer` and by-reference `credential_offer_uri`
|
|
92
|
+
- resolves issuer metadata from `/.well-known/openid-credential-issuer[issuer-path]`
|
|
93
|
+
- uses advertised metadata endpoints instead of hardcoding paths
|
|
94
|
+
|
|
95
|
+
### `import`
|
|
96
|
+
|
|
97
|
+
Import an already-issued compact `dc+sd-jwt` credential.
|
|
98
|
+
|
|
99
|
+
Use this only when you already have the raw credential blob. Prefer `receive` whenever you have an OpenID4VCI offer.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
openid4vc-wallet import \
|
|
103
|
+
--wallet-dir ./my-wallet \
|
|
104
|
+
--credential-file ./issuer/credential.txt
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `list`
|
|
108
|
+
|
|
109
|
+
List stored credentials.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
openid4vc-wallet list --wallet-dir ./my-wallet
|
|
113
|
+
openid4vc-wallet list --wallet-dir ./my-wallet --vct urn:eudi:pid:1
|
|
114
|
+
openid4vc-wallet list --wallet-dir ./my-wallet --issuer https://issuer.example
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `show`
|
|
118
|
+
|
|
119
|
+
Show a single stored credential by id.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
openid4vc-wallet show --wallet-dir ./my-wallet --credential-id <id>
|
|
123
|
+
openid4vc-wallet show --wallet-dir ./my-wallet --credential-id <id> --output raw
|
|
124
|
+
openid4vc-wallet show --wallet-dir ./my-wallet --credential-id <id> --output json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `present`
|
|
128
|
+
|
|
129
|
+
Create a DCQL-based OpenID4VP presentation from wallet credentials.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
openid4vc-wallet present \
|
|
133
|
+
--wallet-dir ./my-wallet \
|
|
134
|
+
--request 'openid4vp://authorize?...'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Global options
|
|
138
|
+
|
|
139
|
+
- running `openid4vc-wallet` with no subcommand starts interactive mode
|
|
140
|
+
- `--wallet-dir <dir>` - wallet directory for interactive mode
|
|
141
|
+
- `--verbose` - enable verbose logging to stderr
|
|
142
|
+
- `--version` - show version number
|
|
143
|
+
- `--help` - show help for a command
|
|
144
|
+
|
|
145
|
+
## Notes
|
|
146
|
+
|
|
147
|
+
- `receive` is the primary credential-ingest path
|
|
148
|
+
- `import` is a local raw-credential fallback
|
|
149
|
+
- `present` auto-submits `direct_post` and `direct_post.jwt` responses unless `--dry-run` is set
|
|
150
|
+
- when multiple credentials match a query, `present` prompts interactively in a TTY or returns an error with a `--credential-id` suggestion in non-interactive environments
|
|
151
|
+
- only by-value DCQL requests are supported
|
|
152
|
+
- `show` automatically fetches, verifies, and decodes IETF status list JWTs for stored credentials that include a `status.status_list` reference
|
|
153
|
+
|
|
154
|
+
## Test
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
bun test packages/wallet-cli/src/index.test.ts
|
|
158
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as _$_vidos_id_openid4vc_wallet0 from "@vidos-id/openid4vc-wallet";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
//#region src/program.d.ts
|
|
5
|
+
declare function createProgram(version: string): Command;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/actions/delete.d.ts
|
|
8
|
+
declare function deleteCredentialAction(rawOptions: unknown): Promise<{
|
|
9
|
+
credentialId: string;
|
|
10
|
+
}>;
|
|
11
|
+
declare function deleteAllCredentialsAction(rawOptions: unknown): Promise<{
|
|
12
|
+
deleted: number;
|
|
13
|
+
}>;
|
|
14
|
+
declare function deleteWalletAction(rawOptions: unknown): Promise<{
|
|
15
|
+
walletDir: string;
|
|
16
|
+
}>;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/actions/import.d.ts
|
|
19
|
+
declare function importCredentialAction(rawOptions: unknown): Promise<{
|
|
20
|
+
credential: {
|
|
21
|
+
id: string;
|
|
22
|
+
format: "dc+sd-jwt";
|
|
23
|
+
compactSdJwt: string;
|
|
24
|
+
issuer: string;
|
|
25
|
+
vct: string;
|
|
26
|
+
holderKeyId: string;
|
|
27
|
+
claims: Record<string, unknown>;
|
|
28
|
+
importedAt: string;
|
|
29
|
+
status?: {
|
|
30
|
+
status_list: {
|
|
31
|
+
idx: number;
|
|
32
|
+
uri: string;
|
|
33
|
+
};
|
|
34
|
+
} | undefined;
|
|
35
|
+
issuerKeyMaterial?: {
|
|
36
|
+
issuer: string;
|
|
37
|
+
jwks: {
|
|
38
|
+
keys: Record<string, unknown>[];
|
|
39
|
+
};
|
|
40
|
+
} | {
|
|
41
|
+
issuer: string;
|
|
42
|
+
jwk: Record<string, unknown>;
|
|
43
|
+
} | undefined;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/actions/init.d.ts
|
|
48
|
+
declare function initWalletAction(rawOptions: unknown): Promise<{
|
|
49
|
+
holderKey: {
|
|
50
|
+
id: string;
|
|
51
|
+
algorithm: "ES256" | "ES384" | "EdDSA";
|
|
52
|
+
publicJwk: Record<string, unknown>;
|
|
53
|
+
privateJwk: Record<string, unknown>;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
};
|
|
56
|
+
imported: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/actions/interactive.d.ts
|
|
60
|
+
declare function interactiveWalletAction(rawOptions: unknown): Promise<void>;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/actions/list.d.ts
|
|
63
|
+
declare function listCredentialsAction(rawOptions: unknown): Promise<{
|
|
64
|
+
credentials: {
|
|
65
|
+
id: string;
|
|
66
|
+
format: "dc+sd-jwt";
|
|
67
|
+
compactSdJwt: string;
|
|
68
|
+
issuer: string;
|
|
69
|
+
vct: string;
|
|
70
|
+
holderKeyId: string;
|
|
71
|
+
claims: Record<string, unknown>;
|
|
72
|
+
importedAt: string;
|
|
73
|
+
status?: {
|
|
74
|
+
status_list: {
|
|
75
|
+
idx: number;
|
|
76
|
+
uri: string;
|
|
77
|
+
};
|
|
78
|
+
} | undefined;
|
|
79
|
+
issuerKeyMaterial?: {
|
|
80
|
+
issuer: string;
|
|
81
|
+
jwks: {
|
|
82
|
+
keys: Record<string, unknown>[];
|
|
83
|
+
};
|
|
84
|
+
} | {
|
|
85
|
+
issuer: string;
|
|
86
|
+
jwk: Record<string, unknown>;
|
|
87
|
+
} | undefined;
|
|
88
|
+
}[];
|
|
89
|
+
}>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/actions/present.d.ts
|
|
92
|
+
declare function presentCredentialAction(rawOptions: unknown): Promise<{
|
|
93
|
+
submitted: boolean;
|
|
94
|
+
submission: _$_vidos_id_openid4vc_wallet0.OpenId4VpResponseSubmissionResult | undefined;
|
|
95
|
+
query: _$_vidos_id_openid4vc_wallet0.ParsedDcqlQuery;
|
|
96
|
+
vpToken: string;
|
|
97
|
+
dcqlPresentation: Record<string, string[]>;
|
|
98
|
+
matchedCredentials: _$_vidos_id_openid4vc_wallet0.MatchedCredential[];
|
|
99
|
+
}>;
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/actions/receive.d.ts
|
|
102
|
+
declare function receiveCredentialAction(rawOptions: unknown): Promise<{
|
|
103
|
+
credential: {
|
|
104
|
+
id: string;
|
|
105
|
+
format: "dc+sd-jwt";
|
|
106
|
+
compactSdJwt: string;
|
|
107
|
+
issuer: string;
|
|
108
|
+
vct: string;
|
|
109
|
+
holderKeyId: string;
|
|
110
|
+
claims: Record<string, unknown>;
|
|
111
|
+
importedAt: string;
|
|
112
|
+
status?: {
|
|
113
|
+
status_list: {
|
|
114
|
+
idx: number;
|
|
115
|
+
uri: string;
|
|
116
|
+
};
|
|
117
|
+
} | undefined;
|
|
118
|
+
issuerKeyMaterial?: {
|
|
119
|
+
issuer: string;
|
|
120
|
+
jwks: {
|
|
121
|
+
keys: Record<string, unknown>[];
|
|
122
|
+
};
|
|
123
|
+
} | {
|
|
124
|
+
issuer: string;
|
|
125
|
+
jwk: Record<string, unknown>;
|
|
126
|
+
} | undefined;
|
|
127
|
+
};
|
|
128
|
+
}>;
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/actions/show.d.ts
|
|
131
|
+
declare function showCredentialAction(rawOptions: unknown): Promise<{
|
|
132
|
+
credential: {
|
|
133
|
+
id: string;
|
|
134
|
+
format: "dc+sd-jwt";
|
|
135
|
+
compactSdJwt: string;
|
|
136
|
+
issuer: string;
|
|
137
|
+
vct: string;
|
|
138
|
+
holderKeyId: string;
|
|
139
|
+
claims: Record<string, unknown>;
|
|
140
|
+
importedAt: string;
|
|
141
|
+
status?: {
|
|
142
|
+
status_list: {
|
|
143
|
+
idx: number;
|
|
144
|
+
uri: string;
|
|
145
|
+
};
|
|
146
|
+
} | undefined;
|
|
147
|
+
issuerKeyMaterial?: {
|
|
148
|
+
issuer: string;
|
|
149
|
+
jwks: {
|
|
150
|
+
keys: Record<string, unknown>[];
|
|
151
|
+
};
|
|
152
|
+
} | {
|
|
153
|
+
issuer: string;
|
|
154
|
+
jwk: Record<string, unknown>;
|
|
155
|
+
} | undefined;
|
|
156
|
+
};
|
|
157
|
+
status: _$_vidos_id_openid4vc_wallet0.ResolvedCredentialStatus | null;
|
|
158
|
+
statusWarning: undefined;
|
|
159
|
+
} | {
|
|
160
|
+
credential: {
|
|
161
|
+
id: string;
|
|
162
|
+
format: "dc+sd-jwt";
|
|
163
|
+
compactSdJwt: string;
|
|
164
|
+
issuer: string;
|
|
165
|
+
vct: string;
|
|
166
|
+
holderKeyId: string;
|
|
167
|
+
claims: Record<string, unknown>;
|
|
168
|
+
importedAt: string;
|
|
169
|
+
status?: {
|
|
170
|
+
status_list: {
|
|
171
|
+
idx: number;
|
|
172
|
+
uri: string;
|
|
173
|
+
};
|
|
174
|
+
} | undefined;
|
|
175
|
+
issuerKeyMaterial?: {
|
|
176
|
+
issuer: string;
|
|
177
|
+
jwks: {
|
|
178
|
+
keys: Record<string, unknown>[];
|
|
179
|
+
};
|
|
180
|
+
} | {
|
|
181
|
+
issuer: string;
|
|
182
|
+
jwk: Record<string, unknown>;
|
|
183
|
+
} | undefined;
|
|
184
|
+
};
|
|
185
|
+
status: null;
|
|
186
|
+
statusWarning: string;
|
|
187
|
+
}>;
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/index.d.ts
|
|
190
|
+
type InteractiveCliOptions = {
|
|
191
|
+
walletDir?: string;
|
|
192
|
+
verbose?: boolean;
|
|
193
|
+
};
|
|
194
|
+
declare function parseInteractiveCliOptions(argv: string[]): InteractiveCliOptions;
|
|
195
|
+
declare function runCli(argv?: string[]): Promise<void>;
|
|
196
|
+
//#endregion
|
|
197
|
+
export { createProgram, deleteAllCredentialsAction, deleteCredentialAction, deleteWalletAction, importCredentialAction, initWalletAction, interactiveWalletAction, listCredentialsAction, parseInteractiveCliOptions, presentCredentialAction, receiveCredentialAction, runCli, showCredentialAction };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { handleCliError, outputFormatSchema, printResult, resolveCliVersion, resolvePackageJsonPath, setVerbose, textOutputFormatSchema, verbose } from "@vidos-id/openid4vc-cli-common";
|
|
4
|
+
import { access, mkdir, readFile, readdir, rm, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { stdout } from "node:process";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { HolderKeyRecordSchema, StoredCredentialRecordSchema, Wallet, createOpenId4VpAuthorizationResponse, parseOpenid4VpAuthorizationUrl, receiveCredentialFromOffer, resolveOpenId4VpRequest, submitOpenId4VpAuthorizationResponse } from "@vidos-id/openid4vc-wallet";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
//#region src/format.ts
|
|
12
|
+
function formatInitResult(input) {
|
|
13
|
+
const source = input.imported ? "imported" : "generated";
|
|
14
|
+
return [`Initialized wallet at ${input.walletDir}`, `Holder key: ${input.holderKey.id} (${input.holderKey.algorithm ?? "unknown"}, ${source})`].join("\n");
|
|
15
|
+
}
|
|
16
|
+
function formatCredentialSummary(action, credential) {
|
|
17
|
+
return [
|
|
18
|
+
`${action} credential ${credential.id}`,
|
|
19
|
+
`VCT: ${credential.vct}`,
|
|
20
|
+
`Issuer: ${credential.issuer}`
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
function formatCredentialList(credentials) {
|
|
24
|
+
if (credentials.length === 0) return "0 credentials found";
|
|
25
|
+
const lines = [`${credentials.length} credential${credentials.length === 1 ? "" : "s"} found`];
|
|
26
|
+
for (const credential of credentials) lines.push(`${credential.id} | ${credential.vct} | ${credential.issuer}`);
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
29
|
+
function formatCredentialDetails(input) {
|
|
30
|
+
const { credential, status, statusWarning } = input;
|
|
31
|
+
const lines = [
|
|
32
|
+
"Credential",
|
|
33
|
+
`ID: ${credential.id}`,
|
|
34
|
+
`Issuer: ${credential.issuer}`,
|
|
35
|
+
`VCT: ${credential.vct}`,
|
|
36
|
+
"",
|
|
37
|
+
"Claims"
|
|
38
|
+
];
|
|
39
|
+
const claimEntries = Object.entries(credential.claims);
|
|
40
|
+
if (claimEntries.length === 0) lines.push("none");
|
|
41
|
+
else for (const [key, value] of claimEntries) lines.push(`${key}: ${formatValue(value)}`);
|
|
42
|
+
lines.push("", "Status");
|
|
43
|
+
if (status) {
|
|
44
|
+
lines.push(`State: ${status.status.label}`);
|
|
45
|
+
lines.push(`Valid: ${status.status.isValid ? "yes" : "no"}`);
|
|
46
|
+
lines.push(`Value: ${status.status.value}`);
|
|
47
|
+
lines.push(`Reference: ${status.statusReference.uri}#${status.statusReference.idx}`);
|
|
48
|
+
if (status.statusList.ttl !== void 0) lines.push(`TTL: ${status.statusList.ttl}`);
|
|
49
|
+
} else if (credential.status?.status_list) {
|
|
50
|
+
lines.push("State: unresolved");
|
|
51
|
+
lines.push(`Reference: ${credential.status.status_list.uri}#${credential.status.status_list.idx}`);
|
|
52
|
+
if (statusWarning) lines.push(`Warning: ${statusWarning}`);
|
|
53
|
+
} else lines.push("State: not present");
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function formatPresentationSummary(result) {
|
|
57
|
+
const lines = ["Presentation created"];
|
|
58
|
+
if (result.matchedCredentials.length === 0) lines.push("Matched credentials: none");
|
|
59
|
+
else {
|
|
60
|
+
lines.push("Matched credentials:");
|
|
61
|
+
for (const credential of result.matchedCredentials) lines.push(`${credential.credentialId} | ${credential.vct} | ${credential.issuer}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push(`Submitted: ${result.submitted ? "yes" : "no"}`);
|
|
64
|
+
if (result.submission !== void 0) lines.push(`Submission response: ${formatValue(result.submission)}`);
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
function formatDeleteCredentialSummary(credentialId) {
|
|
68
|
+
return `Deleted credential ${credentialId}`;
|
|
69
|
+
}
|
|
70
|
+
function formatDeleteAllCredentialsSummary(deleted) {
|
|
71
|
+
return `Deleted ${deleted} credential${deleted === 1 ? "" : "s"}`;
|
|
72
|
+
}
|
|
73
|
+
function formatDeleteWalletSummary(walletDir) {
|
|
74
|
+
return `Deleted wallet at ${walletDir}`;
|
|
75
|
+
}
|
|
76
|
+
function formatValue(value) {
|
|
77
|
+
if (typeof value === "string") return value;
|
|
78
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
79
|
+
if (value === null || value === void 0) return String(value);
|
|
80
|
+
return JSON.stringify(value);
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/prompts.ts
|
|
84
|
+
var PromptSession = class {
|
|
85
|
+
close() {}
|
|
86
|
+
async text(label, options) {
|
|
87
|
+
const { value } = await inquirer.prompt([{
|
|
88
|
+
type: options?.password ? "password" : "input",
|
|
89
|
+
name: "value",
|
|
90
|
+
message: label,
|
|
91
|
+
default: options?.defaultValue,
|
|
92
|
+
mask: options?.password ? "*" : void 0,
|
|
93
|
+
validate: (input) => {
|
|
94
|
+
if (input.trim() || options?.allowEmpty || options?.defaultValue !== void 0) return true;
|
|
95
|
+
return "A value is required";
|
|
96
|
+
}
|
|
97
|
+
}]);
|
|
98
|
+
return value.trim() || options?.defaultValue || "";
|
|
99
|
+
}
|
|
100
|
+
async choose(label, choices) {
|
|
101
|
+
const { value } = await inquirer.prompt([{
|
|
102
|
+
type: "list",
|
|
103
|
+
name: "value",
|
|
104
|
+
message: label,
|
|
105
|
+
choices: choices.map((choice) => ({
|
|
106
|
+
name: choice.label,
|
|
107
|
+
value: choice.value
|
|
108
|
+
}))
|
|
109
|
+
}]);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
async confirm(label, defaultValue = true) {
|
|
113
|
+
const { value } = await inquirer.prompt([{
|
|
114
|
+
type: "confirm",
|
|
115
|
+
name: "value",
|
|
116
|
+
message: label,
|
|
117
|
+
default: defaultValue
|
|
118
|
+
}]);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/schemas.ts
|
|
124
|
+
const importOptionsSchema = z.object({
|
|
125
|
+
walletDir: z.string().min(1),
|
|
126
|
+
credential: z.string().optional(),
|
|
127
|
+
credentialFile: z.string().optional(),
|
|
128
|
+
output: textOutputFormatSchema.default("text")
|
|
129
|
+
}).superRefine((value, ctx) => {
|
|
130
|
+
if (!value.credential && !value.credentialFile) ctx.addIssue({
|
|
131
|
+
code: z.ZodIssueCode.custom,
|
|
132
|
+
message: "Provide --credential or --credential-file",
|
|
133
|
+
path: ["credential"]
|
|
134
|
+
});
|
|
135
|
+
if (value.credential && value.credentialFile) ctx.addIssue({
|
|
136
|
+
code: z.ZodIssueCode.custom,
|
|
137
|
+
message: "Use only one of --credential or --credential-file",
|
|
138
|
+
path: ["credential"]
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
const receiveOptionsSchema = z.object({
|
|
142
|
+
walletDir: z.string().min(1),
|
|
143
|
+
offer: z.string().min(1),
|
|
144
|
+
output: textOutputFormatSchema.default("text")
|
|
145
|
+
});
|
|
146
|
+
const listOptionsSchema = z.object({
|
|
147
|
+
walletDir: z.string().min(1),
|
|
148
|
+
vct: z.string().optional(),
|
|
149
|
+
issuer: z.string().optional(),
|
|
150
|
+
output: textOutputFormatSchema.default("text")
|
|
151
|
+
});
|
|
152
|
+
const deleteOptionsSchema = z.object({
|
|
153
|
+
walletDir: z.string().min(1),
|
|
154
|
+
credentialId: z.string().min(1)
|
|
155
|
+
});
|
|
156
|
+
const initOptionsSchema = z.object({
|
|
157
|
+
walletDir: z.string().min(1),
|
|
158
|
+
alg: z.enum([
|
|
159
|
+
"ES256",
|
|
160
|
+
"ES384",
|
|
161
|
+
"EdDSA"
|
|
162
|
+
]).optional(),
|
|
163
|
+
holderKeyFile: z.string().optional(),
|
|
164
|
+
output: textOutputFormatSchema.default("text")
|
|
165
|
+
});
|
|
166
|
+
const showOptionsSchema = z.object({
|
|
167
|
+
walletDir: z.string().min(1),
|
|
168
|
+
credentialId: z.string().min(1),
|
|
169
|
+
output: outputFormatSchema.default("text")
|
|
170
|
+
});
|
|
171
|
+
const presentOptionsSchema = z.object({
|
|
172
|
+
walletDir: z.string().min(1),
|
|
173
|
+
request: z.string().min(1),
|
|
174
|
+
credentialId: z.string().optional(),
|
|
175
|
+
dryRun: z.boolean().optional().default(false),
|
|
176
|
+
output: outputFormatSchema.default("text")
|
|
177
|
+
});
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/storage.ts
|
|
180
|
+
const walletManifestSchema = z.object({
|
|
181
|
+
version: z.literal(1),
|
|
182
|
+
credentials: z.array(z.object({
|
|
183
|
+
id: z.string().min(1),
|
|
184
|
+
fileName: z.string().min(1),
|
|
185
|
+
issuer: z.string().min(1),
|
|
186
|
+
vct: z.string().min(1),
|
|
187
|
+
importedAt: z.string().datetime()
|
|
188
|
+
})).default([]),
|
|
189
|
+
updatedAt: z.string().datetime()
|
|
190
|
+
});
|
|
191
|
+
const defaultManifest = () => ({
|
|
192
|
+
version: 1,
|
|
193
|
+
credentials: [],
|
|
194
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
});
|
|
196
|
+
const isMissingFileError = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
197
|
+
var FileSystemWalletStorage = class {
|
|
198
|
+
walletDir;
|
|
199
|
+
holderKeyPath;
|
|
200
|
+
manifestPath;
|
|
201
|
+
credentialsDir;
|
|
202
|
+
constructor(walletDir) {
|
|
203
|
+
this.walletDir = walletDir;
|
|
204
|
+
this.holderKeyPath = join(walletDir, "holder-key.json");
|
|
205
|
+
this.manifestPath = join(walletDir, "wallet.json");
|
|
206
|
+
this.credentialsDir = join(walletDir, "credentials");
|
|
207
|
+
}
|
|
208
|
+
async getHolderKey() {
|
|
209
|
+
return await readJsonFile(this.holderKeyPath, HolderKeyRecordSchema) ?? null;
|
|
210
|
+
}
|
|
211
|
+
async setHolderKey(record) {
|
|
212
|
+
await this.ensureLayout();
|
|
213
|
+
await writeJsonFile(this.holderKeyPath, HolderKeyRecordSchema.parse(record));
|
|
214
|
+
}
|
|
215
|
+
async listCredentials() {
|
|
216
|
+
await this.ensureLayout();
|
|
217
|
+
const manifest = await this.readManifest() ?? defaultManifest();
|
|
218
|
+
return (await Promise.all(manifest.credentials.map(async ({ id }) => this.getCredential(id)))).filter((record) => record !== null);
|
|
219
|
+
}
|
|
220
|
+
async getCredential(id) {
|
|
221
|
+
await this.ensureLayout();
|
|
222
|
+
return await readJsonFile(this.credentialPath(id), StoredCredentialRecordSchema) ?? null;
|
|
223
|
+
}
|
|
224
|
+
async setCredential(record) {
|
|
225
|
+
await this.ensureLayout();
|
|
226
|
+
const parsed = StoredCredentialRecordSchema.parse(record);
|
|
227
|
+
await writeJsonFile(this.credentialPath(parsed.id), parsed);
|
|
228
|
+
const manifest = await this.readManifest() ?? defaultManifest();
|
|
229
|
+
const entry = {
|
|
230
|
+
id: parsed.id,
|
|
231
|
+
fileName: this.credentialFileName(parsed.id),
|
|
232
|
+
issuer: parsed.issuer,
|
|
233
|
+
vct: parsed.vct,
|
|
234
|
+
importedAt: parsed.importedAt
|
|
235
|
+
};
|
|
236
|
+
const existingIndex = manifest.credentials.findIndex((candidate) => candidate.id === parsed.id);
|
|
237
|
+
if (existingIndex >= 0) manifest.credentials[existingIndex] = entry;
|
|
238
|
+
else manifest.credentials.push(entry);
|
|
239
|
+
manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
240
|
+
await writeJsonFile(this.manifestPath, manifest);
|
|
241
|
+
}
|
|
242
|
+
async deleteCredential(id) {
|
|
243
|
+
await this.ensureLayout();
|
|
244
|
+
const manifest = await this.readManifest() ?? defaultManifest();
|
|
245
|
+
const nextCredentials = manifest.credentials.filter((candidate) => candidate.id !== id);
|
|
246
|
+
if (nextCredentials.length === manifest.credentials.length) return false;
|
|
247
|
+
await unlinkIfExists(this.credentialPath(id));
|
|
248
|
+
manifest.credentials = nextCredentials;
|
|
249
|
+
manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
|
+
await writeJsonFile(this.manifestPath, manifest);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
async deleteAllCredentials() {
|
|
254
|
+
await this.ensureLayout();
|
|
255
|
+
const manifest = await this.readManifest() ?? defaultManifest();
|
|
256
|
+
const deleted = manifest.credentials.length;
|
|
257
|
+
await Promise.all(manifest.credentials.map(({ id }) => unlinkIfExists(this.credentialPath(id))));
|
|
258
|
+
manifest.credentials = [];
|
|
259
|
+
manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
260
|
+
await writeJsonFile(this.manifestPath, manifest);
|
|
261
|
+
return deleted;
|
|
262
|
+
}
|
|
263
|
+
async deleteWallet() {
|
|
264
|
+
await rm(this.walletDir, {
|
|
265
|
+
recursive: true,
|
|
266
|
+
force: true
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async ensureLayout() {
|
|
270
|
+
await mkdir(this.credentialsDir, { recursive: true });
|
|
271
|
+
if (await this.readManifest() === null) await writeJsonFile(this.manifestPath, defaultManifest());
|
|
272
|
+
}
|
|
273
|
+
async readManifest() {
|
|
274
|
+
const parsed = await readJsonFile(this.manifestPath, walletManifestSchema);
|
|
275
|
+
if (parsed) return parsed;
|
|
276
|
+
const fileNames = await readDirectoryNames(this.credentialsDir);
|
|
277
|
+
if (fileNames.length === 0) return null;
|
|
278
|
+
const manifest = {
|
|
279
|
+
version: 1,
|
|
280
|
+
credentials: (await Promise.all(fileNames.filter((fileName) => fileName.endsWith(".json")).map(async (fileName) => {
|
|
281
|
+
const record = await readJsonFile(join(this.credentialsDir, fileName), StoredCredentialRecordSchema);
|
|
282
|
+
return record ? {
|
|
283
|
+
id: record.id,
|
|
284
|
+
fileName,
|
|
285
|
+
issuer: record.issuer,
|
|
286
|
+
vct: record.vct,
|
|
287
|
+
importedAt: record.importedAt
|
|
288
|
+
} : null;
|
|
289
|
+
}))).filter((record) => record !== null),
|
|
290
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
291
|
+
};
|
|
292
|
+
await writeJsonFile(this.manifestPath, manifest);
|
|
293
|
+
return manifest;
|
|
294
|
+
}
|
|
295
|
+
credentialPath(id) {
|
|
296
|
+
return join(this.credentialsDir, this.credentialFileName(id));
|
|
297
|
+
}
|
|
298
|
+
credentialFileName(id) {
|
|
299
|
+
return `${encodeURIComponent(id)}.json`;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
async function readJsonFile(filePath, schema) {
|
|
303
|
+
try {
|
|
304
|
+
const content = await readFile(filePath, "utf8");
|
|
305
|
+
return schema.parse(JSON.parse(content));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (isMissingFileError(error)) return null;
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function writeJsonFile(filePath, value) {
|
|
312
|
+
await writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
|
|
313
|
+
}
|
|
314
|
+
async function unlinkIfExists(filePath) {
|
|
315
|
+
try {
|
|
316
|
+
await unlink(filePath);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (isMissingFileError(error)) return;
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function readDirectoryNames(directoryPath) {
|
|
323
|
+
try {
|
|
324
|
+
return await readdir(directoryPath);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (isMissingFileError(error)) return [];
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/actions/delete.ts
|
|
332
|
+
const deleteWalletOptionsSchema = deleteOptionsSchema.omit({ credentialId: true });
|
|
333
|
+
async function deleteCredentialAction(rawOptions) {
|
|
334
|
+
const options = deleteOptionsSchema.parse(rawOptions);
|
|
335
|
+
await new FileSystemWalletStorage(options.walletDir).deleteCredential(options.credentialId);
|
|
336
|
+
return { credentialId: options.credentialId };
|
|
337
|
+
}
|
|
338
|
+
async function deleteAllCredentialsAction(rawOptions) {
|
|
339
|
+
return { deleted: await new FileSystemWalletStorage(deleteWalletOptionsSchema.parse(rawOptions).walletDir).deleteAllCredentials() };
|
|
340
|
+
}
|
|
341
|
+
async function deleteWalletAction(rawOptions) {
|
|
342
|
+
const options = deleteWalletOptionsSchema.parse(rawOptions);
|
|
343
|
+
await new FileSystemWalletStorage(options.walletDir).deleteWallet();
|
|
344
|
+
return { walletDir: options.walletDir };
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/actions/list.ts
|
|
348
|
+
async function listCredentialsAction(rawOptions) {
|
|
349
|
+
const options = listOptionsSchema.parse(rawOptions);
|
|
350
|
+
return { credentials: (await new Wallet(new FileSystemWalletStorage(options.walletDir)).listCredentials()).filter((credential) => {
|
|
351
|
+
if (options.vct && credential.vct !== options.vct) return false;
|
|
352
|
+
if (options.issuer && credential.issuer !== options.issuer) return false;
|
|
353
|
+
return true;
|
|
354
|
+
}) };
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/actions/delete-helpers.ts
|
|
358
|
+
async function chooseCredentialId(prompt, walletDir) {
|
|
359
|
+
const list = await listCredentialsAction({ walletDir });
|
|
360
|
+
if (list.credentials.length === 0) {
|
|
361
|
+
stdout.write("0 credentials found\n\n");
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return prompt.choose("Select a credential", list.credentials.map((credential) => ({
|
|
365
|
+
label: `${credential.id} | ${credential.vct} | ${credential.issuer}`,
|
|
366
|
+
value: credential.id
|
|
367
|
+
})));
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/actions/import.ts
|
|
371
|
+
async function importCredentialAction(rawOptions) {
|
|
372
|
+
const options = importOptionsSchema.parse(rawOptions);
|
|
373
|
+
const wallet = new Wallet(new FileSystemWalletStorage(options.walletDir));
|
|
374
|
+
const credential = (options.credential ?? await readFile(options.credentialFile, "utf8")).trim();
|
|
375
|
+
return { credential: await wallet.importCredential({ credential }) };
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/actions/init.ts
|
|
379
|
+
async function initWalletAction(rawOptions) {
|
|
380
|
+
const options = initOptionsSchema.parse(rawOptions);
|
|
381
|
+
const wallet = new Wallet(new FileSystemWalletStorage(options.walletDir));
|
|
382
|
+
if (options.holderKeyFile) {
|
|
383
|
+
const raw = JSON.parse(await readFile(options.holderKeyFile, "utf8"));
|
|
384
|
+
const privateJwk = raw.privateJwk ?? raw;
|
|
385
|
+
const publicJwk = raw.publicJwk ?? raw;
|
|
386
|
+
const algorithm = detectAlgorithm(publicJwk, options.alg);
|
|
387
|
+
return {
|
|
388
|
+
holderKey: await wallet.importHolderKey({
|
|
389
|
+
privateJwk,
|
|
390
|
+
publicJwk,
|
|
391
|
+
algorithm
|
|
392
|
+
}),
|
|
393
|
+
imported: true
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
holderKey: await wallet.getOrCreateHolderKey(options.alg),
|
|
398
|
+
imported: false
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function detectAlgorithm(jwk, explicit) {
|
|
402
|
+
if (explicit) return explicit;
|
|
403
|
+
const kty = jwk.kty;
|
|
404
|
+
const crv = jwk.crv;
|
|
405
|
+
if (kty === "EC" && crv === "P-384") return "ES384";
|
|
406
|
+
if (kty === "EC") return "ES256";
|
|
407
|
+
if (kty === "OKP") return "EdDSA";
|
|
408
|
+
throw new Error("Cannot infer algorithm from key type. Use --alg to specify.");
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/selected-storage.ts
|
|
412
|
+
var SelectedCredentialStorage = class {
|
|
413
|
+
constructor(base, selectedCredential) {
|
|
414
|
+
this.base = base;
|
|
415
|
+
this.selectedCredential = selectedCredential;
|
|
416
|
+
}
|
|
417
|
+
getHolderKey() {
|
|
418
|
+
return this.base.getHolderKey();
|
|
419
|
+
}
|
|
420
|
+
setHolderKey(record) {
|
|
421
|
+
if (!record) throw new Error("holder key is required");
|
|
422
|
+
return this.base.setHolderKey(record);
|
|
423
|
+
}
|
|
424
|
+
async listCredentials() {
|
|
425
|
+
return [this.selectedCredential];
|
|
426
|
+
}
|
|
427
|
+
async getCredential(id) {
|
|
428
|
+
if (id !== this.selectedCredential.id) return null;
|
|
429
|
+
return this.selectedCredential;
|
|
430
|
+
}
|
|
431
|
+
setCredential(record) {
|
|
432
|
+
return this.base.setCredential(record);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/actions/present.ts
|
|
437
|
+
async function presentCredentialAction(rawOptions) {
|
|
438
|
+
const options = presentOptionsSchema.parse(rawOptions);
|
|
439
|
+
const request = await parsePresentationRequest(options.request);
|
|
440
|
+
const storage = new FileSystemWalletStorage(options.walletDir);
|
|
441
|
+
const wallet = options.credentialId ? await createSelectedWallet(storage, options.credentialId) : new Wallet(storage);
|
|
442
|
+
const selectedCredentials = options.credentialId ? void 0 : await maybeSelectCredentials(wallet, request, rawOptions.prompt);
|
|
443
|
+
const presentation = await wallet.createPresentation(request, { selectedCredentials });
|
|
444
|
+
const authorizationResponse = createOpenId4VpAuthorizationResponse(request, presentation);
|
|
445
|
+
const submission = !options.dryRun && (request.response_mode === "direct_post" || request.response_mode === "direct_post.jwt") ? await submitOpenId4VpAuthorizationResponse(request, authorizationResponse) : void 0;
|
|
446
|
+
return {
|
|
447
|
+
...presentation,
|
|
448
|
+
submitted: submission !== void 0,
|
|
449
|
+
submission
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function parsePresentationRequest(value) {
|
|
453
|
+
const trimmed = unwrapQuotedInput(value.trim());
|
|
454
|
+
if (trimmed.startsWith("openid4vp:")) {
|
|
455
|
+
verbose("Parsing openid4vp:// authorization URL");
|
|
456
|
+
return parseOpenid4VpAuthorizationUrl(trimmed);
|
|
457
|
+
}
|
|
458
|
+
verbose("Parsing inline OpenID4VP request JSON");
|
|
459
|
+
return resolveOpenId4VpRequest(JSON.parse(trimmed));
|
|
460
|
+
}
|
|
461
|
+
function unwrapQuotedInput(value) {
|
|
462
|
+
if (value.length < 2) return value;
|
|
463
|
+
const quote = value[0];
|
|
464
|
+
if ((quote === "\"" || quote === "'") && value.at(-1) === quote) return value.slice(1, -1).trim();
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
async function maybeSelectCredentials(wallet, request, prompt) {
|
|
468
|
+
const ambiguousQueries = (await wallet.inspectDcqlQuery(request)).queries.filter((queryMatch) => queryMatch.credentials.length > 1);
|
|
469
|
+
if (ambiguousQueries.length === 0) return;
|
|
470
|
+
const selections = {};
|
|
471
|
+
for (const queryMatch of ambiguousQueries) selections[queryMatch.queryId] = prompt ? await prompt(queryMatch) : await promptForCredentialSelection(queryMatch);
|
|
472
|
+
return selections;
|
|
473
|
+
}
|
|
474
|
+
async function promptForCredentialSelection(queryMatch) {
|
|
475
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
476
|
+
const candidates = queryMatch.credentials.map((c) => ` ${c.credentialId} (${c.vct}, ${c.issuer})`).join("\n");
|
|
477
|
+
throw new Error(`Multiple credentials match query "${queryMatch.queryId}":\n${candidates}\n\nRerun with --credential-id to select one, for example:\n openid4vc-wallet present --wallet-dir <dir> --request <value> --credential-id ${queryMatch.credentials[0]?.credentialId ?? "<id>"}`);
|
|
478
|
+
}
|
|
479
|
+
const prompt = new PromptSession();
|
|
480
|
+
try {
|
|
481
|
+
return await prompt.choose(`Multiple credentials match query ${queryMatch.queryId}`, queryMatch.credentials.map((credential) => ({
|
|
482
|
+
label: `${credential.credentialId} | ${credential.vct} | ${credential.issuer} | ${formatClaimPreview(credential.claims)}`,
|
|
483
|
+
value: credential.credentialId
|
|
484
|
+
})));
|
|
485
|
+
} finally {
|
|
486
|
+
prompt.close();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function formatClaimPreview(claims) {
|
|
490
|
+
const preview = Object.entries(claims).slice(0, 2).map(([key, value]) => `${key}=${formatClaimValue(value)}`).join(", ");
|
|
491
|
+
return preview.length > 0 ? preview : "no disclosed claims";
|
|
492
|
+
}
|
|
493
|
+
function formatClaimValue(value) {
|
|
494
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
495
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
496
|
+
if (value && typeof value === "object") return JSON.stringify(value);
|
|
497
|
+
return "?";
|
|
498
|
+
}
|
|
499
|
+
async function createSelectedWallet(storage, credentialId) {
|
|
500
|
+
const credential = await storage.getCredential(credentialId);
|
|
501
|
+
if (!credential) throw new Error(`Credential ${credentialId} not found`);
|
|
502
|
+
return new Wallet(new SelectedCredentialStorage(storage, credential));
|
|
503
|
+
}
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/actions/receive.ts
|
|
506
|
+
async function receiveCredentialAction(rawOptions) {
|
|
507
|
+
const options = receiveOptionsSchema.parse(rawOptions);
|
|
508
|
+
return { credential: await receiveCredentialFromOffer(new Wallet(new FileSystemWalletStorage(options.walletDir)), options.offer) };
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/actions/show.ts
|
|
512
|
+
async function showCredentialAction(rawOptions) {
|
|
513
|
+
const options = showOptionsSchema.parse(rawOptions);
|
|
514
|
+
const storage = new FileSystemWalletStorage(options.walletDir);
|
|
515
|
+
const wallet = new Wallet(storage);
|
|
516
|
+
const credential = await storage.getCredential(options.credentialId);
|
|
517
|
+
if (!credential) throw new Error(`Credential ${options.credentialId} not found`);
|
|
518
|
+
try {
|
|
519
|
+
return {
|
|
520
|
+
credential,
|
|
521
|
+
status: await wallet.getCredentialStatus(options.credentialId),
|
|
522
|
+
statusWarning: void 0
|
|
523
|
+
};
|
|
524
|
+
} catch (error) {
|
|
525
|
+
return {
|
|
526
|
+
credential,
|
|
527
|
+
status: null,
|
|
528
|
+
statusWarning: error instanceof Error ? error.message : String(error)
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/actions/interactive.ts
|
|
534
|
+
const interactiveChoices = [
|
|
535
|
+
{
|
|
536
|
+
label: "Receive credential offer",
|
|
537
|
+
value: "receive"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
label: "List credentials",
|
|
541
|
+
value: "list"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
label: "Show credential",
|
|
545
|
+
value: "show"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
label: "Delete credential",
|
|
549
|
+
value: "delete"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
label: "Delete all credentials",
|
|
553
|
+
value: "delete-all"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
label: "Present credential",
|
|
557
|
+
value: "present"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
label: "Import raw credential",
|
|
561
|
+
value: "import"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
label: "Reinitialize wallet",
|
|
565
|
+
value: "init"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
label: "Delete wallet initialization",
|
|
569
|
+
value: "delete-wallet"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
label: "Switch wallet directory",
|
|
573
|
+
value: "switch"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
label: "Exit",
|
|
577
|
+
value: "exit"
|
|
578
|
+
}
|
|
579
|
+
];
|
|
580
|
+
async function interactiveWalletAction(rawOptions) {
|
|
581
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive mode requires a TTY. Use an explicit subcommand for non-interactive usage.");
|
|
582
|
+
const options = rawOptions ?? {};
|
|
583
|
+
const prompt = new PromptSession();
|
|
584
|
+
let walletDir = await prompt.text("Wallet directory", { defaultValue: options.walletDir ?? "./wallet-data" });
|
|
585
|
+
try {
|
|
586
|
+
while (true) {
|
|
587
|
+
const choice = await prompt.choose("Wallet CLI", [...interactiveChoices]);
|
|
588
|
+
stdout.write("\n");
|
|
589
|
+
try {
|
|
590
|
+
const nextWalletDir = await runInteractiveChoice({
|
|
591
|
+
prompt,
|
|
592
|
+
walletDir,
|
|
593
|
+
choice
|
|
594
|
+
});
|
|
595
|
+
if (nextWalletDir === void 0) return;
|
|
596
|
+
walletDir = nextWalletDir;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
process.stderr.write(`${formatInteractiveError(error)}\n\n`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} finally {
|
|
602
|
+
prompt.close();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function runInteractiveChoice({ prompt, walletDir, choice }) {
|
|
606
|
+
switch (choice) {
|
|
607
|
+
case "exit": return;
|
|
608
|
+
case "switch": {
|
|
609
|
+
const nextWalletDir = await prompt.text("Wallet directory", { defaultValue: walletDir });
|
|
610
|
+
stdout.write(`Using wallet ${nextWalletDir}.\n\n`);
|
|
611
|
+
return nextWalletDir;
|
|
612
|
+
}
|
|
613
|
+
case "init": {
|
|
614
|
+
const result = await initWalletAction({
|
|
615
|
+
walletDir,
|
|
616
|
+
alg: await prompt.choose("Holder key algorithm", [
|
|
617
|
+
{
|
|
618
|
+
label: "ES256",
|
|
619
|
+
value: "ES256"
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
label: "ES384",
|
|
623
|
+
value: "ES384"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
label: "EdDSA",
|
|
627
|
+
value: "EdDSA"
|
|
628
|
+
}
|
|
629
|
+
])
|
|
630
|
+
});
|
|
631
|
+
stdout.write(`${formatInitResult({
|
|
632
|
+
walletDir,
|
|
633
|
+
holderKey: result.holderKey,
|
|
634
|
+
imported: result.imported
|
|
635
|
+
})}\n\n`);
|
|
636
|
+
return walletDir;
|
|
637
|
+
}
|
|
638
|
+
case "receive": {
|
|
639
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
640
|
+
const result = await receiveCredentialAction({
|
|
641
|
+
walletDir,
|
|
642
|
+
offer: await prompt.text("Credential offer or offer URI")
|
|
643
|
+
});
|
|
644
|
+
stdout.write(`${formatCredentialSummary("Received", result.credential)}\n\n`);
|
|
645
|
+
return walletDir;
|
|
646
|
+
}
|
|
647
|
+
case "import": {
|
|
648
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
649
|
+
const result = await importCredentialAction({
|
|
650
|
+
walletDir,
|
|
651
|
+
credential: await prompt.text("Compact dc+sd-jwt credential")
|
|
652
|
+
});
|
|
653
|
+
stdout.write(`${formatCredentialSummary("Imported", result.credential)}\n\n`);
|
|
654
|
+
return walletDir;
|
|
655
|
+
}
|
|
656
|
+
case "list": {
|
|
657
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
658
|
+
const result = await listCredentialsAction({ walletDir });
|
|
659
|
+
stdout.write(`${formatCredentialList(result.credentials)}\n\n`);
|
|
660
|
+
return walletDir;
|
|
661
|
+
}
|
|
662
|
+
case "show": {
|
|
663
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
664
|
+
const credentialId = await chooseCredentialId(prompt, walletDir);
|
|
665
|
+
if (credentialId === null) return walletDir;
|
|
666
|
+
const result = await showCredentialAction({
|
|
667
|
+
walletDir,
|
|
668
|
+
credentialId
|
|
669
|
+
});
|
|
670
|
+
if (result.statusWarning) process.stderr.write(`Warning: failed to resolve credential status: ${result.statusWarning}\n`);
|
|
671
|
+
stdout.write(`${formatCredentialDetails({
|
|
672
|
+
credential: result.credential,
|
|
673
|
+
status: result.status,
|
|
674
|
+
statusWarning: result.statusWarning
|
|
675
|
+
})}\n\n`);
|
|
676
|
+
return walletDir;
|
|
677
|
+
}
|
|
678
|
+
case "delete": {
|
|
679
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
680
|
+
const credentialId = await chooseCredentialId(prompt, walletDir);
|
|
681
|
+
if (credentialId === null) return walletDir;
|
|
682
|
+
if (!await prompt.confirm(`Delete credential ${credentialId}?`, false)) {
|
|
683
|
+
stdout.write("Action cancelled.\n\n");
|
|
684
|
+
return walletDir;
|
|
685
|
+
}
|
|
686
|
+
await deleteCredentialAction({
|
|
687
|
+
walletDir,
|
|
688
|
+
credentialId
|
|
689
|
+
});
|
|
690
|
+
stdout.write(`${formatDeleteCredentialSummary(credentialId)}\n\n`);
|
|
691
|
+
return walletDir;
|
|
692
|
+
}
|
|
693
|
+
case "delete-all": {
|
|
694
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
695
|
+
if (!await prompt.confirm("Delete all credentials?", false)) {
|
|
696
|
+
stdout.write("Action cancelled.\n\n");
|
|
697
|
+
return walletDir;
|
|
698
|
+
}
|
|
699
|
+
const result = await deleteAllCredentialsAction({ walletDir });
|
|
700
|
+
stdout.write(`${formatDeleteAllCredentialsSummary(result.deleted)}\n\n`);
|
|
701
|
+
return walletDir;
|
|
702
|
+
}
|
|
703
|
+
case "present": {
|
|
704
|
+
if (!await ensureWalletReady(prompt, walletDir)) return walletDir;
|
|
705
|
+
const result = await presentCredentialAction({
|
|
706
|
+
walletDir,
|
|
707
|
+
request: await prompt.text("OpenID4VP request JSON or openid4vp:// URL")
|
|
708
|
+
});
|
|
709
|
+
stdout.write(`${formatPresentationSummary(result)}\n\n`);
|
|
710
|
+
return walletDir;
|
|
711
|
+
}
|
|
712
|
+
case "delete-wallet":
|
|
713
|
+
if (!await walletExists(walletDir)) {
|
|
714
|
+
stdout.write(`Wallet ${walletDir} is not initialized yet.\n\n`);
|
|
715
|
+
return walletDir;
|
|
716
|
+
}
|
|
717
|
+
if (!await prompt.confirm(`Delete wallet initialization at ${walletDir} and exit?`, false)) {
|
|
718
|
+
stdout.write("Action cancelled.\n\n");
|
|
719
|
+
return walletDir;
|
|
720
|
+
}
|
|
721
|
+
await deleteWalletAction({ walletDir });
|
|
722
|
+
stdout.write(`${formatDeleteWalletSummary(walletDir)}\n\n`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function ensureWalletReady(prompt, walletDir) {
|
|
727
|
+
if (await walletExists(walletDir)) return true;
|
|
728
|
+
stdout.write(`Wallet ${walletDir} is not initialized yet.\n`);
|
|
729
|
+
if (!await prompt.confirm("Create it now?", true)) {
|
|
730
|
+
stdout.write("Action cancelled.\n\n");
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
const result = await initWalletAction({
|
|
734
|
+
walletDir,
|
|
735
|
+
alg: await prompt.choose("Holder key algorithm", [
|
|
736
|
+
{
|
|
737
|
+
label: "ES256",
|
|
738
|
+
value: "ES256"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
label: "ES384",
|
|
742
|
+
value: "ES384"
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
label: "EdDSA",
|
|
746
|
+
value: "EdDSA"
|
|
747
|
+
}
|
|
748
|
+
])
|
|
749
|
+
});
|
|
750
|
+
stdout.write(`${formatInitResult({
|
|
751
|
+
walletDir,
|
|
752
|
+
holderKey: result.holderKey,
|
|
753
|
+
imported: result.imported
|
|
754
|
+
})}\n\n`);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
async function walletExists(walletDir) {
|
|
758
|
+
try {
|
|
759
|
+
await access(walletDir);
|
|
760
|
+
await access(`${walletDir}/wallet.json`);
|
|
761
|
+
await access(`${walletDir}/holder-key.json`);
|
|
762
|
+
return true;
|
|
763
|
+
} catch {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function formatInteractiveError(error) {
|
|
768
|
+
if (error instanceof Error && error.message.trim()) return error.message;
|
|
769
|
+
return String(error);
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/program.ts
|
|
773
|
+
function createProgram(version) {
|
|
774
|
+
const program = new Command().name("openid4vc-wallet").version(version).description("Demo wallet CLI for OpenID4VCI receipt, credential storage, status resolution, and OpenID4VP presentation. Run without a subcommand to start interactive mode.").addHelpText("after", "\nInteractive mode:\n Run `openid4vc-wallet` without a subcommand to open the prompt-driven workflow.").option("--verbose", "Enable verbose logging to stderr", false).hook("preAction", (_thisCommand, actionCommand) => {
|
|
775
|
+
if (actionCommand.optsWithGlobals().verbose) setVerbose(true);
|
|
776
|
+
});
|
|
777
|
+
program.command("init").description("Initialize a wallet directory and create a holder key").requiredOption("--wallet-dir <dir>", "Path to the wallet storage directory (created if it does not exist)").option("--alg <algorithm>", "Holder key algorithm: ES256, ES384, or EdDSA (default: ES256)").option("--holder-key-file <file>", "Import an existing holder key from a JWK JSON file instead of generating one").option("--output <format>", "Output format: text or json", "text").addHelpText("after", `\nExamples:\n $ openid4vc-wallet init --wallet-dir ./my-wallet\n $ openid4vc-wallet init --wallet-dir ./my-wallet --alg EdDSA\n $ openid4vc-wallet init --wallet-dir ./my-wallet --holder-key-file ./existing-key.jwk.json\n $ openid4vc-wallet init --wallet-dir ./my-wallet --output json\n\nNotes:\n - Default output is a concise text summary; use --output json for full details\n - --holder-key-file accepts either a bare private JWK or an object with privateJwk/publicJwk fields\n - If the key algorithm cannot be inferred from the JWK, pass --alg explicitly`).action(async (options) => {
|
|
778
|
+
verbose(`Initializing wallet in ${options.walletDir}`);
|
|
779
|
+
const result = await initWalletAction(options);
|
|
780
|
+
if (options.output === "json") {
|
|
781
|
+
printResult(result, "json");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
printResult(formatInitResult({
|
|
785
|
+
walletDir: options.walletDir,
|
|
786
|
+
holderKey: result.holderKey,
|
|
787
|
+
imported: result.imported
|
|
788
|
+
}), "text");
|
|
789
|
+
});
|
|
790
|
+
program.command("receive").description("Receive and store a credential from an OpenID4VCI credential offer using issuer metadata discovery").requiredOption("--wallet-dir <dir>", "Path to the wallet storage directory").requiredOption("--offer <value>", "Credential offer JSON or an openid-credential-offer:// URI").option("--output <format>", "Output format: text or json", "text").addHelpText("after", `\nExamples:\n $ openid4vc-wallet receive \\
|
|
791
|
+
--wallet-dir ./my-wallet \\
|
|
792
|
+
--offer 'openid-credential-offer://?credential_offer=...'\n\n $ openid4vc-wallet receive \\
|
|
793
|
+
--wallet-dir ./my-wallet \\
|
|
794
|
+
--offer '{"credential_issuer":"https://issuer.example",...}'\n\n $ openid4vc-wallet receive \\
|
|
795
|
+
--wallet-dir ./my-wallet \\
|
|
796
|
+
--offer 'openid-credential-offer://?credential_offer=...' \\
|
|
797
|
+
--output json\n\nNotes:\n - Default output is a concise text summary; use --output json for full details\n - Supports by-value credential_offer and by-reference credential_offer_uri inputs\n - Resolves issuer metadata from credential_issuer via /.well-known/openid-credential-issuer[issuer-path]\n - Uses token_endpoint, credential_endpoint, and optional nonce_endpoint from the fetched metadata\n - Does not hardcode endpoint paths and does not expose manual endpoint overrides\n - Current flow covers the minimal OpenID4VCI subset: pre-authorized code, JWT proof, and single dc+sd-jwt issuance\n - A credential_offer_uri is fetched first, then redeemed like an inline offer`).action(async (options) => {
|
|
798
|
+
verbose(`Receiving credential into ${options.walletDir}`);
|
|
799
|
+
const result = await receiveCredentialAction(options);
|
|
800
|
+
if (options.output === "json") {
|
|
801
|
+
printResult(result, "json");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
printResult(formatCredentialSummary("Received", result.credential), "text");
|
|
805
|
+
});
|
|
806
|
+
program.command("import").description("Import an already-issued dc+sd-jwt credential into the wallet").requiredOption("--wallet-dir <dir>", "Path to the wallet storage directory").option("--credential <value>", "Inline credential text (compact dc+sd-jwt)").option("--credential-file <file>", "Path to a credential file (compact dc+sd-jwt text)").option("--output <format>", "Output format: text or json", "text").addHelpText("after", `\nExamples:\n $ openid4vc-wallet import \\
|
|
807
|
+
--wallet-dir ./my-wallet \\
|
|
808
|
+
--credential-file ./issuer/credential.txt\n\n $ openid4vc-wallet import \\
|
|
809
|
+
--wallet-dir ./my-wallet \\
|
|
810
|
+
--credential 'eyJ...'\n\n $ openid4vc-wallet import \\
|
|
811
|
+
--wallet-dir ./my-wallet \\
|
|
812
|
+
--credential-file ./issuer/credential.txt \\
|
|
813
|
+
--output json\n\nNotes:\n - Default output is a concise text summary; use --output json for full details\n - Provide exactly one of --credential or --credential-file\n - Prefer openid4vc-wallet receive when you have an OpenID4VCI credential offer\n - This command imports an already-issued compact dc+sd-jwt; it does not resolve credential offers`).action(async (options) => {
|
|
814
|
+
verbose(`Importing credential`);
|
|
815
|
+
const result = await importCredentialAction(options);
|
|
816
|
+
if (options.output === "json") {
|
|
817
|
+
printResult(result, "json");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
printResult(formatCredentialSummary("Imported", result.credential), "text");
|
|
821
|
+
});
|
|
822
|
+
program.command("list").description("List stored credentials in the wallet").requiredOption("--wallet-dir <dir>", "Path to the wallet storage directory").option("--vct <uri>", "Filter by Verifiable Credential Type URI (e.g. urn:eudi:pid:1)").option("--issuer <url>", "Filter by issuer identifier URL (e.g. https://issuer.example)").option("--output <format>", "Output format: text or json", "text").addHelpText("after", `\nExamples:\n $ openid4vc-wallet list --wallet-dir ./my-wallet\n $ openid4vc-wallet list --wallet-dir ./my-wallet --vct urn:eudi:pid:1\n $ openid4vc-wallet list --wallet-dir ./my-wallet --issuer https://issuer.example\n $ openid4vc-wallet list --wallet-dir ./my-wallet --output json`).action(async (options) => {
|
|
823
|
+
verbose(`Listing credentials in ${options.walletDir}`);
|
|
824
|
+
const result = await listCredentialsAction(options);
|
|
825
|
+
if (options.output === "json") {
|
|
826
|
+
printResult(result, "json");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
printResult(formatCredentialList(result.credentials), "text");
|
|
830
|
+
});
|
|
831
|
+
program.command("show").description("Show a single stored credential by id").requiredOption("--wallet-dir <dir>", "Path to the wallet storage directory").requiredOption("--credential-id <id>", "Credential id (from list output) to display").option("--output <format>", "Output format: text, json, or raw (compact sd-jwt text)", "text").addHelpText("after", `\nExamples:\n $ openid4vc-wallet show --wallet-dir ./my-wallet --credential-id <id>\n $ openid4vc-wallet show --wallet-dir ./my-wallet --credential-id <id> --output raw\n $ openid4vc-wallet show --wallet-dir ./my-wallet --credential-id <id> --output json\n\nNotes:\n - Status resolution runs automatically when the stored credential has a status reference\n - If status resolution fails, the credential is still shown and a warning is printed\n - Default output is a sectioned text view; use --output json for full details\n - --output raw prints only the compact sd-jwt credential text`).action(async (options) => {
|
|
832
|
+
verbose(`Showing credential ${options.credentialId}`);
|
|
833
|
+
const result = await showCredentialAction(options);
|
|
834
|
+
if (options.output === "raw") {
|
|
835
|
+
process.stdout.write(`${result.credential.compactSdJwt}\n`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (result.statusWarning) process.stderr.write(`Warning: failed to resolve credential status: ${result.statusWarning}\n`);
|
|
839
|
+
if (options.output === "text") {
|
|
840
|
+
printResult(formatCredentialDetails({
|
|
841
|
+
credential: result.credential,
|
|
842
|
+
status: result.status,
|
|
843
|
+
statusWarning: result.statusWarning
|
|
844
|
+
}), "text");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
printResult(result, options.output);
|
|
848
|
+
});
|
|
849
|
+
program.command("present").description("Create a DCQL-based OpenID4VP presentation from wallet credentials").requiredOption("--wallet-dir <dir>", "Path to the wallet storage directory").requiredOption("--request <value>", "OpenID4VP request JSON or an openid4vp:// authorization URL").option("--credential-id <id>", "Use a specific credential for the presentation (skip selection prompt)").option("--dry-run", "Build the VP response but do not submit it to the verifier").option("--output <format>", "Output format: text, json, or raw (vp_token text only)", "text").addHelpText("after", `\nExamples:\n $ openid4vc-wallet present \\
|
|
850
|
+
--wallet-dir ./my-wallet \\
|
|
851
|
+
--request 'openid4vp://authorize?...'\n\n $ openid4vc-wallet present \\
|
|
852
|
+
--wallet-dir ./my-wallet \\
|
|
853
|
+
--request '{"client_id":"https://verifier.example","nonce":"...","dcql_query":{...}}' \\
|
|
854
|
+
--credential-id <id> \\
|
|
855
|
+
--dry-run\n\n $ openid4vc-wallet present \\
|
|
856
|
+
--wallet-dir ./my-wallet \\
|
|
857
|
+
--request 'openid4vp://authorize?...' \\
|
|
858
|
+
--output raw\n\n $ openid4vc-wallet present \\
|
|
859
|
+
--wallet-dir ./my-wallet \\
|
|
860
|
+
--request 'openid4vp://authorize?...' \\
|
|
861
|
+
--output json\n\nNotes:\n - Default output is a concise text summary; use --output json for full details\n - --output raw prints only the vp_token\n - direct_post and direct_post.jwt requests are auto-submitted unless --dry-run is set\n - If multiple credentials match and --credential-id is omitted, the CLI prompts in a TTY and errors in non-interactive environments`).action(async (options) => {
|
|
862
|
+
verbose(`Creating presentation from ${options.walletDir}`);
|
|
863
|
+
const result = await presentCredentialAction(options);
|
|
864
|
+
if (options.output === "raw") {
|
|
865
|
+
process.stdout.write(`${result.vpToken}\n`);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (options.output === "text") {
|
|
869
|
+
printResult(formatPresentationSummary(result), "text");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
printResult(result, options.output);
|
|
873
|
+
});
|
|
874
|
+
return program;
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/index.ts
|
|
878
|
+
function parseInteractiveCliOptions(argv) {
|
|
879
|
+
const options = {};
|
|
880
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
881
|
+
const arg = argv[index];
|
|
882
|
+
if (!arg) continue;
|
|
883
|
+
if (arg === "--") break;
|
|
884
|
+
if (arg === "--verbose") {
|
|
885
|
+
options.verbose = true;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (arg === "--wallet-dir") {
|
|
889
|
+
const value = argv[index + 1];
|
|
890
|
+
if (!value || value.startsWith("-")) throw new Error("option '--wallet-dir <dir>' argument missing");
|
|
891
|
+
options.walletDir = value;
|
|
892
|
+
index += 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (arg.startsWith("--wallet-dir=")) options.walletDir = arg.slice(13);
|
|
896
|
+
}
|
|
897
|
+
return options;
|
|
898
|
+
}
|
|
899
|
+
function findFirstPositionalArg(argv) {
|
|
900
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
901
|
+
const arg = argv[index];
|
|
902
|
+
if (!arg) continue;
|
|
903
|
+
if (arg === "--") return argv[index + 1];
|
|
904
|
+
if (arg === "--wallet-dir") {
|
|
905
|
+
index += 1;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (arg.startsWith("-")) continue;
|
|
909
|
+
return arg;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async function runCli(argv = process.argv) {
|
|
913
|
+
const program = createProgram(await resolveCliVersion(resolvePackageJsonPath(import.meta.url)));
|
|
914
|
+
try {
|
|
915
|
+
const firstPositionalArg = findFirstPositionalArg(argv);
|
|
916
|
+
const commandNames = new Set(program.commands.map((command) => command.name()));
|
|
917
|
+
if (firstPositionalArg === void 0 && !argv.includes("--help") && !argv.includes("-h") && !argv.includes("--version") && !argv.includes("-V")) {
|
|
918
|
+
const interactiveOptions = parseInteractiveCliOptions(argv);
|
|
919
|
+
if (interactiveOptions.verbose) setVerbose(true);
|
|
920
|
+
await interactiveWalletAction(interactiveOptions);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (firstPositionalArg !== void 0 && !commandNames.has(firstPositionalArg)) {
|
|
924
|
+
await program.parseAsync(argv);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
await program.parseAsync(argv);
|
|
928
|
+
} catch (error) {
|
|
929
|
+
handleCliError(error);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? process.cwd()).href) runCli().catch((error) => {
|
|
933
|
+
handleCliError(error);
|
|
934
|
+
});
|
|
935
|
+
//#endregion
|
|
936
|
+
export { createProgram, deleteAllCredentialsAction, deleteCredentialAction, deleteWalletAction, importCredentialAction, initWalletAction, interactiveWalletAction, listCredentialsAction, parseInteractiveCliOptions, presentCredentialAction, receiveCredentialAction, runCli, showCredentialAction };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vidos-id/openid4vc-wallet-cli",
|
|
3
|
+
"description": "CLI for dc+sd-jwt wallet storage and OpenID4VP presentation.",
|
|
4
|
+
"version": "0.0.0-test1",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/vidos-id/openid4vc-tools.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.mjs",
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"openid4vc-wallet": "./dist/index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"development": {
|
|
21
|
+
"types": "./src/index.ts",
|
|
22
|
+
"default": "./src/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"default": "./dist/index.mjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"registry": "https://registry.npmjs.org/",
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"check-types": "tsc --noEmit --project ../../tsconfig.json",
|
|
35
|
+
"publish": "bun publish",
|
|
36
|
+
"test": "bun --conditions=development test --pass-with-no-tests"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@vidos-id/openid4vc-cli-common": "0.0.0-test1",
|
|
44
|
+
"@vidos-id/openid4vc-wallet": "0.0.0-test1",
|
|
45
|
+
"commander": "^14.0.3",
|
|
46
|
+
"inquirer": "^12.9.6",
|
|
47
|
+
"zod": "^4.3.6"
|
|
48
|
+
}
|
|
49
|
+
}
|