@vidos-id/openid4vc-issuer-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/dist/index.mjs ADDED
@@ -0,0 +1,1055 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import { handleCliError, printResult, readTextInput, resolveCliVersion, resolvePackageJsonPath, setVerbose, verbose } from "@vidos-id/openid4vc-cli-common";
4
+ import { Command } from "commander";
5
+ import { ACTIVE_TOKEN_STATUS, REVOKED_TOKEN_STATUS, SUSPENDED_TOKEN_STATUS, createIssuanceInputSchema, createTemplateInputSchema, deleteResponseSchema, getTokenStatusLabel, issuanceDetailSchema, issuanceSchema, sessionResponseSchema, templateSchema, updateIssuanceStatusInputSchema } from "@vidos-id/openid4vc-issuer-web-shared";
6
+ import { z } from "zod";
7
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import { stdout } from "node:process";
11
+ import inquirer from "inquirer";
12
+ //#region src/schemas.ts
13
+ const serverUrlSchema = z.url();
14
+ const sessionFileSchema = z.object({
15
+ serverUrl: serverUrlSchema,
16
+ cookieHeader: z.string().min(1),
17
+ user: sessionResponseSchema.shape.user
18
+ });
19
+ const baseCliOptionsSchema = z.object({
20
+ serverUrl: serverUrlSchema.optional(),
21
+ sessionFile: z.string().min(1).optional()
22
+ });
23
+ const authSignInOptionsSchema = baseCliOptionsSchema.extend({
24
+ anonymous: z.boolean().optional(),
25
+ username: z.string().min(1).optional(),
26
+ password: z.string().min(1).optional()
27
+ }).superRefine((value, ctx) => {
28
+ if (value.anonymous) {
29
+ if (value.username || value.password) ctx.addIssue({
30
+ code: z.ZodIssueCode.custom,
31
+ message: "Anonymous sign-in cannot be combined with --username or --password",
32
+ path: ["anonymous"]
33
+ });
34
+ return;
35
+ }
36
+ if (!value.username || !value.password) ctx.addIssue({
37
+ code: z.ZodIssueCode.custom,
38
+ message: "Provide --anonymous or both --username and --password",
39
+ path: ["username"]
40
+ });
41
+ });
42
+ const authSignUpOptionsSchema = baseCliOptionsSchema.extend({
43
+ username: z.string().min(1),
44
+ password: z.string().min(1)
45
+ });
46
+ const claimsInputSchema = z.object({
47
+ claims: z.string().optional(),
48
+ claimsFile: z.string().min(1).optional()
49
+ }).superRefine((value, ctx) => {
50
+ if (value.claims && value.claimsFile) ctx.addIssue({
51
+ code: z.ZodIssueCode.custom,
52
+ message: "Use only one of --claims or --claims-file",
53
+ path: ["claims"]
54
+ });
55
+ });
56
+ const templateCreateOptionsSchema = baseCliOptionsSchema.extend({
57
+ name: z.string().min(1),
58
+ vct: z.string().min(1)
59
+ }).and(claimsInputSchema);
60
+ const templateDeleteOptionsSchema = baseCliOptionsSchema.extend({ templateId: z.string().min(1) });
61
+ const issuanceStatusLabelSchema = z.enum([
62
+ "active",
63
+ "revoked",
64
+ "suspended"
65
+ ]);
66
+ const issuanceCreateOptionsSchema = baseCliOptionsSchema.extend({
67
+ templateId: z.string().min(1),
68
+ status: issuanceStatusLabelSchema.optional()
69
+ }).and(claimsInputSchema);
70
+ const issuanceIdOptionsSchema = baseCliOptionsSchema.extend({ issuanceId: z.string().min(1) });
71
+ const issuanceStatusUpdateOptionsSchema = issuanceIdOptionsSchema.extend({ status: issuanceStatusLabelSchema });
72
+ const interactiveOptionsSchema = baseCliOptionsSchema;
73
+ const authApiResponseSchema = z.object({
74
+ token: z.string().nullable().optional(),
75
+ user: z.record(z.string(), z.unknown()).optional(),
76
+ message: z.string().optional(),
77
+ error: z.string().optional(),
78
+ code: z.string().optional()
79
+ });
80
+ const appErrorResponseSchema = z.object({
81
+ error: z.string().optional(),
82
+ message: z.string().optional(),
83
+ code: z.string().optional()
84
+ });
85
+ const templateListSchema = z.array(templateSchema);
86
+ const issuanceListSchema = z.array(issuanceSchema);
87
+ const issuerMetadataSchema = z.object({
88
+ credential_issuer: z.string().url(),
89
+ token_endpoint: z.string().url(),
90
+ credential_endpoint: z.string().url(),
91
+ nonce_endpoint: z.string().url().optional(),
92
+ jwks: z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) }),
93
+ credential_configurations_supported: z.record(z.string(), z.record(z.string(), z.unknown()))
94
+ });
95
+ //#endregion
96
+ //#region src/client.ts
97
+ var IssuerWebClient = class {
98
+ cookieHeader = "";
99
+ constructor(options) {
100
+ this.options = options;
101
+ this.cookieHeader = options.session?.cookieHeader ?? "";
102
+ }
103
+ get serverUrl() {
104
+ return this.options.serverUrl;
105
+ }
106
+ getCookieHeader() {
107
+ return this.cookieHeader;
108
+ }
109
+ async signInAnonymous() {
110
+ const response = await this.request("/api/auth/sign-in/anonymous", { method: "POST" });
111
+ authApiResponseSchema.parse(await this.parseJson(response, "anonymous sign-in"));
112
+ return this.getSession();
113
+ }
114
+ async signInUsername(input) {
115
+ const response = await this.request("/api/auth/sign-in/username", {
116
+ method: "POST",
117
+ body: input
118
+ });
119
+ authApiResponseSchema.parse(await this.parseJson(response, "username sign-in"));
120
+ return this.getSession();
121
+ }
122
+ async signUpUsername(input) {
123
+ const username = input.username.trim();
124
+ const response = await this.request("/api/auth/sign-up/email", {
125
+ method: "POST",
126
+ body: {
127
+ email: `temp-${crypto.randomUUID()}@issuer-web.local`,
128
+ name: username,
129
+ username,
130
+ password: input.password
131
+ }
132
+ });
133
+ authApiResponseSchema.parse(await this.parseJson(response, "sign-up"));
134
+ return this.getSession();
135
+ }
136
+ async signOut() {
137
+ await this.request("/api/auth/sign-out", { method: "POST" });
138
+ this.cookieHeader = "";
139
+ }
140
+ async getSession() {
141
+ const response = await this.request("/api/session");
142
+ return sessionResponseSchema.parse(await this.parseJson(response, "session"));
143
+ }
144
+ async getMetadata() {
145
+ const response = await this.request("/.well-known/openid-credential-issuer");
146
+ return issuerMetadataSchema.parse(await this.parseJson(response, "issuer metadata"));
147
+ }
148
+ async listTemplates() {
149
+ const response = await this.request("/api/templates");
150
+ return templateListSchema.parse(await this.parseJson(response, "template list"));
151
+ }
152
+ async createTemplate(input) {
153
+ const payload = createTemplateInputSchema.parse(input);
154
+ const response = await this.request("/api/templates", {
155
+ method: "POST",
156
+ body: payload
157
+ });
158
+ return templateSchema.parse(await this.parseJson(response, "template creation"));
159
+ }
160
+ async deleteTemplate(templateId) {
161
+ const response = await this.request(`/api/templates/${templateId}`, { method: "DELETE" });
162
+ deleteResponseSchema.parse(await this.parseJson(response, "template deletion"));
163
+ }
164
+ async listIssuances() {
165
+ const response = await this.request("/api/issuances");
166
+ return issuanceListSchema.parse(await this.parseJson(response, "issuance list"));
167
+ }
168
+ async createIssuance(input) {
169
+ const payload = createIssuanceInputSchema.parse(input);
170
+ const response = await this.request("/api/issuances", {
171
+ method: "POST",
172
+ body: payload
173
+ });
174
+ return issuanceDetailSchema.parse(await this.parseJson(response, "issuance creation"));
175
+ }
176
+ async getIssuance(issuanceId) {
177
+ const response = await this.request(`/api/issuances/${issuanceId}`);
178
+ return issuanceDetailSchema.parse(await this.parseJson(response, "issuance detail"));
179
+ }
180
+ async updateIssuanceStatus(issuanceId, input) {
181
+ const payload = updateIssuanceStatusInputSchema.parse(input);
182
+ const response = await this.request(`/api/issuances/${issuanceId}/status`, {
183
+ method: "PATCH",
184
+ body: payload
185
+ });
186
+ return issuanceDetailSchema.parse(await this.parseJson(response, "issuance status update"));
187
+ }
188
+ async request(path, init = {}) {
189
+ const headers = new Headers(init.headers);
190
+ headers.set("accept", "application/json");
191
+ if (this.cookieHeader) headers.set("cookie", this.cookieHeader);
192
+ let body = init.body;
193
+ if (body && typeof body === "object" && !(body instanceof FormData) && !(body instanceof URLSearchParams) && !(body instanceof Blob) && !(body instanceof ArrayBuffer)) {
194
+ headers.set("content-type", "application/json");
195
+ body = JSON.stringify(body);
196
+ }
197
+ const url = new URL(path, this.options.serverUrl);
198
+ verbose(`Requesting ${init.method ?? "GET"} ${url}`);
199
+ const response = await (this.options.fetchImpl ?? fetch)(url, {
200
+ ...init,
201
+ headers,
202
+ body
203
+ });
204
+ this.updateCookies(response);
205
+ if (!response.ok) throw await this.createRequestError(response);
206
+ return response;
207
+ }
208
+ updateCookies(response) {
209
+ const setCookies = getSetCookies(response.headers);
210
+ if (setCookies.length === 0) return;
211
+ this.cookieHeader = setCookies.map((value) => value.split(";")[0]).filter(Boolean).join("; ");
212
+ }
213
+ async createRequestError(response) {
214
+ let message = `Request failed with status ${response.status}`;
215
+ try {
216
+ const payload = appErrorResponseSchema.safeParse(await response.json());
217
+ if (payload.success) message = payload.data.message ?? payload.data.error ?? message;
218
+ } catch {
219
+ try {
220
+ const text = (await response.text()).trim();
221
+ if (text) message = text;
222
+ } catch {}
223
+ }
224
+ return new Error(message);
225
+ }
226
+ async parseJson(response, label) {
227
+ try {
228
+ return await response.json();
229
+ } catch {
230
+ throw new Error(`Failed to parse ${label} response`);
231
+ }
232
+ }
233
+ };
234
+ function statusLabelToValue(label) {
235
+ if (label === "active") return ACTIVE_TOKEN_STATUS;
236
+ if (label === "revoked") return REVOKED_TOKEN_STATUS;
237
+ return SUSPENDED_TOKEN_STATUS;
238
+ }
239
+ function getSetCookies(headers) {
240
+ const withGetSetCookie = headers;
241
+ if (typeof withGetSetCookie.getSetCookie === "function") return withGetSetCookie.getSetCookie();
242
+ const combined = headers.get("set-cookie");
243
+ if (!combined) return [];
244
+ return combined.split(", ").filter((part) => part.includes("="));
245
+ }
246
+ //#endregion
247
+ //#region src/session.ts
248
+ const DEFAULT_SERVER_URL = "http://localhost:3001";
249
+ function resolveDefaultSessionFilePath() {
250
+ return join(homedir(), ".config", "vidos-id", "openid4vc-issuer-session.json");
251
+ }
252
+ function resolveSessionFilePath(options) {
253
+ return options?.sessionFile ?? resolveDefaultSessionFilePath();
254
+ }
255
+ async function readStoredSession(options) {
256
+ const filePath = resolveSessionFilePath(options);
257
+ try {
258
+ const raw = JSON.parse(await readFile(filePath, "utf8"));
259
+ return sessionFileSchema.parse(raw);
260
+ } catch (error) {
261
+ if (error.code === "ENOENT") return null;
262
+ throw error;
263
+ }
264
+ }
265
+ async function writeStoredSession(session, options) {
266
+ const filePath = resolveSessionFilePath(options);
267
+ await mkdir(dirname(filePath), { recursive: true });
268
+ await writeFile(filePath, `${JSON.stringify(session, null, 2)}\n`, "utf8");
269
+ verbose(`Saved openid4vc-issuer session to ${filePath}`);
270
+ return filePath;
271
+ }
272
+ async function clearStoredSession(options) {
273
+ const filePath = resolveSessionFilePath(options);
274
+ await rm(filePath, { force: true });
275
+ verbose(`Cleared openid4vc-issuer session at ${filePath}`);
276
+ return filePath;
277
+ }
278
+ async function requireStoredSession(options) {
279
+ const session = await readStoredSession(options);
280
+ if (!session) throw new Error("No saved issuer session. Run `openid4vc-issuer auth signin` or `openid4vc-issuer` first.");
281
+ return session;
282
+ }
283
+ function resolveServerUrl(options, session) {
284
+ return options?.serverUrl ?? session?.serverUrl ?? DEFAULT_SERVER_URL;
285
+ }
286
+ function assertSessionMatchesServerUrl(serverUrl, session) {
287
+ if (session.serverUrl !== serverUrl) throw new Error(`Saved session targets ${session.serverUrl}. Sign in again for ${serverUrl} or omit --server-url.`);
288
+ }
289
+ //#endregion
290
+ //#region src/actions/auth.ts
291
+ async function authSignInAction(rawOptions, deps = {}) {
292
+ const options = authSignInOptionsSchema.parse(rawOptions);
293
+ const serverUrl = resolveServerUrl(options);
294
+ const client = new IssuerWebClient({
295
+ serverUrl,
296
+ fetchImpl: deps.fetchImpl
297
+ });
298
+ const user = requireUser(options.anonymous ? await client.signInAnonymous() : await signInWithUsernamePassword(client, options));
299
+ await writeStoredSession({
300
+ serverUrl,
301
+ cookieHeader: client.getCookieHeader(),
302
+ user
303
+ }, options);
304
+ return {
305
+ serverUrl,
306
+ user
307
+ };
308
+ }
309
+ async function authSignUpAction(rawOptions, deps = {}) {
310
+ const options = authSignUpOptionsSchema.parse(rawOptions);
311
+ const serverUrl = resolveServerUrl(options);
312
+ const client = new IssuerWebClient({
313
+ serverUrl,
314
+ fetchImpl: deps.fetchImpl
315
+ });
316
+ const user = requireUser(await client.signUpUsername({
317
+ username: options.username,
318
+ password: options.password
319
+ }));
320
+ await writeStoredSession({
321
+ serverUrl,
322
+ cookieHeader: client.getCookieHeader(),
323
+ user
324
+ }, options);
325
+ return {
326
+ serverUrl,
327
+ user
328
+ };
329
+ }
330
+ async function authWhoAmIAction(rawOptions, deps = {}) {
331
+ const options = baseCliOptionsSchema.parse(rawOptions);
332
+ const stored = await requireStoredSession(options);
333
+ const serverUrl = resolveServerUrl(options, stored);
334
+ assertSessionMatchesServerUrl(serverUrl, stored);
335
+ const client = new IssuerWebClient({
336
+ serverUrl,
337
+ fetchImpl: deps.fetchImpl,
338
+ session: stored
339
+ });
340
+ const user = requireUser(await client.getSession());
341
+ await writeStoredSession({
342
+ serverUrl,
343
+ cookieHeader: client.getCookieHeader(),
344
+ user
345
+ }, options);
346
+ return {
347
+ serverUrl,
348
+ user
349
+ };
350
+ }
351
+ async function authSignOutAction(rawOptions, deps = {}) {
352
+ const options = baseCliOptionsSchema.parse(rawOptions);
353
+ const stored = await requireStoredSession(options);
354
+ const serverUrl = resolveServerUrl(options, stored);
355
+ assertSessionMatchesServerUrl(serverUrl, stored);
356
+ const client = new IssuerWebClient({
357
+ serverUrl,
358
+ fetchImpl: deps.fetchImpl,
359
+ session: stored
360
+ });
361
+ try {
362
+ await client.signOut();
363
+ } finally {
364
+ await clearStoredSession(options);
365
+ }
366
+ return { serverUrl };
367
+ }
368
+ async function signInWithUsernamePassword(client, options) {
369
+ if (!options.username || !options.password) throw new Error("Provide --anonymous or both --username and --password");
370
+ return client.signInUsername({
371
+ username: options.username,
372
+ password: options.password
373
+ });
374
+ }
375
+ function requireUser(session) {
376
+ if (!session.user) throw new Error("Authentication succeeded but no active session was returned.");
377
+ return session.user;
378
+ }
379
+ //#endregion
380
+ //#region src/format.ts
381
+ function section(title, lines) {
382
+ return [title, ...lines.map((line) => ` ${line}`)].join("\n");
383
+ }
384
+ function jsonBlock(value) {
385
+ return JSON.stringify(value, null, 2).split("\n").map((line) => ` ${line}`).join("\n");
386
+ }
387
+ function x5cToPem(x5c) {
388
+ const lines = ["-----BEGIN CERTIFICATE-----"];
389
+ for (let i = 0; i < x5c.length; i += 64) lines.push(x5c.slice(i, i + 64));
390
+ lines.push("-----END CERTIFICATE-----");
391
+ return lines.join("\n");
392
+ }
393
+ function formatSessionSummary(input) {
394
+ return section("Session", [
395
+ `server: ${input.serverUrl}`,
396
+ `user: ${input.user.username ?? input.user.name}`,
397
+ `name: ${input.user.name}`,
398
+ `mode: ${input.user.isAnonymous ? "guest" : "username"}`,
399
+ `user id: ${input.user.id}`
400
+ ]);
401
+ }
402
+ function formatTemplateList(templates) {
403
+ if (templates.length === 0) return "No templates found.";
404
+ return [`Templates (${templates.length})`, ...templates.flatMap((template, index) => [
405
+ `${index + 1}. ${template.name} [${template.kind}]`,
406
+ ` id: ${template.id}`,
407
+ ` vct: ${template.vct}`,
408
+ ` configuration: ${template.credentialConfigurationId}`
409
+ ])].join("\n");
410
+ }
411
+ function formatTemplateSummary(template) {
412
+ return [
413
+ section("Template", [
414
+ `name: ${template.name}`,
415
+ `id: ${template.id}`,
416
+ `kind: ${template.kind}`,
417
+ `vct: ${template.vct}`,
418
+ `configuration: ${template.credentialConfigurationId}`
419
+ ]),
420
+ "",
421
+ "Default claims",
422
+ jsonBlock(template.defaultClaims)
423
+ ].join("\n");
424
+ }
425
+ function formatIssuanceList(issuances) {
426
+ if (issuances.length === 0) return "No issuances found.";
427
+ return [`Issuances (${issuances.length})`, ...issuances.flatMap((issuance, index) => [
428
+ `${index + 1}. ${issuance.vct}`,
429
+ ` id: ${issuance.id}`,
430
+ ` state: ${issuance.state}`,
431
+ ` status: ${getTokenStatusLabel(issuance.status)}`,
432
+ ` created: ${issuance.createdAt}`
433
+ ])].join("\n");
434
+ }
435
+ function formatIssuanceSummary(detail) {
436
+ const { issuance } = detail;
437
+ return [
438
+ section("Issuance", [
439
+ `id: ${issuance.id}`,
440
+ `template: ${issuance.templateId}`,
441
+ `vct: ${issuance.vct}`,
442
+ `state: ${issuance.state}`,
443
+ `status: ${getTokenStatusLabel(issuance.status)}`,
444
+ `created: ${issuance.createdAt}`
445
+ ]),
446
+ "",
447
+ "Offer URI",
448
+ ` ${issuance.offerUri}`,
449
+ "",
450
+ "Claims",
451
+ jsonBlock(issuance.claims)
452
+ ].join("\n");
453
+ }
454
+ function formatDeletedTemplate(templateId) {
455
+ return `Deleted template ${templateId}.`;
456
+ }
457
+ function formatSignedOut(serverUrl) {
458
+ return serverUrl ? `Signed out from ${serverUrl}.` : "Signed out.";
459
+ }
460
+ function formatIssuerMetadata(metadata) {
461
+ const endpointLines = [
462
+ `credential issuer: ${metadata.credential_issuer}`,
463
+ `token endpoint: ${metadata.token_endpoint}`,
464
+ `credential endpoint: ${metadata.credential_endpoint}`
465
+ ];
466
+ if (metadata.nonce_endpoint) endpointLines.push(`nonce endpoint: ${metadata.nonce_endpoint}`);
467
+ const signingKeyLines = metadata.jwks.keys.flatMap((key, index) => {
468
+ const lines = [
469
+ `Key ${index + 1}`,
470
+ ` kid: ${typeof key.kid === "string" ? key.kid : "-"}`,
471
+ ` alg: ${typeof key.alg === "string" ? key.alg : "-"}`,
472
+ ` kty: ${typeof key.kty === "string" ? key.kty : "-"}`,
473
+ " jwk:",
474
+ jsonBlock(Object.fromEntries(Object.entries(key).filter(([name]) => name !== "x5c")))
475
+ ];
476
+ const x5cValues = Array.isArray(key.x5c) ? key.x5c.filter((value) => typeof value === "string") : [];
477
+ if (x5cValues.length === 0) {
478
+ lines.push(" x5c: none");
479
+ return lines;
480
+ }
481
+ for (const [certIndex, cert] of x5cValues.entries()) {
482
+ lines.push(` certificate ${certIndex + 1}:`);
483
+ lines.push(...x5cToPem(cert).split("\n").map((line) => ` ${line}`));
484
+ }
485
+ return lines;
486
+ });
487
+ const credentialConfigurationLines = Object.entries(metadata.credential_configurations_supported).flatMap(([configId, config]) => [configId, jsonBlock(config)]);
488
+ return [
489
+ section("Endpoints", endpointLines),
490
+ "",
491
+ section("Signing Keys", signingKeyLines),
492
+ "",
493
+ section("Credential Configurations Supported", credentialConfigurationLines)
494
+ ].join("\n");
495
+ }
496
+ //#endregion
497
+ //#region src/prompts.ts
498
+ var PromptSession = class {
499
+ close() {}
500
+ async text(label, options) {
501
+ const { value } = await inquirer.prompt([{
502
+ type: options?.password ? "password" : "input",
503
+ name: "value",
504
+ message: label,
505
+ default: options?.defaultValue,
506
+ mask: options?.password ? "*" : void 0,
507
+ validate: (input) => {
508
+ if (input.trim() || options?.allowEmpty || options?.defaultValue !== void 0) return true;
509
+ return "A value is required";
510
+ }
511
+ }]);
512
+ return value.trim() || options?.defaultValue || "";
513
+ }
514
+ async choose(label, choices) {
515
+ const { value } = await inquirer.prompt([{
516
+ type: "list",
517
+ name: "value",
518
+ message: label,
519
+ choices: choices.map((choice) => ({
520
+ name: choice.label,
521
+ value: choice.value
522
+ }))
523
+ }]);
524
+ return value;
525
+ }
526
+ async confirm(label, defaultValue = true) {
527
+ const { value } = await inquirer.prompt([{
528
+ type: "confirm",
529
+ name: "value",
530
+ message: label,
531
+ default: defaultValue
532
+ }]);
533
+ return value;
534
+ }
535
+ };
536
+ //#endregion
537
+ //#region src/actions/issuances.ts
538
+ async function listIssuancesAction(rawOptions, deps = {}) {
539
+ const { client, options, serverUrl, session } = await getAuthenticatedClient$1(baseCliOptionsSchema.parse(rawOptions), deps);
540
+ const issuances = await client.listIssuances();
541
+ await writeStoredSession({
542
+ ...session,
543
+ serverUrl,
544
+ cookieHeader: client.getCookieHeader()
545
+ }, options);
546
+ return {
547
+ serverUrl,
548
+ issuances
549
+ };
550
+ }
551
+ async function createIssuanceAction(rawOptions, deps = {}) {
552
+ const options = issuanceCreateOptionsSchema.parse(rawOptions);
553
+ const { client, serverUrl, session } = await getAuthenticatedClient$1(options, deps);
554
+ const claimsText = await readTextInput(options.claims, options.claimsFile).catch(() => void 0);
555
+ const input = { templateId: options.templateId };
556
+ if (claimsText) input.claims = parseJsonObject$1(claimsText, "issuance claims");
557
+ if (options.status) input.status = statusLabelToValue(options.status);
558
+ const detail = await client.createIssuance(input);
559
+ await writeStoredSession({
560
+ ...session,
561
+ serverUrl,
562
+ cookieHeader: client.getCookieHeader()
563
+ }, options);
564
+ return {
565
+ serverUrl,
566
+ detail
567
+ };
568
+ }
569
+ async function showIssuanceAction(rawOptions, deps = {}) {
570
+ const options = issuanceIdOptionsSchema.parse(rawOptions);
571
+ const { client, serverUrl, session } = await getAuthenticatedClient$1(options, deps);
572
+ const detail = await client.getIssuance(options.issuanceId);
573
+ await writeStoredSession({
574
+ ...session,
575
+ serverUrl,
576
+ cookieHeader: client.getCookieHeader()
577
+ }, options);
578
+ return {
579
+ serverUrl,
580
+ detail
581
+ };
582
+ }
583
+ async function updateIssuanceStatusAction(rawOptions, deps = {}) {
584
+ const options = issuanceStatusUpdateOptionsSchema.parse(rawOptions);
585
+ const { client, serverUrl, session } = await getAuthenticatedClient$1(options, deps);
586
+ const detail = await client.updateIssuanceStatus(options.issuanceId, { status: statusLabelToValue(options.status) });
587
+ await writeStoredSession({
588
+ ...session,
589
+ serverUrl,
590
+ cookieHeader: client.getCookieHeader()
591
+ }, options);
592
+ return {
593
+ serverUrl,
594
+ detail
595
+ };
596
+ }
597
+ async function getAuthenticatedClient$1(options, deps) {
598
+ const session = await requireStoredSession(options);
599
+ const serverUrl = resolveServerUrl(options, session);
600
+ assertSessionMatchesServerUrl(serverUrl, session);
601
+ return {
602
+ client: new IssuerWebClient({
603
+ serverUrl,
604
+ fetchImpl: deps.fetchImpl,
605
+ session
606
+ }),
607
+ options,
608
+ serverUrl,
609
+ session
610
+ };
611
+ }
612
+ function parseJsonObject$1(value, label) {
613
+ let parsed;
614
+ try {
615
+ parsed = JSON.parse(value);
616
+ } catch {
617
+ throw new Error(`${label} must be valid JSON`);
618
+ }
619
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label} must be a JSON object`);
620
+ return parsed;
621
+ }
622
+ //#endregion
623
+ //#region src/actions/metadata.ts
624
+ async function metadataAction(rawOptions, deps = {}) {
625
+ const serverUrl = resolveServerUrl(baseCliOptionsSchema.parse(rawOptions));
626
+ return {
627
+ serverUrl,
628
+ metadata: await new IssuerWebClient({
629
+ serverUrl,
630
+ fetchImpl: deps.fetchImpl
631
+ }).getMetadata()
632
+ };
633
+ }
634
+ //#endregion
635
+ //#region src/actions/templates.ts
636
+ async function listTemplatesAction(rawOptions, deps = {}) {
637
+ const { client, options, serverUrl, session } = await getAuthenticatedClient(baseCliOptionsSchema.parse(rawOptions), deps);
638
+ const templates = await client.listTemplates();
639
+ await writeStoredSession({
640
+ ...session,
641
+ serverUrl,
642
+ cookieHeader: client.getCookieHeader()
643
+ }, options);
644
+ return {
645
+ serverUrl,
646
+ templates
647
+ };
648
+ }
649
+ async function createTemplateAction(rawOptions, deps = {}) {
650
+ const options = templateCreateOptionsSchema.parse(rawOptions);
651
+ const { client, serverUrl, session } = await getAuthenticatedClient(options, deps);
652
+ const claimsText = options.claims !== void 0 || options.claimsFile !== void 0 ? await readTextInput(options.claims, options.claimsFile) : void 0;
653
+ const template = await client.createTemplate({
654
+ name: options.name,
655
+ vct: options.vct,
656
+ defaultClaims: claimsText ? parseJsonObject(claimsText, "template claims") : {}
657
+ });
658
+ await writeStoredSession({
659
+ ...session,
660
+ serverUrl,
661
+ cookieHeader: client.getCookieHeader()
662
+ }, options);
663
+ return {
664
+ serverUrl,
665
+ template
666
+ };
667
+ }
668
+ async function deleteTemplateAction(rawOptions, deps = {}) {
669
+ const options = templateDeleteOptionsSchema.parse(rawOptions);
670
+ const { client, serverUrl, session } = await getAuthenticatedClient(options, deps);
671
+ await client.deleteTemplate(options.templateId);
672
+ await writeStoredSession({
673
+ ...session,
674
+ serverUrl,
675
+ cookieHeader: client.getCookieHeader()
676
+ }, options);
677
+ return {
678
+ serverUrl,
679
+ templateId: options.templateId
680
+ };
681
+ }
682
+ async function getAuthenticatedClient(options, deps) {
683
+ const session = await requireStoredSession(options);
684
+ const serverUrl = resolveServerUrl(options, session);
685
+ assertSessionMatchesServerUrl(serverUrl, session);
686
+ return {
687
+ client: new IssuerWebClient({
688
+ serverUrl,
689
+ fetchImpl: deps.fetchImpl,
690
+ session
691
+ }),
692
+ options,
693
+ serverUrl,
694
+ session
695
+ };
696
+ }
697
+ function parseJsonObject(value, label) {
698
+ let parsed;
699
+ try {
700
+ parsed = JSON.parse(value);
701
+ } catch {
702
+ throw new Error(`${label} must be valid JSON`);
703
+ }
704
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label} must be a JSON object`);
705
+ return parsed;
706
+ }
707
+ //#endregion
708
+ //#region src/actions/interactive.ts
709
+ async function interactiveAction(rawOptions, deps = {}) {
710
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive mode requires a TTY. Use an explicit subcommand for non-interactive usage.");
711
+ const options = interactiveOptionsSchema.parse(rawOptions);
712
+ const prompt = new PromptSession();
713
+ let serverUrl = await resolveInteractiveServerUrl(prompt, options);
714
+ try {
715
+ while (true) {
716
+ const session = await readStoredSession(options);
717
+ if (!session || session.serverUrl !== serverUrl) await authenticateInteractive(prompt, {
718
+ ...options,
719
+ serverUrl
720
+ }, deps);
721
+ const choice = await prompt.choose("Issuer CLI", [
722
+ {
723
+ label: "Who am I",
724
+ value: "whoami"
725
+ },
726
+ {
727
+ label: "Show issuer metadata",
728
+ value: "metadata"
729
+ },
730
+ {
731
+ label: "List templates",
732
+ value: "templates-list"
733
+ },
734
+ {
735
+ label: "Create template",
736
+ value: "templates-create"
737
+ },
738
+ {
739
+ label: "Delete template",
740
+ value: "templates-delete"
741
+ },
742
+ {
743
+ label: "List issuances",
744
+ value: "issuances-list"
745
+ },
746
+ {
747
+ label: "Create issuance",
748
+ value: "issuances-create"
749
+ },
750
+ {
751
+ label: "Show issuance",
752
+ value: "issuances-show"
753
+ },
754
+ {
755
+ label: "Update issuance status",
756
+ value: "issuances-status"
757
+ },
758
+ {
759
+ label: "Switch server",
760
+ value: "switch-server"
761
+ },
762
+ {
763
+ label: "Sign out",
764
+ value: "signout"
765
+ },
766
+ {
767
+ label: "Exit",
768
+ value: "exit"
769
+ }
770
+ ]);
771
+ stdout.write("\n");
772
+ if (choice === "exit") return;
773
+ if (choice === "switch-server") {
774
+ serverUrl = await resolveInteractiveServerUrl(prompt, {
775
+ ...options,
776
+ serverUrl
777
+ });
778
+ continue;
779
+ }
780
+ if (choice === "whoami") {
781
+ const result = await authWhoAmIAction({
782
+ ...options,
783
+ serverUrl
784
+ }, deps);
785
+ stdout.write(`${formatSessionSummary(result)}\n\n`);
786
+ continue;
787
+ }
788
+ if (choice === "metadata") {
789
+ const result = await metadataAction({
790
+ ...options,
791
+ serverUrl
792
+ }, deps);
793
+ stdout.write(`${formatIssuerMetadata(result.metadata)}\n\n`);
794
+ continue;
795
+ }
796
+ if (choice === "templates-list") {
797
+ const result = await listTemplatesAction({
798
+ ...options,
799
+ serverUrl
800
+ }, deps);
801
+ stdout.write(`${formatTemplateList(result.templates)}\n\n`);
802
+ continue;
803
+ }
804
+ if (choice === "templates-create") {
805
+ const name = await prompt.text("Template name");
806
+ const vct = await prompt.text("VCT");
807
+ const claims = await prompt.text("Default claims JSON", { defaultValue: "{}" });
808
+ const result = await createTemplateAction({
809
+ ...options,
810
+ serverUrl,
811
+ name,
812
+ vct,
813
+ claims
814
+ }, deps);
815
+ stdout.write(`${formatTemplateSummary(result.template)}\n\n`);
816
+ continue;
817
+ }
818
+ if (choice === "templates-delete") {
819
+ const result = await listTemplatesAction({
820
+ ...options,
821
+ serverUrl
822
+ }, deps);
823
+ if (result.templates.length === 0) {
824
+ stdout.write("No templates found.\n\n");
825
+ continue;
826
+ }
827
+ const selectable = result.templates.filter((template) => template.kind === "custom");
828
+ if (selectable.length === 0) {
829
+ stdout.write("No custom templates can be deleted.\n\n");
830
+ continue;
831
+ }
832
+ const templateId = await prompt.choose("Select a template to delete", selectable.map((template) => ({
833
+ label: `${template.name} (${template.id})`,
834
+ value: template.id
835
+ })));
836
+ if (!await prompt.confirm(`Delete template ${templateId}?`, false)) {
837
+ stdout.write("Cancelled.\n\n");
838
+ continue;
839
+ }
840
+ const deleted = await deleteTemplateAction({
841
+ ...options,
842
+ serverUrl,
843
+ templateId
844
+ }, deps);
845
+ stdout.write(`${formatDeletedTemplate(deleted.templateId)}\n\n`);
846
+ continue;
847
+ }
848
+ if (choice === "issuances-list") {
849
+ const result = await listIssuancesAction({
850
+ ...options,
851
+ serverUrl
852
+ }, deps);
853
+ stdout.write(`${formatIssuanceList(result.issuances)}\n\n`);
854
+ continue;
855
+ }
856
+ if (choice === "issuances-create") {
857
+ const templates = await listTemplatesAction({
858
+ ...options,
859
+ serverUrl
860
+ }, deps);
861
+ if (templates.templates.length === 0) {
862
+ stdout.write("No templates available. Create one first.\n\n");
863
+ continue;
864
+ }
865
+ const templateId = await prompt.choose("Select a template", templates.templates.map((template) => ({
866
+ label: `${template.name} (${template.vct})`,
867
+ value: template.id
868
+ })));
869
+ const claims = await prompt.text("Issuance claims JSON", { defaultValue: "{}" });
870
+ const status = await prompt.choose("Initial status", [
871
+ {
872
+ label: "active",
873
+ value: "active"
874
+ },
875
+ {
876
+ label: "suspended",
877
+ value: "suspended"
878
+ },
879
+ {
880
+ label: "revoked",
881
+ value: "revoked"
882
+ }
883
+ ]);
884
+ const created = await createIssuanceAction({
885
+ ...options,
886
+ serverUrl,
887
+ templateId,
888
+ claims,
889
+ status
890
+ }, deps);
891
+ stdout.write(`${formatIssuanceSummary(created.detail)}\n\n`);
892
+ continue;
893
+ }
894
+ if (choice === "issuances-show") {
895
+ const issuanceId = await prompt.text("Issuance id");
896
+ const result = await showIssuanceAction({
897
+ ...options,
898
+ serverUrl,
899
+ issuanceId
900
+ }, deps);
901
+ stdout.write(`${formatIssuanceSummary(result.detail)}\n\n`);
902
+ continue;
903
+ }
904
+ if (choice === "issuances-status") {
905
+ const issuanceId = await prompt.text("Issuance id");
906
+ const status = await prompt.choose("New status", [
907
+ {
908
+ label: "active",
909
+ value: "active"
910
+ },
911
+ {
912
+ label: "suspended",
913
+ value: "suspended"
914
+ },
915
+ {
916
+ label: "revoked",
917
+ value: "revoked"
918
+ }
919
+ ]);
920
+ const result = await updateIssuanceStatusAction({
921
+ ...options,
922
+ serverUrl,
923
+ issuanceId,
924
+ status
925
+ }, deps);
926
+ stdout.write(`${formatIssuanceSummary(result.detail)}\n\n`);
927
+ continue;
928
+ }
929
+ if (choice === "signout") {
930
+ const result = await authSignOutAction({
931
+ ...options,
932
+ serverUrl
933
+ }, deps);
934
+ stdout.write(`${formatSignedOut(result.serverUrl)}\n\n`);
935
+ }
936
+ }
937
+ } finally {
938
+ prompt.close();
939
+ }
940
+ }
941
+ async function authenticateInteractive(prompt, options, deps) {
942
+ stdout.write(`No saved session for ${options.serverUrl}.\n`);
943
+ const choice = await prompt.choose("Choose authentication mode", [
944
+ {
945
+ label: "Continue as guest",
946
+ value: "guest"
947
+ },
948
+ {
949
+ label: "Sign in",
950
+ value: "signin"
951
+ },
952
+ {
953
+ label: "Create account",
954
+ value: "signup"
955
+ }
956
+ ]);
957
+ if (choice === "guest") {
958
+ const result = await authSignInAction({
959
+ ...options,
960
+ anonymous: true
961
+ }, deps);
962
+ stdout.write(`${formatSessionSummary(result)}\n\n`);
963
+ return;
964
+ }
965
+ const username = await prompt.text("Username");
966
+ const password = await prompt.text("Password", { password: true });
967
+ if (choice === "signup") {
968
+ const result = await authSignUpAction({
969
+ ...options,
970
+ username,
971
+ password
972
+ }, deps);
973
+ stdout.write(`${formatSessionSummary(result)}\n\n`);
974
+ return;
975
+ }
976
+ const result = await authSignInAction({
977
+ ...options,
978
+ username,
979
+ password
980
+ }, deps);
981
+ stdout.write(`${formatSessionSummary(result)}\n\n`);
982
+ }
983
+ async function resolveInteractiveServerUrl(prompt, options) {
984
+ const saved = await readStoredSession(options);
985
+ return prompt.text("Issuer web server URL", { defaultValue: resolveServerUrl(options, saved ?? void 0) });
986
+ }
987
+ //#endregion
988
+ //#region src/program.ts
989
+ function withCommonOptions(command) {
990
+ return command.option("--server-url <url>", "Issuer web server base URL (default: saved session, ISSUER_WEB_SERVER_URL, or http://localhost:3001)").option("--session-file <file>", "Override the saved session file location");
991
+ }
992
+ function createProgram(version) {
993
+ const program = withCommonOptions(new Command().name("openid4vc-issuer").version(version).description("Terminal client for openid4vc-issuer-web-server. Run without a subcommand to start interactive mode.").addHelpText("after", "\nInteractive mode:\n Run `openid4vc-issuer` without a subcommand to open the prompt-driven workflow.").showHelpAfterError().option("--verbose", "Enable verbose logging to stderr", false).hook("preAction", (_thisCommand, actionCommand) => {
994
+ if (actionCommand.optsWithGlobals().verbose) setVerbose(true);
995
+ })).action(async (options) => {
996
+ await interactiveAction(options);
997
+ });
998
+ withCommonOptions(program.command("metadata").description("Show issuer metadata from /.well-known/openid-credential-issuer").option("--output <format>", "Output format: text or json", "text")).action(async (options) => {
999
+ const result = await metadataAction(options);
1000
+ printResult(options.output === "json" ? result.metadata : formatIssuerMetadata(result.metadata), options.output);
1001
+ });
1002
+ const auth = program.command("auth").description("Authenticate and inspect the current session");
1003
+ withCommonOptions(auth.command("signin").description("Sign in with a guest session or username/password").option("--anonymous", "Start a guest session").option("--username <name>", "Username for sign-in").option("--password <password>", "Password for sign-in").addHelpText("after", `\nExamples:\n $ openid4vc-issuer auth signin --anonymous\n $ openid4vc-issuer auth signin --server-url http://localhost:3001 --username ada --password secret`)).action(async (options) => {
1004
+ verbose(`Signing in to ${options.serverUrl ?? "saved/default server"}`);
1005
+ printResult(formatSessionSummary(await authSignInAction(options)), "text");
1006
+ });
1007
+ withCommonOptions(auth.command("signup").description("Create an account and save the resulting session").requiredOption("--username <name>", "Username for the new account").requiredOption("--password <password>", "Password for the new account")).action(async (options) => {
1008
+ printResult(formatSessionSummary(await authSignUpAction(options)), "text");
1009
+ });
1010
+ withCommonOptions(auth.command("whoami").description("Show the currently saved session")).action(async (options) => {
1011
+ printResult(formatSessionSummary(await authWhoAmIAction(options)), "text");
1012
+ });
1013
+ withCommonOptions(auth.command("signout").description("Sign out and clear the saved session")).action(async (options) => {
1014
+ printResult(formatSignedOut((await authSignOutAction(options)).serverUrl), "text");
1015
+ });
1016
+ const templates = program.command("templates").description("Manage credential templates through openid4vc-issuer-web-server");
1017
+ withCommonOptions(templates.command("list").description("List templates visible to the current user")).action(async (options) => {
1018
+ printResult(formatTemplateList((await listTemplatesAction(options)).templates), "text");
1019
+ });
1020
+ withCommonOptions(templates.command("create").description("Create a custom template").requiredOption("--name <value>", "Template name").requiredOption("--vct <value>", "Verifiable Credential Type").option("--claims <json>", "Inline JSON object for default claims").option("--claims-file <file>", "Path to a JSON file with default claims").addHelpText("after", `\nExamples:\n $ openid4vc-issuer templates create --name "Conference Pass" --vct urn:eudi:pid:1 --claims '{"given_name":"Ada"}'\n $ openid4vc-issuer templates create --name "PID" --vct urn:eudi:pid:1 --claims-file ./claims.json`)).action(async (options) => {
1021
+ printResult(formatTemplateSummary((await createTemplateAction(options)).template), "text");
1022
+ });
1023
+ withCommonOptions(templates.command("delete").description("Delete a custom template by id").requiredOption("--template-id <id>", "Template id")).action(async (options) => {
1024
+ printResult(formatDeletedTemplate((await deleteTemplateAction(options)).templateId), "text");
1025
+ });
1026
+ const issuances = program.command("issuances").description("Create and manage credential offers");
1027
+ withCommonOptions(issuances.command("list").description("List issuances for the current user")).action(async (options) => {
1028
+ printResult(formatIssuanceList((await listIssuancesAction(options)).issuances), "text");
1029
+ });
1030
+ withCommonOptions(issuances.command("create").description("Create a new issuance offer from a template").requiredOption("--template-id <id>", "Template id").option("--claims <json>", "Inline JSON object with issuance claims").option("--claims-file <file>", "Path to a JSON file with issuance claims").option("--status <value>", "Initial credential status: active, suspended, or revoked").addHelpText("after", `\nExample:\n $ openid4vc-issuer issuances create --template-id <template-id> --claims '{"seat":"A-12"}' --status active`)).action(async (options) => {
1031
+ printResult(formatIssuanceSummary((await createIssuanceAction(options)).detail), "text");
1032
+ });
1033
+ withCommonOptions(issuances.command("show").description("Show one issuance including the offer URI").requiredOption("--issuance-id <id>", "Issuance id")).action(async (options) => {
1034
+ printResult(formatIssuanceSummary((await showIssuanceAction(options)).detail), "text");
1035
+ });
1036
+ withCommonOptions(issuances.command("status").description("Update the credential status for an issuance").requiredOption("--issuance-id <id>", "Issuance id").requiredOption("--status <value>", "New credential status: active, suspended, or revoked")).action(async (options) => {
1037
+ printResult(formatIssuanceSummary((await updateIssuanceStatusAction(options)).detail), "text");
1038
+ });
1039
+ return program;
1040
+ }
1041
+ //#endregion
1042
+ //#region src/index.ts
1043
+ async function runCli(argv = process.argv) {
1044
+ const version = await resolveCliVersion(resolvePackageJsonPath(import.meta.url));
1045
+ try {
1046
+ await createProgram(version).parseAsync(argv);
1047
+ } catch (error) {
1048
+ handleCliError(error);
1049
+ }
1050
+ }
1051
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? process.cwd()).href) runCli().catch((error) => {
1052
+ handleCliError(error);
1053
+ });
1054
+ //#endregion
1055
+ export { authSignInAction, authSignOutAction, authSignUpAction, authWhoAmIAction, createIssuanceAction, createProgram, createTemplateAction, deleteTemplateAction, interactiveAction, listIssuancesAction, listTemplatesAction, metadataAction, runCli, showIssuanceAction, updateIssuanceStatusAction };