@vidos-id/openid4vc-wallet-cli 0.0.0-rc.1

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 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
+ ```
@@ -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,50 @@
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-rc.1",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/vidos-id/openid4vc-tools.git",
8
+ "directory": "packages/wallet-cli"
9
+ },
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=20"
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "types": "./dist/index.d.mts",
16
+ "bin": {
17
+ "openid4vc-wallet": "./dist/index.mjs"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "development": {
22
+ "types": "./src/index.ts",
23
+ "default": "./src/index.ts"
24
+ },
25
+ "types": "./dist/index.d.mts",
26
+ "default": "./dist/index.mjs"
27
+ }
28
+ },
29
+ "publishConfig": {
30
+ "registry": "https://registry.npmjs.org/",
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "check-types": "tsc --noEmit --project ../../tsconfig.json",
36
+ "package-publish": "bun publish",
37
+ "test": "bun --conditions=development test --pass-with-no-tests"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md"
42
+ ],
43
+ "dependencies": {
44
+ "@vidos-id/openid4vc-cli-common": "0.0.0-rc.1",
45
+ "@vidos-id/openid4vc-wallet": "0.0.0-rc.1",
46
+ "commander": "^14.0.3",
47
+ "inquirer": "^12.9.6",
48
+ "zod": "^4.3.6"
49
+ }
50
+ }