@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/README.md +207 -0
- package/dist/index.d.mts +198 -0
- package/dist/index.mjs +1055 -0
- package/package.json +49 -0
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 };
|