@vidos-id/openid4vc-issuer-cli 0.10.0 → 0.10.2

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