@eudiplo/cli 8.0.2-main.e4bc4d4 → 8.1.0-main.3c4fbcd

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.
Files changed (49) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/config/portability/migrations.spec.js +5 -3
  3. package/dist/commands/config/portability/migrations.spec.js.map +1 -1
  4. package/dist/commands/doctor/action.d.ts +2 -1
  5. package/dist/commands/doctor/action.d.ts.map +1 -1
  6. package/dist/commands/doctor/action.js +25 -7
  7. package/dist/commands/doctor/action.js.map +1 -1
  8. package/dist/commands/doctor/index.d.ts.map +1 -1
  9. package/dist/commands/doctor/index.js +2 -0
  10. package/dist/commands/doctor/index.js.map +1 -1
  11. package/dist/index.js +2496 -466
  12. package/dist/runtime.js +1 -1
  13. package/dist/runtime.js.map +1 -1
  14. package/dist/services/api-auth.d.ts +31 -0
  15. package/dist/services/api-auth.d.ts.map +1 -0
  16. package/dist/services/api-auth.js +78 -0
  17. package/dist/services/api-auth.js.map +1 -0
  18. package/dist/services/backend-checks.d.ts +24 -0
  19. package/dist/services/backend-checks.d.ts.map +1 -0
  20. package/dist/services/backend-checks.js +105 -0
  21. package/dist/services/backend-checks.js.map +1 -0
  22. package/dist/services/compose-project.js +6 -6
  23. package/dist/services/compose-project.js.map +1 -1
  24. package/dist/services/compose-upgrade.js +1 -1
  25. package/dist/services/compose-upgrade.js.map +1 -1
  26. package/dist/services/container-state.d.ts +14 -0
  27. package/dist/services/container-state.d.ts.map +1 -0
  28. package/dist/services/container-state.js +70 -0
  29. package/dist/services/container-state.js.map +1 -0
  30. package/dist/services/deployment-drivers.d.ts.map +1 -1
  31. package/dist/services/deployment-drivers.js +77 -0
  32. package/dist/services/deployment-drivers.js.map +1 -1
  33. package/dist/services/diagnostics.d.ts +19 -2
  34. package/dist/services/diagnostics.d.ts.map +1 -1
  35. package/dist/services/diagnostics.js +213 -49
  36. package/dist/services/diagnostics.js.map +1 -1
  37. package/dist/services/tls-expiry.d.ts +16 -0
  38. package/dist/services/tls-expiry.d.ts.map +1 -0
  39. package/dist/services/tls-expiry.js +80 -0
  40. package/dist/services/tls-expiry.js.map +1 -0
  41. package/dist/types.d.ts +4 -1
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +3 -3
  44. package/templates/demo-config/trust-lists/trustlist-580831bc-ef11-43f4-a3be-a2b6bf1b29a3-config.json +1 -97
  45. package/templates/docker-compose.yml +35 -28
  46. package/templates/schemas/IssuanceConfig.schema.json +13 -0
  47. package/templates/schemas/IssuanceConfigFile.schema.json +663 -1
  48. package/templates/schemas/PresentationConfigCreateDto.schema.json +3 -6
  49. package/templates/schemas/PresentationConfigFile.schema.json +723 -1
package/dist/index.js CHANGED
@@ -2216,20 +2216,20 @@ var require_resolve = __commonJS({
2216
2216
  return false;
2217
2217
  }
2218
2218
  function countKeys(schema) {
2219
- let count = 0;
2219
+ let count2 = 0;
2220
2220
  for (const key in schema) {
2221
2221
  if (key === "$ref")
2222
2222
  return Infinity;
2223
- count++;
2223
+ count2++;
2224
2224
  if (SIMPLE_INLINED.has(key))
2225
2225
  continue;
2226
2226
  if (typeof schema[key] == "object") {
2227
- (0, util_1.eachItem)(schema[key], (sch) => count += countKeys(sch));
2227
+ (0, util_1.eachItem)(schema[key], (sch) => count2 += countKeys(sch));
2228
2228
  }
2229
- if (count === Infinity)
2229
+ if (count2 === Infinity)
2230
2230
  return Infinity;
2231
2231
  }
2232
- return count;
2232
+ return count2;
2233
2233
  }
2234
2234
  function getFullPath(resolver, id = "", normalize) {
2235
2235
  if (normalize !== false)
@@ -3828,7 +3828,7 @@ var require_fast_uri = __commonJS({
3828
3828
  normalizeString(uri, options);
3829
3829
  } else if (typeof uri === "object") {
3830
3830
  uri = /** @type {T} */
3831
- parse4(serialize(uri, options), options);
3831
+ parse5(serialize(uri, options), options);
3832
3832
  }
3833
3833
  return uri;
3834
3834
  }
@@ -3868,8 +3868,8 @@ var require_fast_uri = __commonJS({
3868
3868
  function resolveComponent(base, relative3, options, skipNormalization) {
3869
3869
  const target = {};
3870
3870
  if (!skipNormalization) {
3871
- base = parse4(serialize(base, options), options);
3872
- relative3 = parse4(serialize(relative3, options), options);
3871
+ base = parse5(serialize(base, options), options);
3872
+ relative3 = parse5(serialize(relative3, options), options);
3873
3873
  }
3874
3874
  options = options || {};
3875
3875
  if (!options.tolerant && relative3.scheme) {
@@ -4161,7 +4161,7 @@ var require_fast_uri = __commonJS({
4161
4161
  }
4162
4162
  return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
4163
4163
  }
4164
- function parse4(uri, opts) {
4164
+ function parse5(uri, opts) {
4165
4165
  return parseWithStatus(uri, opts).parsed;
4166
4166
  }
4167
4167
  function normalizeString(uri, opts) {
@@ -4198,7 +4198,7 @@ var require_fast_uri = __commonJS({
4198
4198
  resolveComponent,
4199
4199
  equal,
4200
4200
  serialize,
4201
- parse: parse4
4201
+ parse: parse5
4202
4202
  };
4203
4203
  module.exports = fastUri;
4204
4204
  module.exports.default = fastUri;
@@ -5699,8 +5699,8 @@ var require_contains = __commonJS({
5699
5699
  cxt.result(valid, () => cxt.reset());
5700
5700
  function validateItemsWithCount() {
5701
5701
  const schValid = gen.name("_valid");
5702
- const count = gen.let("count", 0);
5703
- validateItems(schValid, () => gen.if(schValid, () => checkLimits(count)));
5702
+ const count2 = gen.let("count", 0);
5703
+ validateItems(schValid, () => gen.if(schValid, () => checkLimits(count2)));
5704
5704
  }
5705
5705
  function validateItems(_valid, block) {
5706
5706
  gen.forRange("i", 0, len, (i2) => {
@@ -5713,16 +5713,16 @@ var require_contains = __commonJS({
5713
5713
  block();
5714
5714
  });
5715
5715
  }
5716
- function checkLimits(count) {
5717
- gen.code((0, codegen_1._)`${count}++`);
5716
+ function checkLimits(count2) {
5717
+ gen.code((0, codegen_1._)`${count2}++`);
5718
5718
  if (max2 === void 0) {
5719
- gen.if((0, codegen_1._)`${count} >= ${min}`, () => gen.assign(valid, true).break());
5719
+ gen.if((0, codegen_1._)`${count2} >= ${min}`, () => gen.assign(valid, true).break());
5720
5720
  } else {
5721
- gen.if((0, codegen_1._)`${count} > ${max2}`, () => gen.assign(valid, false).break());
5721
+ gen.if((0, codegen_1._)`${count2} > ${max2}`, () => gen.assign(valid, false).break());
5722
5722
  if (min === 1)
5723
5723
  gen.assign(valid, true);
5724
5724
  else
5725
- gen.if((0, codegen_1._)`${count} >= ${min}`, () => gen.assign(valid, true));
5725
+ gen.if((0, codegen_1._)`${count2} >= ${min}`, () => gen.assign(valid, true));
5726
5726
  }
5727
5727
  }
5728
5728
  }
@@ -12065,10 +12065,10 @@ import { isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2
12065
12065
  function createScanner(text, ignoreTrivia = false) {
12066
12066
  const len = text.length;
12067
12067
  let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
12068
- function scanHexDigits(count, exact) {
12068
+ function scanHexDigits(count2, exact) {
12069
12069
  let digits = 0;
12070
12070
  let value2 = 0;
12071
- while (digits < count || !exact) {
12071
+ while (digits < count2 || !exact) {
12072
12072
  let ch = text.charCodeAt(pos);
12073
12073
  if (ch >= 48 && ch <= 57) {
12074
12074
  value2 = value2 * 16 + ch - 48;
@@ -12082,7 +12082,7 @@ function createScanner(text, ignoreTrivia = false) {
12082
12082
  pos++;
12083
12083
  digits++;
12084
12084
  }
12085
- if (digits < count) {
12085
+ if (digits < count2) {
12086
12086
  value2 = -1;
12087
12087
  }
12088
12088
  return value2;
@@ -12707,9 +12707,9 @@ function format(documentText, range, options) {
12707
12707
  }
12708
12708
  return editOperations;
12709
12709
  }
12710
- function repeat(s, count) {
12710
+ function repeat(s, count2) {
12711
12711
  let result = "";
12712
- for (let i2 = 0; i2 < count; i2++) {
12712
+ for (let i2 = 0; i2 < count2; i2++) {
12713
12713
  result += s;
12714
12714
  }
12715
12715
  return result;
@@ -14887,7 +14887,7 @@ var CONFIG_FORMATS = {
14887
14887
  IssuanceConfig: {
14888
14888
  slug: "issuance-config",
14889
14889
  file: "IssuanceConfigFile",
14890
- version: 1
14890
+ version: 2
14891
14891
  },
14892
14892
  CredentialConfig: {
14893
14893
  slug: "credential-config",
@@ -14897,7 +14897,7 @@ var CONFIG_FORMATS = {
14897
14897
  PresentationConfig: {
14898
14898
  slug: "presentation-config",
14899
14899
  file: "PresentationConfigFile",
14900
- version: 1
14900
+ version: 2
14901
14901
  },
14902
14902
  AttributeProvider: {
14903
14903
  slug: "attribute-provider",
@@ -14936,7 +14936,42 @@ function resourceId(input) {
14936
14936
  throw new Error(`Configuration spec.${field} is required`);
14937
14937
  return id;
14938
14938
  }
14939
- var CONFIG_MIGRATIONS = [];
14939
+ var presentationConfigV1ToV2 = {
14940
+ id: "presentation-config-v2-provides-attestations",
14941
+ kind: "PresentationConfig",
14942
+ from: 1,
14943
+ to: 2,
14944
+ migrate(spec) {
14945
+ const registrationCert = spec.registration_cert;
14946
+ if (!object(registrationCert) || !object(registrationCert.body) || !("provided_attestations" in registrationCert.body)) {
14947
+ return { spec };
14948
+ }
14949
+ const { provided_attestations: _, ...body } = registrationCert.body;
14950
+ return {
14951
+ spec: { ...spec, registration_cert: { ...registrationCert, body } },
14952
+ issues: [
14953
+ {
14954
+ severity: "warning",
14955
+ code: "PROVIDED_ATTESTATIONS_REMOVED",
14956
+ path: "/spec/registration_cert/body/provided_attestations",
14957
+ message: "provided_attestations is not part of the registrar API and was removed. Set provides_attestations to the credential type identifiers (SD-JWT VC vct or mdoc doctype) the issuer provides."
14958
+ }
14959
+ ]
14960
+ };
14961
+ }
14962
+ };
14963
+ var CONFIG_MIGRATIONS = [
14964
+ {
14965
+ id: "issuance-config-v2-offer-lifetime",
14966
+ kind: "IssuanceConfig",
14967
+ from: 1,
14968
+ to: 2,
14969
+ // v2 only adds the optional offerLifetimeSeconds: a v1 spec is a
14970
+ // valid v2 spec whose offers keep having no lifetime.
14971
+ migrate: (spec) => ({ spec })
14972
+ },
14973
+ presentationConfigV1ToV2
14974
+ ];
14940
14975
  function schemaUrl(kind, version = CONFIG_FORMATS[kind].version) {
14941
14976
  if (!Number.isSafeInteger(version) || version < 1)
14942
14977
  throw new Error("Invalid config version");
@@ -15260,12 +15295,43 @@ function assertConfigBundle(input) {
15260
15295
  }
15261
15296
 
15262
15297
  // ../../packages/eudiplo-config-format/dist/config-values.js
15298
+ var isVariableChar = (char) => char >= "A" && char <= "Z" || char >= "0" && char <= "9" || char === "_";
15299
+ function replacePlaceholders(input, replace) {
15300
+ let output = "";
15301
+ let copied = 0;
15302
+ let start = input.indexOf("${");
15303
+ while (start !== -1) {
15304
+ let cursor = start + 2;
15305
+ while (cursor < input.length && isVariableChar(input[cursor]))
15306
+ cursor++;
15307
+ const variable = input.slice(start + 2, cursor);
15308
+ let end = -1;
15309
+ let fallback;
15310
+ if (variable && input[cursor] === "}")
15311
+ end = cursor;
15312
+ else if (variable && input[cursor] === ":") {
15313
+ end = input.indexOf("}", cursor + 1);
15314
+ if (end === -1)
15315
+ break;
15316
+ fallback = input.slice(cursor + 1, end);
15317
+ }
15318
+ if (end === -1) {
15319
+ start = input.indexOf("${", start + 2);
15320
+ continue;
15321
+ }
15322
+ output += input.slice(copied, start);
15323
+ output += replace(input.slice(start, end + 1), variable, fallback);
15324
+ copied = end + 1;
15325
+ start = input.indexOf("${", copied);
15326
+ }
15327
+ return output + input.slice(copied);
15328
+ }
15263
15329
  function resolveConfigVariables(input, env) {
15264
15330
  const issues = [];
15265
15331
  const seen = /* @__PURE__ */ new WeakMap();
15266
15332
  function visit2(value, path2) {
15267
15333
  if (typeof value === "string")
15268
- return value.replace(/\$\{([A-Z0-9_]+)(?::([^}]*))?\}/g, (match, variable, fallback) => {
15334
+ return replacePlaceholders(value, (match, variable, fallback) => {
15269
15335
  if (env[variable] !== void 0 && env[variable] !== "")
15270
15336
  return env[variable];
15271
15337
  if (fallback !== void 0)
@@ -19970,215 +20036,1609 @@ var CONFIG_SCHEMAS = [
19970
20036
  "title": "CreateWebhookEndpointDto"
19971
20037
  }
19972
20038
  }
19973
- }
19974
- ];
19975
-
19976
- // ../../packages/eudiplo-config-format/dist/config-validator.js
19977
- var ajv = new import__.Ajv2020({
19978
- allErrors: true,
19979
- strict: false,
19980
- validateFormats: false
19981
- });
19982
- for (const schema of CONFIG_SCHEMAS)
19983
- ajv.addSchema(schema);
19984
- function validateConfigDocument(document) {
19985
- const file = serializeDocument(document);
19986
- const validate = ajv.getSchema(file.$schema);
19987
- if (!validate)
19988
- throw new Error(`No bundled validator for ${file.$schema}`);
19989
- const issues = [];
19990
- if (!validate(file)) {
19991
- for (const issue of validate.errors ?? [])
19992
- issues.push({
19993
- severity: "error",
19994
- code: "CONFIG_SCHEMA_VALIDATION_FAILED",
19995
- path: issue.instancePath || "/",
19996
- message: issue.keyword === "additionalProperties" ? `Unknown property: ${String(issue.params.additionalProperty)}` : issue.message ?? "Invalid configuration",
19997
- resource: { kind: document.kind, id: resourceId(document) }
19998
- });
19999
- }
20000
- const id = document.kind === "Client" ? document.spec.clientId : document.spec.id;
20001
- if (id !== void 0 && id !== resourceId(document))
20002
- issues.push({
20003
- severity: "error",
20004
- code: "RESOURCE_ID_MISMATCH",
20005
- path: document.kind === "Client" ? "/spec/clientId" : "/spec/id",
20006
- message: "Resource identifier does not match metadata.id"
20007
- });
20008
- return issues;
20009
- }
20010
-
20011
- // src/commands/config/portability/migrations.ts
20012
- function upgradeDocument(input) {
20013
- const result = migrateDocument(input, validateConfigDocument);
20014
- return { ...result, document: serializeDocument(result.document) };
20015
- }
20016
-
20017
- // src/commands/config/portability/upgrade.ts
20018
- var codec = new ConfigBundleCodec();
20019
- function parse3(text, path2) {
20020
- try {
20021
- return JSON.parse(text);
20022
- } catch {
20023
- throw new Error(`Invalid JSON in ${path2}`);
20024
- }
20025
- }
20026
- function upgradeJson(input, file, context) {
20027
- const bundle = Array.isArray(input?.documents);
20028
- if (bundle) assertConfigBundle(input);
20029
- const results = (bundle ? input.documents : [input]).map(
20030
- (document) => upgradeDocument(document)
20031
- );
20032
- let blocked = false;
20033
- for (const result of results) {
20034
- for (const issue of result.issues) {
20035
- context.stderr.write(
20036
- `${file}${issue.path}: ${issue.severity} ${issue.code}: ${issue.message}
20037
- `
20038
- );
20039
- if (issue.severity !== "warning") blocked = true;
20040
- }
20041
- for (const migration of result.migrations)
20042
- context.stdout.write(`${file}: migration ${migration}
20043
- `);
20044
- }
20045
- if (blocked) throw new Error(`${file}: upgrade blocked; no output written`);
20046
- if (!bundle) return results[0].document;
20047
- const documents = results.map(
20048
- (result) => result.document
20049
- );
20050
- const resources = input.manifest.resources.map(
20051
- (resource) => {
20052
- const index = input.documents.findIndex(
20053
- (candidate) => normalizeDocument(candidate).kind === resource.kind && resourceId(candidate) === resource.id
20054
- );
20055
- const document = documents[index];
20056
- return {
20057
- ...resource,
20058
- id: resourceId(document),
20059
- $schema: document.$schema,
20060
- sha256: createHash2("sha256").update(JSON.stringify(document)).digest("hex")
20061
- };
20062
- }
20063
- );
20064
- const output = {
20065
- ...input,
20066
- manifest: { ...input.manifest, formatVersion: 2, resources },
20067
- documents
20068
- };
20069
- assertConfigBundle(output);
20070
- return output;
20071
- }
20072
- function report(input, output, file, parsed, context) {
20073
- const changed = stableConfigJson(input) !== stableConfigJson(output);
20074
- context.stdout.write(
20075
- `${file}: ${changed ? "upgrade required" : "current"}
20076
- `
20077
- );
20078
- if (parsed.flags.diff === true)
20079
- for (const change of configChanges(input, output)) {
20080
- context.stdout.write(
20081
- ` ${change.path}: ${JSON.stringify(change.before) ?? "(absent)"} -> ${JSON.stringify(change.after) ?? "(absent)"}
20082
- `
20083
- );
20084
- }
20085
- return changed;
20086
- }
20087
- function inferredKind(path2) {
20088
- const normalized = path2.split(sep2).join("/");
20089
- const match = TENANT_RESOURCE_REGISTRY.find(
20090
- (entry) => entry.kind === "file" ? normalized === entry.file || normalized.endsWith(`/${entry.file}`) : normalized.endsWith(`/${entry.subfolder}/${basename2(path2)}`) || normalized === `${entry.subfolder}/${basename2(path2)}`
20091
- );
20092
- return CONFIG_RESOURCE_KINDS.find(
20093
- (kind) => `${CONFIG_FORMATS[kind].file}.schema.json` === match?.schemaFile
20094
- );
20095
- }
20096
- function wrapBare(kind, input, path2) {
20097
- if (!input || typeof input !== "object" || Array.isArray(input))
20098
- throw new Error(`${path2}: configuration must be an object`);
20099
- const singleton = {
20100
- Tenant: "tenant",
20101
- KmsConfig: "kms",
20102
- RegistrarConfig: "registrar",
20103
- IssuanceConfig: "issuance"
20104
- };
20105
- const spec = structuredClone(input);
20106
- if (kind === "KeyChain" && spec.key && !spec.keySource) {
20107
- spec.keySource = { type: "private-jwk", jwk: spec.key };
20108
- delete spec.key;
20109
- }
20110
- const id = String(
20111
- singleton[kind] ?? spec.id ?? spec.clientId ?? basename2(path2, ".json")
20112
- );
20113
- if (!singleton[kind]) spec[kind === "Client" ? "clientId" : "id"] = id;
20114
- return { $schema: schemaUrl(kind, 1), metadata: { generation: 1 }, spec };
20115
- }
20116
- async function folderFiles(path2) {
20117
- const files = [];
20118
- for (const entry of await readdir(path2, { withFileTypes: true })) {
20119
- if (entry.name.startsWith(".")) continue;
20120
- const file = join4(path2, entry.name);
20121
- if (entry.isSymbolicLink())
20122
- throw new Error(`Config folder contains a symbolic link: ${file}`);
20123
- if (entry.isDirectory()) files.push(...await folderFiles(file));
20124
- else if (entry.isFile()) files.push(file);
20125
- }
20126
- return files.sort();
20127
- }
20128
- async function upgradeFolder(source, parsed, context) {
20129
- const outputs = /* @__PURE__ */ new Map();
20130
- let changed = 0;
20131
- for (const file of await folderFiles(source)) {
20132
- if (extname(file) !== ".json" || relative2(source, file).split(sep2).includes("images"))
20133
- continue;
20134
- const input = parse3(await readFile4(file, "utf8"), file);
20135
- const kind = inferredKind(relative2(source, file));
20136
- if (!kind && !isConfigDocument(input)) continue;
20137
- const candidate = isConfigDocument(input) ? input : wrapBare(kind, input, file);
20138
- const output = upgradeJson(candidate, file, context);
20139
- if (kind && normalizeDocument(output).kind !== kind)
20140
- throw new Error(`${file}: expected ${kind} configuration`);
20141
- if (report(input, output, relative2(source, file), parsed, context))
20142
- changed++;
20143
- outputs.set(
20144
- relative2(source, file),
20145
- `${JSON.stringify(output, null, 2)}
20146
- `
20147
- );
20148
- }
20149
- if (!outputs.size)
20150
- throw new Error(`No configuration documents found in ${source}`);
20151
- context.stdout.write(
20152
- `${outputs.size - changed} current, ${changed} require upgrading
20153
- `
20154
- );
20155
- if (parsed.flags.check === true) return changed ? 1 : 0;
20156
- if (parsed.flags["dry-run"] === true) return 0;
20157
- const destination = resolve3(
20158
- context.cwd,
20159
- readStringFlag(parsed.flags, "output") ?? `${source}.upgraded`
20160
- );
20161
- const overlap = (parent, child) => {
20162
- const path2 = relative2(parent, child);
20163
- return path2 === "" || !path2.startsWith(`..${sep2}`) && path2 !== "..";
20164
- };
20165
- if (overlap(source, destination) || overlap(destination, source))
20166
- throw new Error(
20167
- "Folder output must be separate from the source directory"
20168
- );
20169
- await lstat(destination).then(
20170
- () => {
20171
- throw new Error(`Output already exists: ${destination}`);
20039
+ },
20040
+ {
20041
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
20042
+ "$id": "https://eudiplo.dev/schemas/v2/IssuanceConfigFile.schema.json",
20043
+ "title": "IssuanceConfigFile",
20044
+ "type": "object",
20045
+ "properties": {
20046
+ "$schema": {
20047
+ "type": "string",
20048
+ "const": "https://eudiplo.dev/schemas/v2/IssuanceConfigFile.schema.json"
20049
+ },
20050
+ "metadata": {
20051
+ "type": "object",
20052
+ "properties": {
20053
+ "generation": {
20054
+ "type": "integer",
20055
+ "minimum": 1,
20056
+ "description": "Monotonically increasing configuration generation."
20057
+ },
20058
+ "ownership": {
20059
+ "type": "string",
20060
+ "enum": [
20061
+ "unmanaged",
20062
+ "file-managed"
20063
+ ],
20064
+ "description": "Requested ownership recorded when the document is applied."
20065
+ }
20066
+ },
20067
+ "additionalProperties": false
20068
+ },
20069
+ "spec": {
20070
+ "$ref": "#/$defs/IssuanceConfig"
20071
+ }
20172
20072
  },
20173
- (error) => {
20174
- if (error.code !== "ENOENT") throw error;
20175
- }
20176
- );
20177
- await mkdir3(dirname3(destination), { recursive: true });
20178
- const staging = await mkdtemp(
20179
- join4(dirname3(destination), ".eudiplo-upgrade-")
20180
- );
20181
- try {
20073
+ "required": [
20074
+ "$schema",
20075
+ "spec"
20076
+ ],
20077
+ "additionalProperties": false,
20078
+ "$defs": {
20079
+ "IssuanceConfig": {
20080
+ "type": "object",
20081
+ "properties": {
20082
+ "batchSize": {
20083
+ "description": "Optional issuance batch size.",
20084
+ "type": "integer",
20085
+ "minimum": 1,
20086
+ "maximum": 9007199254740991
20087
+ },
20088
+ "dPopRequired": {
20089
+ "description": "Require DPoP proofs for issuance endpoints.",
20090
+ "type": "boolean"
20091
+ },
20092
+ "walletAttestationRequired": {
20093
+ "description": "Default wallet attestation requirement for managed authorization servers.",
20094
+ "type": "boolean"
20095
+ },
20096
+ "walletProviderTrustLists": {
20097
+ "description": "Shared wallet provider trust lists for key attestations and default authorization server wallet authentication.",
20098
+ "type": "array",
20099
+ "items": {
20100
+ "type": "object",
20101
+ "properties": {
20102
+ "trustListId": {
20103
+ "type": "string",
20104
+ "minLength": 1
20105
+ },
20106
+ "url": {
20107
+ "description": "URL of the wallet provider trust list.",
20108
+ "type": "string",
20109
+ "format": "uri"
20110
+ },
20111
+ "verifierKey": {
20112
+ "description": "Optional verifier key material used for trust list verification.",
20113
+ "type": "object",
20114
+ "propertyNames": {
20115
+ "type": "string"
20116
+ },
20117
+ "additionalProperties": {}
20118
+ },
20119
+ "verifierX509Der": {
20120
+ "description": "Optional verifier certificate in DER/base64 form.",
20121
+ "type": "string"
20122
+ }
20123
+ },
20124
+ "additionalProperties": false
20125
+ }
20126
+ },
20127
+ "signingKeyId": {
20128
+ "description": "Default signing key chain id for credential issuance.",
20129
+ "type": "string",
20130
+ "minLength": 1
20131
+ },
20132
+ "authorizationServers": {
20133
+ "minItems": 1,
20134
+ "type": "array",
20135
+ "items": {
20136
+ "oneOf": [
20137
+ {
20138
+ "type": "object",
20139
+ "properties": {
20140
+ "type": {
20141
+ "type": "string",
20142
+ "const": "external",
20143
+ "description": "Use an externally managed authorization server."
20144
+ },
20145
+ "id": {
20146
+ "type": "string",
20147
+ "minLength": 1,
20148
+ "description": "Authorization server identifier."
20149
+ },
20150
+ "issuer": {
20151
+ "type": "string",
20152
+ "format": "uri",
20153
+ "description": "Issuer URL for the external authorization server."
20154
+ },
20155
+ "sessionBinding": {
20156
+ "description": "Explicit mapping from an external access-token claim to an existing issuance session.",
20157
+ "type": "object",
20158
+ "properties": {
20159
+ "method": {
20160
+ "type": "string",
20161
+ "const": "access_token_claim",
20162
+ "description": "Read the correlation value from a configured access-token claim."
20163
+ },
20164
+ "claim": {
20165
+ "type": "string",
20166
+ "minLength": 1,
20167
+ "description": "Claim name that contains the issuance-session correlation value."
20168
+ }
20169
+ },
20170
+ "required": [
20171
+ "method",
20172
+ "claim"
20173
+ ],
20174
+ "additionalProperties": false
20175
+ },
20176
+ "label": {
20177
+ "description": "Optional display label for UI selection.",
20178
+ "type": "string"
20179
+ },
20180
+ "enabled": {
20181
+ "description": "Whether this authorization server entry is enabled.",
20182
+ "type": "boolean"
20183
+ }
20184
+ },
20185
+ "required": [
20186
+ "type",
20187
+ "id",
20188
+ "issuer"
20189
+ ],
20190
+ "additionalProperties": false
20191
+ },
20192
+ {
20193
+ "type": "object",
20194
+ "properties": {
20195
+ "type": {
20196
+ "type": "string",
20197
+ "const": "oid4vp",
20198
+ "description": "Use OID4VP-based authorization server chaining."
20199
+ },
20200
+ "id": {
20201
+ "type": "string",
20202
+ "minLength": 1,
20203
+ "description": "Authorization server identifier."
20204
+ },
20205
+ "presentationConfigId": {
20206
+ "type": "string",
20207
+ "minLength": 1,
20208
+ "description": "Presentation configuration id used during authorization."
20209
+ },
20210
+ "immediateWalletRedirect": {
20211
+ "description": "Redirect wallets immediately after authorization response creation.",
20212
+ "type": "boolean"
20213
+ },
20214
+ "token": {
20215
+ "description": "Optional token issuance settings.",
20216
+ "type": "object",
20217
+ "properties": {
20218
+ "lifetimeSeconds": {
20219
+ "description": "Access token lifetime in seconds.",
20220
+ "type": "number",
20221
+ "minimum": 60
20222
+ },
20223
+ "signingKeyId": {
20224
+ "description": "Optional key chain id used to sign issued tokens.",
20225
+ "type": "string",
20226
+ "minLength": 1
20227
+ },
20228
+ "refreshTokenEnabled": {
20229
+ "description": "Enable issuing refresh tokens.",
20230
+ "type": "boolean"
20231
+ },
20232
+ "refreshTokenExpiresInSeconds": {
20233
+ "description": "Refresh token lifetime in seconds.",
20234
+ "type": "number",
20235
+ "minimum": 60
20236
+ }
20237
+ },
20238
+ "additionalProperties": false
20239
+ },
20240
+ "requireDPoP": {
20241
+ "description": "Require DPoP proofs for token/credential requests.",
20242
+ "type": "boolean"
20243
+ },
20244
+ "walletAttestationRequired": {
20245
+ "description": "Require wallet attestation for this authorization server.",
20246
+ "type": "boolean"
20247
+ },
20248
+ "walletProviderTrustLists": {
20249
+ "description": "Optional wallet provider trust list references for this authorization server.",
20250
+ "type": "array",
20251
+ "items": {
20252
+ "type": "object",
20253
+ "properties": {
20254
+ "trustListId": {
20255
+ "type": "string",
20256
+ "minLength": 1
20257
+ },
20258
+ "url": {
20259
+ "description": "URL of the wallet provider trust list.",
20260
+ "type": "string",
20261
+ "format": "uri"
20262
+ },
20263
+ "verifierKey": {
20264
+ "description": "Optional verifier key material used for trust list verification.",
20265
+ "type": "object",
20266
+ "propertyNames": {
20267
+ "type": "string"
20268
+ },
20269
+ "additionalProperties": {}
20270
+ },
20271
+ "verifierX509Der": {
20272
+ "description": "Optional verifier certificate in DER/base64 form.",
20273
+ "type": "string"
20274
+ }
20275
+ },
20276
+ "additionalProperties": false
20277
+ }
20278
+ },
20279
+ "label": {
20280
+ "description": "Optional display label for UI selection.",
20281
+ "type": "string"
20282
+ },
20283
+ "enabled": {
20284
+ "description": "Whether this authorization server entry is enabled.",
20285
+ "type": "boolean"
20286
+ }
20287
+ },
20288
+ "required": [
20289
+ "type",
20290
+ "id",
20291
+ "presentationConfigId"
20292
+ ],
20293
+ "additionalProperties": false
20294
+ },
20295
+ {
20296
+ "type": "object",
20297
+ "properties": {
20298
+ "type": {
20299
+ "type": "string",
20300
+ "const": "chained",
20301
+ "description": "Use upstream OIDC as authorization source."
20302
+ },
20303
+ "id": {
20304
+ "type": "string",
20305
+ "minLength": 1,
20306
+ "description": "Authorization server identifier."
20307
+ },
20308
+ "upstream": {
20309
+ "type": "object",
20310
+ "properties": {
20311
+ "issuer": {
20312
+ "type": "string",
20313
+ "format": "uri",
20314
+ "description": "Upstream OIDC issuer URL."
20315
+ },
20316
+ "clientId": {
20317
+ "type": "string",
20318
+ "minLength": 1,
20319
+ "description": "Client id for upstream OIDC authentication."
20320
+ },
20321
+ "clientSecret": {
20322
+ "description": "Optional client secret for upstream OIDC authentication.",
20323
+ "type": "string",
20324
+ "minLength": 1
20325
+ },
20326
+ "scopes": {
20327
+ "description": "Optional scopes requested from the upstream issuer.",
20328
+ "type": "array",
20329
+ "items": {
20330
+ "type": "string",
20331
+ "minLength": 1
20332
+ }
20333
+ }
20334
+ },
20335
+ "required": [
20336
+ "issuer",
20337
+ "clientId"
20338
+ ],
20339
+ "additionalProperties": false,
20340
+ "description": "Upstream OIDC connection settings."
20341
+ },
20342
+ "token": {
20343
+ "description": "Optional token issuance settings.",
20344
+ "type": "object",
20345
+ "properties": {
20346
+ "lifetimeSeconds": {
20347
+ "description": "Access token lifetime in seconds.",
20348
+ "type": "number",
20349
+ "minimum": 60
20350
+ },
20351
+ "signingKeyId": {
20352
+ "description": "Optional key chain id used to sign issued tokens.",
20353
+ "type": "string",
20354
+ "minLength": 1
20355
+ },
20356
+ "refreshTokenEnabled": {
20357
+ "description": "Enable issuing refresh tokens.",
20358
+ "type": "boolean"
20359
+ },
20360
+ "refreshTokenExpiresInSeconds": {
20361
+ "description": "Refresh token lifetime in seconds.",
20362
+ "type": "number",
20363
+ "minimum": 60
20364
+ }
20365
+ },
20366
+ "additionalProperties": false
20367
+ },
20368
+ "requireDPoP": {
20369
+ "description": "Require DPoP proofs for token/credential requests.",
20370
+ "type": "boolean"
20371
+ },
20372
+ "walletAttestationRequired": {
20373
+ "description": "Require wallet attestation for this authorization server.",
20374
+ "type": "boolean"
20375
+ },
20376
+ "walletProviderTrustLists": {
20377
+ "description": "Optional wallet provider trust list references for this authorization server.",
20378
+ "type": "array",
20379
+ "items": {
20380
+ "type": "object",
20381
+ "properties": {
20382
+ "trustListId": {
20383
+ "type": "string",
20384
+ "minLength": 1
20385
+ },
20386
+ "url": {
20387
+ "description": "URL of the wallet provider trust list.",
20388
+ "type": "string",
20389
+ "format": "uri"
20390
+ },
20391
+ "verifierKey": {
20392
+ "description": "Optional verifier key material used for trust list verification.",
20393
+ "type": "object",
20394
+ "propertyNames": {
20395
+ "type": "string"
20396
+ },
20397
+ "additionalProperties": {}
20398
+ },
20399
+ "verifierX509Der": {
20400
+ "description": "Optional verifier certificate in DER/base64 form.",
20401
+ "type": "string"
20402
+ }
20403
+ },
20404
+ "additionalProperties": false
20405
+ }
20406
+ },
20407
+ "label": {
20408
+ "description": "Optional display label for UI selection.",
20409
+ "type": "string"
20410
+ },
20411
+ "enabled": {
20412
+ "description": "Whether this authorization server entry is enabled.",
20413
+ "type": "boolean"
20414
+ }
20415
+ },
20416
+ "required": [
20417
+ "type",
20418
+ "id",
20419
+ "upstream"
20420
+ ],
20421
+ "additionalProperties": false
20422
+ },
20423
+ {
20424
+ "type": "object",
20425
+ "properties": {
20426
+ "type": {
20427
+ "type": "string",
20428
+ "const": "built-in",
20429
+ "description": "Use EUDIPLO built-in authorization server."
20430
+ },
20431
+ "id": {
20432
+ "type": "string",
20433
+ "minLength": 1,
20434
+ "description": "Authorization server identifier."
20435
+ },
20436
+ "token": {
20437
+ "description": "Optional token issuance settings.",
20438
+ "type": "object",
20439
+ "properties": {
20440
+ "lifetimeSeconds": {
20441
+ "description": "Access token lifetime in seconds.",
20442
+ "type": "number",
20443
+ "minimum": 60
20444
+ },
20445
+ "signingKeyId": {
20446
+ "description": "Optional key chain id used to sign issued tokens.",
20447
+ "type": "string",
20448
+ "minLength": 1
20449
+ },
20450
+ "refreshTokenEnabled": {
20451
+ "description": "Enable issuing refresh tokens.",
20452
+ "type": "boolean"
20453
+ },
20454
+ "refreshTokenExpiresInSeconds": {
20455
+ "description": "Refresh token lifetime in seconds.",
20456
+ "type": "number",
20457
+ "minimum": 60
20458
+ }
20459
+ },
20460
+ "additionalProperties": false
20461
+ },
20462
+ "requireDPoP": {
20463
+ "description": "Require DPoP proofs for token/credential requests.",
20464
+ "type": "boolean"
20465
+ },
20466
+ "walletAttestationRequired": {
20467
+ "description": "Require wallet attestation for this authorization server.",
20468
+ "type": "boolean"
20469
+ },
20470
+ "walletProviderTrustLists": {
20471
+ "description": "Optional wallet provider trust list references for this authorization server.",
20472
+ "type": "array",
20473
+ "items": {
20474
+ "type": "object",
20475
+ "properties": {
20476
+ "trustListId": {
20477
+ "type": "string",
20478
+ "minLength": 1
20479
+ },
20480
+ "url": {
20481
+ "description": "URL of the wallet provider trust list.",
20482
+ "type": "string",
20483
+ "format": "uri"
20484
+ },
20485
+ "verifierKey": {
20486
+ "description": "Optional verifier key material used for trust list verification.",
20487
+ "type": "object",
20488
+ "propertyNames": {
20489
+ "type": "string"
20490
+ },
20491
+ "additionalProperties": {}
20492
+ },
20493
+ "verifierX509Der": {
20494
+ "description": "Optional verifier certificate in DER/base64 form.",
20495
+ "type": "string"
20496
+ }
20497
+ },
20498
+ "additionalProperties": false
20499
+ }
20500
+ },
20501
+ "label": {
20502
+ "description": "Optional display label for UI selection.",
20503
+ "type": "string"
20504
+ },
20505
+ "enabled": {
20506
+ "description": "Whether this authorization server entry is enabled.",
20507
+ "type": "boolean"
20508
+ }
20509
+ },
20510
+ "required": [
20511
+ "type",
20512
+ "id"
20513
+ ],
20514
+ "additionalProperties": false
20515
+ }
20516
+ ],
20517
+ "description": "Supported authorization server configurations."
20518
+ },
20519
+ "description": "Configured authorization server entries."
20520
+ },
20521
+ "federation": {
20522
+ "description": "Optional OpenID Federation settings.",
20523
+ "anyOf": [
20524
+ {
20525
+ "type": "object",
20526
+ "properties": {
20527
+ "role": {
20528
+ "description": "Federation role for this issuer.",
20529
+ "type": "string",
20530
+ "enum": [
20531
+ "trust_anchor",
20532
+ "intermediate",
20533
+ "leaf"
20534
+ ]
20535
+ },
20536
+ "mode": {
20537
+ "description": "Federation operation mode.",
20538
+ "type": "string",
20539
+ "enum": [
20540
+ "federation-only",
20541
+ "hybrid"
20542
+ ]
20543
+ },
20544
+ "entityId": {
20545
+ "description": "Optional local federation entity id.",
20546
+ "type": "string"
20547
+ },
20548
+ "enforceSigningPolicy": {
20549
+ "description": "Enforce strict signing policy checks.",
20550
+ "type": "boolean"
20551
+ },
20552
+ "cacheTtlSeconds": {
20553
+ "description": "Cache time-to-live for federation metadata in seconds.",
20554
+ "type": "integer",
20555
+ "minimum": 1,
20556
+ "maximum": 9007199254740991
20557
+ },
20558
+ "trustAnchors": {
20559
+ "type": "array",
20560
+ "items": {
20561
+ "type": "object",
20562
+ "properties": {
20563
+ "entityId": {
20564
+ "type": "string",
20565
+ "minLength": 1,
20566
+ "description": "Federation trust anchor entity id."
20567
+ },
20568
+ "entityConfigurationUri": {
20569
+ "type": "string",
20570
+ "format": "uri",
20571
+ "description": "Entity configuration URI for the trust anchor."
20572
+ }
20573
+ },
20574
+ "required": [
20575
+ "entityId",
20576
+ "entityConfigurationUri"
20577
+ ],
20578
+ "additionalProperties": false
20579
+ },
20580
+ "description": "Federation trust anchors."
20581
+ }
20582
+ },
20583
+ "required": [
20584
+ "trustAnchors"
20585
+ ],
20586
+ "additionalProperties": false
20587
+ },
20588
+ {
20589
+ "type": "null"
20590
+ }
20591
+ ]
20592
+ },
20593
+ "registrationCertificate": {
20594
+ "description": "Optional registration certificate settings.",
20595
+ "anyOf": [
20596
+ {
20597
+ "type": "object",
20598
+ "properties": {
20599
+ "enabled": {
20600
+ "description": "Enable issuer registration certificate support.",
20601
+ "type": "boolean"
20602
+ },
20603
+ "mode": {
20604
+ "description": "How registration certificate data is provided.",
20605
+ "type": "string",
20606
+ "enum": [
20607
+ "import",
20608
+ "generate"
20609
+ ]
20610
+ },
20611
+ "jwt": {
20612
+ "description": "Optional registration certificate JWT when using import mode.",
20613
+ "type": "string"
20614
+ },
20615
+ "privacyPolicy": {
20616
+ "description": "Optional privacy policy URI.",
20617
+ "type": "string"
20618
+ },
20619
+ "supportUri": {
20620
+ "description": "Optional support URI.",
20621
+ "type": "string"
20622
+ }
20623
+ },
20624
+ "additionalProperties": false
20625
+ },
20626
+ {
20627
+ "type": "null"
20628
+ }
20629
+ ]
20630
+ },
20631
+ "display": {
20632
+ "description": "Localized issuer metadata shown to wallets.",
20633
+ "type": "array",
20634
+ "items": {
20635
+ "type": "object",
20636
+ "properties": {
20637
+ "name": {
20638
+ "description": "Issuer display name.",
20639
+ "type": "string"
20640
+ },
20641
+ "locale": {
20642
+ "description": "Locale tag for this display entry.",
20643
+ "type": "string"
20644
+ },
20645
+ "logo": {
20646
+ "description": "Optional issuer logo metadata.",
20647
+ "type": "object",
20648
+ "properties": {
20649
+ "uri": {
20650
+ "type": "string",
20651
+ "minLength": 1,
20652
+ "description": "Logo URI."
20653
+ },
20654
+ "alt_text": {
20655
+ "description": "Optional localized alternative text.",
20656
+ "type": "string"
20657
+ }
20658
+ },
20659
+ "required": [
20660
+ "uri"
20661
+ ],
20662
+ "additionalProperties": {}
20663
+ }
20664
+ },
20665
+ "additionalProperties": {}
20666
+ }
20667
+ },
20668
+ "notificationEndpointEnabled": {
20669
+ "description": "Whether the OID4VCI notification endpoint is exposed for this issuance configuration.",
20670
+ "type": "boolean"
20671
+ },
20672
+ "credentialResponseEncryption": {
20673
+ "description": "Enable encrypted credential responses.",
20674
+ "type": "boolean"
20675
+ },
20676
+ "credentialRequestEncryption": {
20677
+ "description": "Require encrypted credential requests.",
20678
+ "type": "boolean"
20679
+ },
20680
+ "txCodeMaxAttempts": {
20681
+ "description": "Maximum verification attempts for transaction codes. Null resets to defaults.",
20682
+ "anyOf": [
20683
+ {
20684
+ "type": "integer",
20685
+ "minimum": 1,
20686
+ "maximum": 9007199254740991
20687
+ },
20688
+ {
20689
+ "type": "null"
20690
+ }
20691
+ ]
20692
+ },
20693
+ "offerLifetimeSeconds": {
20694
+ "description": "Default lifetime of credential offers in seconds. Can be overridden per offer request. Null or unset: offers do not expire.",
20695
+ "anyOf": [
20696
+ {
20697
+ "type": "integer",
20698
+ "minimum": 1,
20699
+ "maximum": 9007199254740991
20700
+ },
20701
+ {
20702
+ "type": "null"
20703
+ }
20704
+ ]
20705
+ }
20706
+ },
20707
+ "required": [
20708
+ "authorizationServers"
20709
+ ],
20710
+ "additionalProperties": false,
20711
+ "title": "IssuanceConfig"
20712
+ }
20713
+ }
20714
+ },
20715
+ {
20716
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
20717
+ "$id": "https://eudiplo.dev/schemas/v2/PresentationConfigFile.schema.json",
20718
+ "title": "PresentationConfigFile",
20719
+ "type": "object",
20720
+ "properties": {
20721
+ "$schema": {
20722
+ "type": "string",
20723
+ "const": "https://eudiplo.dev/schemas/v2/PresentationConfigFile.schema.json"
20724
+ },
20725
+ "metadata": {
20726
+ "type": "object",
20727
+ "properties": {
20728
+ "generation": {
20729
+ "type": "integer",
20730
+ "minimum": 1,
20731
+ "description": "Monotonically increasing configuration generation."
20732
+ },
20733
+ "ownership": {
20734
+ "type": "string",
20735
+ "enum": [
20736
+ "unmanaged",
20737
+ "file-managed"
20738
+ ],
20739
+ "description": "Requested ownership recorded when the document is applied."
20740
+ }
20741
+ },
20742
+ "additionalProperties": false
20743
+ },
20744
+ "spec": {
20745
+ "allOf": [
20746
+ {
20747
+ "$ref": "#/$defs/PresentationConfigCreateDto"
20748
+ },
20749
+ {
20750
+ "type": "object",
20751
+ "required": [
20752
+ "id"
20753
+ ],
20754
+ "properties": {
20755
+ "id": {
20756
+ "type": "string",
20757
+ "minLength": 1
20758
+ }
20759
+ }
20760
+ }
20761
+ ]
20762
+ }
20763
+ },
20764
+ "required": [
20765
+ "$schema",
20766
+ "spec"
20767
+ ],
20768
+ "additionalProperties": false,
20769
+ "$defs": {
20770
+ "PresentationConfigCreateDto": {
20771
+ "type": "object",
20772
+ "properties": {
20773
+ "id": {
20774
+ "type": "string",
20775
+ "minLength": 1,
20776
+ "description": "Presentation configuration identifier."
20777
+ },
20778
+ "description": {
20779
+ "description": "Optional presentation configuration description.",
20780
+ "type": [
20781
+ "string",
20782
+ "null"
20783
+ ]
20784
+ },
20785
+ "lifeTime": {
20786
+ "description": "Presentation request lifetime in seconds.",
20787
+ "type": "integer",
20788
+ "minimum": 1,
20789
+ "maximum": 9007199254740991
20790
+ },
20791
+ "skewSeconds": {
20792
+ "description": "Clock skew tolerance in seconds.",
20793
+ "type": "integer",
20794
+ "minimum": 0,
20795
+ "maximum": 9007199254740991
20796
+ },
20797
+ "statusCheckMode": {
20798
+ "description": "Revocation/status check mode.",
20799
+ "type": "string",
20800
+ "enum": [
20801
+ "strict",
20802
+ "best_effort",
20803
+ "disabled"
20804
+ ]
20805
+ },
20806
+ "dcql_query": {
20807
+ "type": "object",
20808
+ "properties": {
20809
+ "credentials": {
20810
+ "minItems": 1,
20811
+ "type": "array",
20812
+ "items": {
20813
+ "oneOf": [
20814
+ {
20815
+ "type": "object",
20816
+ "properties": {
20817
+ "id": {
20818
+ "type": "string",
20819
+ "minLength": 1,
20820
+ "pattern": "^[A-Za-z0-9_-]+$",
20821
+ "description": "Credential query identifier."
20822
+ },
20823
+ "multiple": {
20824
+ "description": "Allow multiple matching credentials.",
20825
+ "type": "boolean"
20826
+ },
20827
+ "claim_sets": {
20828
+ "description": "Optional claim set constraints.",
20829
+ "type": "array",
20830
+ "items": {
20831
+ "type": "array",
20832
+ "items": {
20833
+ "type": "string"
20834
+ }
20835
+ }
20836
+ },
20837
+ "trusted_authorities": {
20838
+ "description": "Optional trusted authority constraints.",
20839
+ "type": "array",
20840
+ "items": {
20841
+ "oneOf": [
20842
+ {
20843
+ "type": "object",
20844
+ "properties": {
20845
+ "type": {
20846
+ "type": "string",
20847
+ "const": "etsi_tl",
20848
+ "description": "Trusted authority type discriminator for ETSI trust lists."
20849
+ },
20850
+ "values": {
20851
+ "type": "array",
20852
+ "items": {
20853
+ "type": "object",
20854
+ "properties": {
20855
+ "trustListId": {
20856
+ "description": "Optional trust list id reference.",
20857
+ "type": "string"
20858
+ },
20859
+ "url": {
20860
+ "description": "Optional trust list URL reference.",
20861
+ "anyOf": [
20862
+ {
20863
+ "type": "string",
20864
+ "format": "uri"
20865
+ },
20866
+ {
20867
+ "type": "string",
20868
+ "pattern": "^<TENANT_URL>(?:\\/.*)?$"
20869
+ }
20870
+ ]
20871
+ },
20872
+ "verifierKey": {
20873
+ "description": "Optional verifier key material.",
20874
+ "type": "object",
20875
+ "propertyNames": {
20876
+ "type": "string"
20877
+ },
20878
+ "additionalProperties": {}
20879
+ },
20880
+ "verifierX509Der": {
20881
+ "description": "Optional verifier certificate in DER/base64 form.",
20882
+ "type": "string"
20883
+ }
20884
+ },
20885
+ "additionalProperties": false
20886
+ },
20887
+ "description": "Trust list references for ETSI TL verification."
20888
+ }
20889
+ },
20890
+ "required": [
20891
+ "type",
20892
+ "values"
20893
+ ],
20894
+ "additionalProperties": false
20895
+ },
20896
+ {
20897
+ "type": "object",
20898
+ "properties": {
20899
+ "type": {
20900
+ "type": "string",
20901
+ "const": "openid_federation",
20902
+ "description": "Trusted authority type discriminator for OpenID Federation."
20903
+ },
20904
+ "values": {
20905
+ "type": "array",
20906
+ "items": {
20907
+ "type": "string"
20908
+ },
20909
+ "description": "OpenID Federation authority identifiers."
20910
+ }
20911
+ },
20912
+ "required": [
20913
+ "type",
20914
+ "values"
20915
+ ],
20916
+ "additionalProperties": false
20917
+ }
20918
+ ]
20919
+ }
20920
+ },
20921
+ "format": {
20922
+ "type": "string",
20923
+ "const": "dc+sd-jwt",
20924
+ "description": "Credential format discriminator."
20925
+ },
20926
+ "meta": {
20927
+ "type": "object",
20928
+ "properties": {
20929
+ "vct_values": {
20930
+ "minItems": 1,
20931
+ "type": "array",
20932
+ "items": {
20933
+ "type": "string"
20934
+ },
20935
+ "description": "Accepted VCT values."
20936
+ }
20937
+ },
20938
+ "required": [
20939
+ "vct_values"
20940
+ ],
20941
+ "additionalProperties": false
20942
+ },
20943
+ "claims": {
20944
+ "description": "Optional claim-level constraints.",
20945
+ "type": "array",
20946
+ "items": {
20947
+ "type": "object",
20948
+ "properties": {
20949
+ "id": {
20950
+ "description": "Optional claim query id.",
20951
+ "type": "string"
20952
+ },
20953
+ "path": {
20954
+ "type": "array",
20955
+ "items": {
20956
+ "type": [
20957
+ "string",
20958
+ "number"
20959
+ ]
20960
+ },
20961
+ "description": "Path to the claim value in presented credentials."
20962
+ },
20963
+ "values": {
20964
+ "description": "Optional allowed values for the claim.",
20965
+ "type": "array",
20966
+ "items": {
20967
+ "type": "string"
20968
+ }
20969
+ }
20970
+ },
20971
+ "required": [
20972
+ "path"
20973
+ ],
20974
+ "additionalProperties": false
20975
+ }
20976
+ }
20977
+ },
20978
+ "required": [
20979
+ "id",
20980
+ "format",
20981
+ "meta"
20982
+ ],
20983
+ "additionalProperties": false
20984
+ },
20985
+ {
20986
+ "type": "object",
20987
+ "properties": {
20988
+ "id": {
20989
+ "type": "string",
20990
+ "minLength": 1,
20991
+ "pattern": "^[A-Za-z0-9_-]+$",
20992
+ "description": "Credential query identifier."
20993
+ },
20994
+ "multiple": {
20995
+ "description": "Allow multiple matching credentials.",
20996
+ "type": "boolean"
20997
+ },
20998
+ "claim_sets": {
20999
+ "description": "Optional claim set constraints.",
21000
+ "type": "array",
21001
+ "items": {
21002
+ "type": "array",
21003
+ "items": {
21004
+ "type": "string"
21005
+ }
21006
+ }
21007
+ },
21008
+ "trusted_authorities": {
21009
+ "description": "Optional trusted authority constraints.",
21010
+ "type": "array",
21011
+ "items": {
21012
+ "oneOf": [
21013
+ {
21014
+ "type": "object",
21015
+ "properties": {
21016
+ "type": {
21017
+ "type": "string",
21018
+ "const": "etsi_tl",
21019
+ "description": "Trusted authority type discriminator for ETSI trust lists."
21020
+ },
21021
+ "values": {
21022
+ "type": "array",
21023
+ "items": {
21024
+ "type": "object",
21025
+ "properties": {
21026
+ "trustListId": {
21027
+ "description": "Optional trust list id reference.",
21028
+ "type": "string"
21029
+ },
21030
+ "url": {
21031
+ "description": "Optional trust list URL reference.",
21032
+ "anyOf": [
21033
+ {
21034
+ "type": "string",
21035
+ "format": "uri"
21036
+ },
21037
+ {
21038
+ "type": "string",
21039
+ "pattern": "^<TENANT_URL>(?:\\/.*)?$"
21040
+ }
21041
+ ]
21042
+ },
21043
+ "verifierKey": {
21044
+ "description": "Optional verifier key material.",
21045
+ "type": "object",
21046
+ "propertyNames": {
21047
+ "type": "string"
21048
+ },
21049
+ "additionalProperties": {}
21050
+ },
21051
+ "verifierX509Der": {
21052
+ "description": "Optional verifier certificate in DER/base64 form.",
21053
+ "type": "string"
21054
+ }
21055
+ },
21056
+ "additionalProperties": false
21057
+ },
21058
+ "description": "Trust list references for ETSI TL verification."
21059
+ }
21060
+ },
21061
+ "required": [
21062
+ "type",
21063
+ "values"
21064
+ ],
21065
+ "additionalProperties": false
21066
+ },
21067
+ {
21068
+ "type": "object",
21069
+ "properties": {
21070
+ "type": {
21071
+ "type": "string",
21072
+ "const": "openid_federation",
21073
+ "description": "Trusted authority type discriminator for OpenID Federation."
21074
+ },
21075
+ "values": {
21076
+ "type": "array",
21077
+ "items": {
21078
+ "type": "string"
21079
+ },
21080
+ "description": "OpenID Federation authority identifiers."
21081
+ }
21082
+ },
21083
+ "required": [
21084
+ "type",
21085
+ "values"
21086
+ ],
21087
+ "additionalProperties": false
21088
+ }
21089
+ ]
21090
+ }
21091
+ },
21092
+ "format": {
21093
+ "type": "string",
21094
+ "const": "mso_mdoc",
21095
+ "description": "Credential format discriminator."
21096
+ },
21097
+ "meta": {
21098
+ "type": "object",
21099
+ "properties": {
21100
+ "doctype_value": {
21101
+ "type": "string",
21102
+ "minLength": 1,
21103
+ "description": "Expected mDoc doctype value."
21104
+ }
21105
+ },
21106
+ "required": [
21107
+ "doctype_value"
21108
+ ],
21109
+ "additionalProperties": false
21110
+ },
21111
+ "claims": {
21112
+ "description": "Optional mDoc claim-level constraints.",
21113
+ "type": "array",
21114
+ "items": {
21115
+ "type": "object",
21116
+ "properties": {
21117
+ "id": {
21118
+ "description": "Optional claim query id.",
21119
+ "type": "string"
21120
+ },
21121
+ "path": {
21122
+ "type": "array",
21123
+ "items": {
21124
+ "type": [
21125
+ "string",
21126
+ "number"
21127
+ ]
21128
+ },
21129
+ "description": "Path to the claim value in presented credentials."
21130
+ },
21131
+ "values": {
21132
+ "description": "Optional allowed values for the claim.",
21133
+ "type": "array",
21134
+ "items": {
21135
+ "type": "string"
21136
+ }
21137
+ },
21138
+ "intent_to_retain": {
21139
+ "description": "Whether relying party intends to retain the claim.",
21140
+ "type": "boolean"
21141
+ }
21142
+ },
21143
+ "required": [
21144
+ "path"
21145
+ ],
21146
+ "additionalProperties": false
21147
+ }
21148
+ }
21149
+ },
21150
+ "required": [
21151
+ "id",
21152
+ "format",
21153
+ "meta"
21154
+ ],
21155
+ "additionalProperties": false
21156
+ }
21157
+ ]
21158
+ },
21159
+ "description": "Credential queries requested by the verifier."
21160
+ },
21161
+ "credential_sets": {
21162
+ "description": "Optional higher-level credential set requirements.",
21163
+ "type": "array",
21164
+ "items": {
21165
+ "type": "object",
21166
+ "properties": {
21167
+ "options": {
21168
+ "minItems": 1,
21169
+ "type": "array",
21170
+ "items": {
21171
+ "minItems": 1,
21172
+ "type": "array",
21173
+ "items": {
21174
+ "type": "string"
21175
+ }
21176
+ },
21177
+ "description": "Alternative credential query id combinations."
21178
+ },
21179
+ "required": {
21180
+ "description": "Whether this credential set is mandatory.",
21181
+ "type": "boolean"
21182
+ }
21183
+ },
21184
+ "required": [
21185
+ "options"
21186
+ ],
21187
+ "additionalProperties": false
21188
+ }
21189
+ }
21190
+ },
21191
+ "required": [
21192
+ "credentials"
21193
+ ],
21194
+ "additionalProperties": false,
21195
+ "description": "DCQL query defining requested credentials and claims."
21196
+ },
21197
+ "transaction_data": {
21198
+ "description": "Optional transaction data descriptors.",
21199
+ "anyOf": [
21200
+ {
21201
+ "type": "array",
21202
+ "items": {
21203
+ "type": "object",
21204
+ "properties": {
21205
+ "type": {
21206
+ "type": "string",
21207
+ "description": "Transaction data type identifier."
21208
+ },
21209
+ "credential_ids": {
21210
+ "type": "array",
21211
+ "items": {
21212
+ "type": "string"
21213
+ },
21214
+ "description": "Credential query ids this transaction data applies to."
21215
+ },
21216
+ "payload": {
21217
+ "description": "Transaction details. Required for TS12 SCA transaction data."
21218
+ }
21219
+ },
21220
+ "required": [
21221
+ "type",
21222
+ "credential_ids"
21223
+ ],
21224
+ "additionalProperties": {}
21225
+ }
21226
+ },
21227
+ {
21228
+ "type": "null"
21229
+ }
21230
+ ]
21231
+ },
21232
+ "registration_cert": {
21233
+ "description": "Optional registration certificate request settings.",
21234
+ "anyOf": [
21235
+ {
21236
+ "type": "object",
21237
+ "properties": {
21238
+ "id": {
21239
+ "type": "string"
21240
+ },
21241
+ "body": {
21242
+ "type": "object",
21243
+ "properties": {
21244
+ "privacy_policy": {
21245
+ "type": "string"
21246
+ },
21247
+ "support_uri": {
21248
+ "type": "string"
21249
+ },
21250
+ "intermediary": {
21251
+ "type": "string"
21252
+ },
21253
+ "purpose": {
21254
+ "type": "array",
21255
+ "items": {
21256
+ "type": "object",
21257
+ "properties": {
21258
+ "lang": {
21259
+ "type": "string"
21260
+ },
21261
+ "content": {
21262
+ "type": "string"
21263
+ }
21264
+ },
21265
+ "required": [
21266
+ "lang",
21267
+ "content"
21268
+ ],
21269
+ "additionalProperties": false
21270
+ }
21271
+ },
21272
+ "credentials": {
21273
+ "type": "array",
21274
+ "items": {
21275
+ "type": "object",
21276
+ "propertyNames": {
21277
+ "type": "string"
21278
+ },
21279
+ "additionalProperties": {}
21280
+ }
21281
+ },
21282
+ "provides_attestations": {
21283
+ "type": "array",
21284
+ "items": {
21285
+ "type": "string",
21286
+ "minLength": 1
21287
+ }
21288
+ }
21289
+ },
21290
+ "additionalProperties": false
21291
+ },
21292
+ "jwt": {
21293
+ "type": "string"
21294
+ }
21295
+ },
21296
+ "additionalProperties": false
21297
+ },
21298
+ {
21299
+ "type": "null"
21300
+ }
21301
+ ]
21302
+ },
21303
+ "registrationCertImportJwt": {
21304
+ "description": "Optional imported registration certificate JWT.",
21305
+ "type": [
21306
+ "string",
21307
+ "null"
21308
+ ]
21309
+ },
21310
+ "registrationCertImportId": {
21311
+ "description": "Optional registrar-side registration certificate id.",
21312
+ "type": [
21313
+ "string",
21314
+ "null"
21315
+ ]
21316
+ },
21317
+ "registrationCertBodyPrivacyPolicy": {
21318
+ "description": "Optional registration certificate privacy policy URI.",
21319
+ "type": [
21320
+ "string",
21321
+ "null"
21322
+ ]
21323
+ },
21324
+ "registrationCertBodySupportUri": {
21325
+ "description": "Optional registration certificate support URI.",
21326
+ "type": [
21327
+ "string",
21328
+ "null"
21329
+ ]
21330
+ },
21331
+ "registrationCertBodyIntermediary": {
21332
+ "description": "Optional registration certificate intermediary.",
21333
+ "type": [
21334
+ "string",
21335
+ "null"
21336
+ ]
21337
+ },
21338
+ "registrationCertBodyPurpose": {
21339
+ "description": "Optional registration certificate purpose entries.",
21340
+ "anyOf": [
21341
+ {
21342
+ "type": "array",
21343
+ "items": {
21344
+ "type": "object",
21345
+ "properties": {
21346
+ "lang": {
21347
+ "type": "string"
21348
+ },
21349
+ "content": {
21350
+ "type": "string"
21351
+ }
21352
+ },
21353
+ "additionalProperties": false
21354
+ }
21355
+ },
21356
+ {
21357
+ "type": "null"
21358
+ }
21359
+ ]
21360
+ },
21361
+ "webhookEndpointId": {
21362
+ "description": "Optional webhook endpoint id for presentation callbacks.",
21363
+ "type": [
21364
+ "string",
21365
+ "null"
21366
+ ]
21367
+ },
21368
+ "attached": {
21369
+ "description": "Optional attachments included with presentation requests.",
21370
+ "anyOf": [
21371
+ {
21372
+ "type": "array",
21373
+ "items": {
21374
+ "type": "object",
21375
+ "properties": {
21376
+ "format": {
21377
+ "type": "string",
21378
+ "description": "Attachment format identifier."
21379
+ },
21380
+ "data": {
21381
+ "description": "Attachment payload."
21382
+ },
21383
+ "credential_ids": {
21384
+ "description": "Optional credential query ids bound to this attachment.",
21385
+ "type": "array",
21386
+ "items": {
21387
+ "type": "string"
21388
+ }
21389
+ }
21390
+ },
21391
+ "required": [
21392
+ "format",
21393
+ "data"
21394
+ ],
21395
+ "additionalProperties": false
21396
+ }
21397
+ },
21398
+ {
21399
+ "type": "null"
21400
+ }
21401
+ ]
21402
+ },
21403
+ "redirectUri": {
21404
+ "description": "Optional redirect URI after presentation completion.",
21405
+ "type": [
21406
+ "string",
21407
+ "null"
21408
+ ]
21409
+ },
21410
+ "accessKeyChainId": {
21411
+ "description": "Optional key chain id for access token/auth operations.",
21412
+ "type": [
21413
+ "string",
21414
+ "null"
21415
+ ]
21416
+ },
21417
+ "readerAuth": {
21418
+ "description": "Whether reader authentication is required for mDoc requests.",
21419
+ "type": [
21420
+ "boolean",
21421
+ "null"
21422
+ ]
21423
+ }
21424
+ },
21425
+ "required": [
21426
+ "id",
21427
+ "dcql_query"
21428
+ ],
21429
+ "additionalProperties": false,
21430
+ "title": "PresentationConfigCreateDto"
21431
+ }
21432
+ }
21433
+ }
21434
+ ];
21435
+
21436
+ // ../../packages/eudiplo-config-format/dist/config-validator.js
21437
+ var ajv = new import__.Ajv2020({
21438
+ allErrors: true,
21439
+ strict: false,
21440
+ validateFormats: false
21441
+ });
21442
+ for (const schema of CONFIG_SCHEMAS)
21443
+ ajv.addSchema(schema);
21444
+ function validateConfigDocument(document) {
21445
+ const file = serializeDocument(document);
21446
+ const validate = ajv.getSchema(file.$schema);
21447
+ if (!validate)
21448
+ throw new Error(`No bundled validator for ${file.$schema}`);
21449
+ const issues = [];
21450
+ if (!validate(file)) {
21451
+ for (const issue of validate.errors ?? [])
21452
+ issues.push({
21453
+ severity: "error",
21454
+ code: "CONFIG_SCHEMA_VALIDATION_FAILED",
21455
+ path: issue.instancePath || "/",
21456
+ message: issue.keyword === "additionalProperties" ? `Unknown property: ${String(issue.params.additionalProperty)}` : issue.message ?? "Invalid configuration",
21457
+ resource: { kind: document.kind, id: resourceId(document) }
21458
+ });
21459
+ }
21460
+ const id = document.kind === "Client" ? document.spec.clientId : document.spec.id;
21461
+ if (id !== void 0 && id !== resourceId(document))
21462
+ issues.push({
21463
+ severity: "error",
21464
+ code: "RESOURCE_ID_MISMATCH",
21465
+ path: document.kind === "Client" ? "/spec/clientId" : "/spec/id",
21466
+ message: "Resource identifier does not match metadata.id"
21467
+ });
21468
+ return issues;
21469
+ }
21470
+
21471
+ // src/commands/config/portability/migrations.ts
21472
+ function upgradeDocument(input) {
21473
+ const result = migrateDocument(input, validateConfigDocument);
21474
+ return { ...result, document: serializeDocument(result.document) };
21475
+ }
21476
+
21477
+ // src/commands/config/portability/upgrade.ts
21478
+ var codec = new ConfigBundleCodec();
21479
+ function parse3(text, path2) {
21480
+ try {
21481
+ return JSON.parse(text);
21482
+ } catch {
21483
+ throw new Error(`Invalid JSON in ${path2}`);
21484
+ }
21485
+ }
21486
+ function upgradeJson(input, file, context) {
21487
+ const bundle = Array.isArray(input?.documents);
21488
+ if (bundle) assertConfigBundle(input);
21489
+ const results = (bundle ? input.documents : [input]).map(
21490
+ (document) => upgradeDocument(document)
21491
+ );
21492
+ let blocked = false;
21493
+ for (const result of results) {
21494
+ for (const issue of result.issues) {
21495
+ context.stderr.write(
21496
+ `${file}${issue.path}: ${issue.severity} ${issue.code}: ${issue.message}
21497
+ `
21498
+ );
21499
+ if (issue.severity !== "warning") blocked = true;
21500
+ }
21501
+ for (const migration of result.migrations)
21502
+ context.stdout.write(`${file}: migration ${migration}
21503
+ `);
21504
+ }
21505
+ if (blocked) throw new Error(`${file}: upgrade blocked; no output written`);
21506
+ if (!bundle) return results[0].document;
21507
+ const documents = results.map(
21508
+ (result) => result.document
21509
+ );
21510
+ const resources = input.manifest.resources.map(
21511
+ (resource) => {
21512
+ const index = input.documents.findIndex(
21513
+ (candidate) => normalizeDocument(candidate).kind === resource.kind && resourceId(candidate) === resource.id
21514
+ );
21515
+ const document = documents[index];
21516
+ return {
21517
+ ...resource,
21518
+ id: resourceId(document),
21519
+ $schema: document.$schema,
21520
+ sha256: createHash2("sha256").update(JSON.stringify(document)).digest("hex")
21521
+ };
21522
+ }
21523
+ );
21524
+ const output = {
21525
+ ...input,
21526
+ manifest: { ...input.manifest, formatVersion: 2, resources },
21527
+ documents
21528
+ };
21529
+ assertConfigBundle(output);
21530
+ return output;
21531
+ }
21532
+ function report(input, output, file, parsed, context) {
21533
+ const changed = stableConfigJson(input) !== stableConfigJson(output);
21534
+ context.stdout.write(
21535
+ `${file}: ${changed ? "upgrade required" : "current"}
21536
+ `
21537
+ );
21538
+ if (parsed.flags.diff === true)
21539
+ for (const change of configChanges(input, output)) {
21540
+ context.stdout.write(
21541
+ ` ${change.path}: ${JSON.stringify(change.before) ?? "(absent)"} -> ${JSON.stringify(change.after) ?? "(absent)"}
21542
+ `
21543
+ );
21544
+ }
21545
+ return changed;
21546
+ }
21547
+ function inferredKind(path2) {
21548
+ const normalized = path2.split(sep2).join("/");
21549
+ const match = TENANT_RESOURCE_REGISTRY.find(
21550
+ (entry) => entry.kind === "file" ? normalized === entry.file || normalized.endsWith(`/${entry.file}`) : normalized.endsWith(`/${entry.subfolder}/${basename2(path2)}`) || normalized === `${entry.subfolder}/${basename2(path2)}`
21551
+ );
21552
+ return CONFIG_RESOURCE_KINDS.find(
21553
+ (kind) => `${CONFIG_FORMATS[kind].file}.schema.json` === match?.schemaFile
21554
+ );
21555
+ }
21556
+ function wrapBare(kind, input, path2) {
21557
+ if (!input || typeof input !== "object" || Array.isArray(input))
21558
+ throw new Error(`${path2}: configuration must be an object`);
21559
+ const singleton = {
21560
+ Tenant: "tenant",
21561
+ KmsConfig: "kms",
21562
+ RegistrarConfig: "registrar",
21563
+ IssuanceConfig: "issuance"
21564
+ };
21565
+ const spec = structuredClone(input);
21566
+ if (kind === "KeyChain" && spec.key && !spec.keySource) {
21567
+ spec.keySource = { type: "private-jwk", jwk: spec.key };
21568
+ delete spec.key;
21569
+ }
21570
+ const id = String(
21571
+ singleton[kind] ?? spec.id ?? spec.clientId ?? basename2(path2, ".json")
21572
+ );
21573
+ if (!singleton[kind]) spec[kind === "Client" ? "clientId" : "id"] = id;
21574
+ return { $schema: schemaUrl(kind, 1), metadata: { generation: 1 }, spec };
21575
+ }
21576
+ async function folderFiles(path2) {
21577
+ const files = [];
21578
+ for (const entry of await readdir(path2, { withFileTypes: true })) {
21579
+ if (entry.name.startsWith(".")) continue;
21580
+ const file = join4(path2, entry.name);
21581
+ if (entry.isSymbolicLink())
21582
+ throw new Error(`Config folder contains a symbolic link: ${file}`);
21583
+ if (entry.isDirectory()) files.push(...await folderFiles(file));
21584
+ else if (entry.isFile()) files.push(file);
21585
+ }
21586
+ return files.sort();
21587
+ }
21588
+ async function upgradeFolder(source, parsed, context) {
21589
+ const outputs = /* @__PURE__ */ new Map();
21590
+ let changed = 0;
21591
+ for (const file of await folderFiles(source)) {
21592
+ if (extname(file) !== ".json" || relative2(source, file).split(sep2).includes("images"))
21593
+ continue;
21594
+ const input = parse3(await readFile4(file, "utf8"), file);
21595
+ const kind = inferredKind(relative2(source, file));
21596
+ if (!kind && !isConfigDocument(input)) continue;
21597
+ const candidate = isConfigDocument(input) ? input : wrapBare(kind, input, file);
21598
+ const output = upgradeJson(candidate, file, context);
21599
+ if (kind && normalizeDocument(output).kind !== kind)
21600
+ throw new Error(`${file}: expected ${kind} configuration`);
21601
+ if (report(input, output, relative2(source, file), parsed, context))
21602
+ changed++;
21603
+ outputs.set(
21604
+ relative2(source, file),
21605
+ `${JSON.stringify(output, null, 2)}
21606
+ `
21607
+ );
21608
+ }
21609
+ if (!outputs.size)
21610
+ throw new Error(`No configuration documents found in ${source}`);
21611
+ context.stdout.write(
21612
+ `${outputs.size - changed} current, ${changed} require upgrading
21613
+ `
21614
+ );
21615
+ if (parsed.flags.check === true) return changed ? 1 : 0;
21616
+ if (parsed.flags["dry-run"] === true) return 0;
21617
+ const destination = resolve3(
21618
+ context.cwd,
21619
+ readStringFlag(parsed.flags, "output") ?? `${source}.upgraded`
21620
+ );
21621
+ const overlap = (parent, child) => {
21622
+ const path2 = relative2(parent, child);
21623
+ return path2 === "" || !path2.startsWith(`..${sep2}`) && path2 !== "..";
21624
+ };
21625
+ if (overlap(source, destination) || overlap(destination, source))
21626
+ throw new Error(
21627
+ "Folder output must be separate from the source directory"
21628
+ );
21629
+ await lstat(destination).then(
21630
+ () => {
21631
+ throw new Error(`Output already exists: ${destination}`);
21632
+ },
21633
+ (error) => {
21634
+ if (error.code !== "ENOENT") throw error;
21635
+ }
21636
+ );
21637
+ await mkdir3(dirname3(destination), { recursive: true });
21638
+ const staging = await mkdtemp(
21639
+ join4(dirname3(destination), ".eudiplo-upgrade-")
21640
+ );
21641
+ try {
20182
21642
  await cp(source, staging, {
20183
21643
  recursive: true,
20184
21644
  filter: (path2) => path2 === source || !relative2(source, path2).split(sep2).some((part) => part.startsWith("."))
@@ -20592,17 +22052,17 @@ function storageEnv(storage) {
20592
22052
  if (storage === "local") {
20593
22053
  return ["STORAGE_DRIVER=local", "LOCAL_STORAGE_DIR=/app/uploads"];
20594
22054
  }
20595
- const minioPassword = randomSecret();
22055
+ const rustfsPassword = randomSecret();
20596
22056
  return [
20597
22057
  "STORAGE_DRIVER=s3",
20598
- "S3_ENDPOINT=http://minio:9000",
22058
+ "S3_ENDPOINT=http://rustfs:9000",
20599
22059
  "S3_REGION=us-east-1",
20600
- "S3_ACCESS_KEY_ID=minioadmin",
20601
- `S3_SECRET_ACCESS_KEY=${minioPassword}`,
22060
+ "S3_ACCESS_KEY_ID=rustfsadmin",
22061
+ `S3_SECRET_ACCESS_KEY=${rustfsPassword}`,
20602
22062
  "S3_BUCKET=uploads",
20603
22063
  "S3_FORCE_PATH_STYLE=true",
20604
- "MINIO_ROOT_USER=minioadmin",
20605
- `MINIO_ROOT_PASSWORD=${minioPassword}`
22064
+ "RUSTFS_ACCESS_KEY=rustfsadmin",
22065
+ `RUSTFS_SECRET_KEY=${rustfsPassword}`
20606
22066
  ];
20607
22067
  }
20608
22068
  function kmsEnv(kms) {
@@ -20755,10 +22215,10 @@ function formatTextReport(scope, rootPath, results) {
20755
22215
  function formatTenantBlock(result) {
20756
22216
  const lines = [`${result.valid ? "PASS" : "FAIL"} ${result.id}`];
20757
22217
  if (result.valid) {
20758
- for (const [resourceType, count] of Object.entries(
22218
+ for (const [resourceType, count2] of Object.entries(
20759
22219
  result.resourceCounts
20760
22220
  )) {
20761
- lines.push(` ${count} ${pluralize(resourceType, count)}`);
22221
+ lines.push(` ${count2} ${pluralize(resourceType, count2)}`);
20762
22222
  }
20763
22223
  } else {
20764
22224
  lines.push(...formatTenantErrors(result.errors));
@@ -20804,8 +22264,8 @@ function groupErrorsByFile(errors) {
20804
22264
  }
20805
22265
  return Array.from(grouped.entries());
20806
22266
  }
20807
- function pluralize(label, count) {
20808
- return count === 1 ? label : `${label}s`;
22267
+ function pluralize(label, count2) {
22268
+ return count2 === 1 ? label : `${label}s`;
20809
22269
  }
20810
22270
 
20811
22271
  // src/commands/config/validate/validator.ts
@@ -21650,6 +23110,68 @@ function assertKnownService(service, known) {
21650
23110
  }
21651
23111
  }
21652
23112
 
23113
+ // src/services/container-state.ts
23114
+ function parseContainerStates(stdout) {
23115
+ const trimmed = stdout.trim();
23116
+ if (!trimmed) {
23117
+ return [];
23118
+ }
23119
+ const entries = trimmed.startsWith("[") ? asArray(JSON.parse(trimmed)) : trimmed.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("{")).map((line) => JSON.parse(line));
23120
+ return entries.map((entry) => {
23121
+ const record = entry;
23122
+ const names = record.Names;
23123
+ return {
23124
+ name: Array.isArray(names) ? String(names[0] ?? "") : String(names ?? record.Name ?? ""),
23125
+ state: String(record.State ?? ""),
23126
+ status: String(record.Status ?? "")
23127
+ };
23128
+ });
23129
+ }
23130
+ function summarizeContainerStates(containers, projectName) {
23131
+ const name = "service containers";
23132
+ if (containers.length === 0) {
23133
+ return {
23134
+ name,
23135
+ status: "warn",
23136
+ message: `No containers found for project ${projectName}. Start it with eudiplo up.`
23137
+ };
23138
+ }
23139
+ const unhealthy = containers.filter(
23140
+ (container) => container.status.toLowerCase().includes("unhealthy")
23141
+ );
23142
+ if (unhealthy.length > 0) {
23143
+ return {
23144
+ name,
23145
+ status: "fail",
23146
+ message: `Unhealthy: ${describe(unhealthy)}`
23147
+ };
23148
+ }
23149
+ const notRunning = containers.filter(
23150
+ (container) => container.state.toLowerCase() !== "running"
23151
+ );
23152
+ if (notRunning.length > 0) {
23153
+ return {
23154
+ name,
23155
+ status: "warn",
23156
+ message: `${containers.length - notRunning.length}/${containers.length} running. Not running: ${describe(notRunning)}`
23157
+ };
23158
+ }
23159
+ const starting = containers.filter(
23160
+ (container) => container.status.toLowerCase().includes("starting")
23161
+ ).length;
23162
+ return {
23163
+ name,
23164
+ status: "pass",
23165
+ message: `${containers.length} container(s) running${starting > 0 ? `, ${starting} still starting` : ""}.`
23166
+ };
23167
+ }
23168
+ function asArray(value) {
23169
+ return Array.isArray(value) ? value : [];
23170
+ }
23171
+ function describe(containers) {
23172
+ return containers.map((container) => `${container.name} (${container.status})`).join(", ");
23173
+ }
23174
+
21653
23175
  // src/services/deployment-drivers.ts
21654
23176
  var drivers = {
21655
23177
  compose: {
@@ -21663,6 +23185,12 @@ var drivers = {
21663
23185
  status: runtime ? "pass" : "warn",
21664
23186
  message: runtime ? `${runtime.name} found at ${runtime.command}` : "Docker or Podman was not found in a supported install location."
21665
23187
  });
23188
+ if (runtime) {
23189
+ checks.push(await composeProviderCheck(runtime, context));
23190
+ checks.push(
23191
+ await composeServicesCheck(runtime, instance, context)
23192
+ );
23193
+ }
21666
23194
  for (const composeFile of getComposeFiles(instance)) {
21667
23195
  let found = true;
21668
23196
  try {
@@ -22208,17 +23736,118 @@ async function captureCompose(args, { instance, context }) {
22208
23736
  "Docker or Podman was not found in a supported install location."
22209
23737
  );
22210
23738
  }
22211
- const composeArgs = [
22212
- ...buildComposeBaseArgs(
22213
- instance,
22214
- projectDirectory,
22215
- getComposeFiles(instance)
22216
- ),
22217
- ...args
22218
- ];
23739
+ const composeArgs = [
23740
+ ...buildComposeBaseArgs(
23741
+ instance,
23742
+ projectDirectory,
23743
+ getComposeFiles(instance)
23744
+ ),
23745
+ ...args
23746
+ ];
23747
+ return new Promise((resolveProcess) => {
23748
+ const child = spawn(composeRuntime.command, composeArgs, {
23749
+ cwd: projectDirectory,
23750
+ env: context.env,
23751
+ stdio: ["ignore", "pipe", "pipe"]
23752
+ });
23753
+ let stdout = "";
23754
+ let stderr = "";
23755
+ child.stdout?.on("data", (chunk) => {
23756
+ stdout += String(chunk);
23757
+ });
23758
+ child.stderr?.on("data", (chunk) => {
23759
+ stderr += String(chunk);
23760
+ });
23761
+ child.on(
23762
+ "error",
23763
+ (error) => resolveProcess({ code: 1, stdout, stderr: error.message })
23764
+ );
23765
+ child.on(
23766
+ "close",
23767
+ (code) => resolveProcess({ code: code ?? 1, stdout, stderr })
23768
+ );
23769
+ });
23770
+ }
23771
+ async function resolveComposeService(options) {
23772
+ const service = readServiceFlag(options.flags);
23773
+ if (service === void 0) {
23774
+ return void 0;
23775
+ }
23776
+ assertServiceName(service);
23777
+ const listed = await captureCompose(
23778
+ buildComposeListServicesArgs(),
23779
+ options
23780
+ );
23781
+ if (listed.code !== 0) {
23782
+ throw new Error(
23783
+ `Could not list Compose services: ${listed.stderr.trim() || `exit code ${listed.code}`}`
23784
+ );
23785
+ }
23786
+ assertKnownService(service, parseServiceList(listed.stdout));
23787
+ return service;
23788
+ }
23789
+ async function composeProviderCheck(runtime, context) {
23790
+ const version = await captureRuntime(
23791
+ runtime.command,
23792
+ ["compose", "version"],
23793
+ context
23794
+ );
23795
+ if (version.code !== 0) {
23796
+ return {
23797
+ name: "compose provider",
23798
+ status: "fail",
23799
+ message: `${runtime.name} compose is not usable: ${firstLine(version.stderr) || `exit code ${version.code}`}`
23800
+ };
23801
+ }
23802
+ return {
23803
+ name: "compose provider",
23804
+ status: "pass",
23805
+ message: firstLine(version.stdout) || `${runtime.name} compose works`
23806
+ };
23807
+ }
23808
+ async function composeServicesCheck(runtime, instance, context) {
23809
+ if (!instance.projectName) {
23810
+ return {
23811
+ name: "service containers",
23812
+ status: "skip",
23813
+ message: "This instance has no Compose project name, so its containers cannot be identified."
23814
+ };
23815
+ }
23816
+ const listed = await captureRuntime(
23817
+ runtime.command,
23818
+ [
23819
+ "ps",
23820
+ "--all",
23821
+ "--filter",
23822
+ `label=com.docker.compose.project=${instance.projectName}`,
23823
+ "--format",
23824
+ "json"
23825
+ ],
23826
+ context
23827
+ );
23828
+ if (listed.code !== 0) {
23829
+ return {
23830
+ name: "service containers",
23831
+ status: "warn",
23832
+ message: `Containers could not be listed: ${firstLine(listed.stderr) || `exit code ${listed.code}`}`
23833
+ };
23834
+ }
23835
+ try {
23836
+ return summarizeContainerStates(
23837
+ parseContainerStates(listed.stdout),
23838
+ instance.projectName
23839
+ );
23840
+ } catch {
23841
+ return {
23842
+ name: "service containers",
23843
+ status: "warn",
23844
+ message: `The container list returned by ${runtime.name} could not be read.`
23845
+ };
23846
+ }
23847
+ }
23848
+ function captureRuntime(command, args, context) {
22219
23849
  return new Promise((resolveProcess) => {
22220
- const child = spawn(composeRuntime.command, composeArgs, {
22221
- cwd: projectDirectory,
23850
+ const child = spawn(command, args, {
22222
23851
  env: context.env,
22223
23852
  stdio: ["ignore", "pipe", "pipe"]
22224
23853
  });
@@ -22240,24 +23869,6 @@ async function captureCompose(args, { instance, context }) {
22240
23869
  );
22241
23870
  });
22242
23871
  }
22243
- async function resolveComposeService(options) {
22244
- const service = readServiceFlag(options.flags);
22245
- if (service === void 0) {
22246
- return void 0;
22247
- }
22248
- assertServiceName(service);
22249
- const listed = await captureCompose(
22250
- buildComposeListServicesArgs(),
22251
- options
22252
- );
22253
- if (listed.code !== 0) {
22254
- throw new Error(
22255
- `Could not list Compose services: ${listed.stderr.trim() || `exit code ${listed.code}`}`
22256
- );
22257
- }
22258
- assertKnownService(service, parseServiceList(listed.stdout));
22259
- return service;
22260
- }
22261
23872
  async function resolveComposeRuntime(env) {
22262
23873
  const preferredRuntime = parsePreferredRuntime(
22263
23874
  env.EUDIPLO_CONTAINER_RUNTIME
@@ -22478,126 +24089,607 @@ async function runDriverCommand(config, command, parsed, context) {
22478
24089
  context.stdout.write(`${command} ${instanceName} (${clientMode})
22479
24090
  `);
22480
24091
  }
22481
- return handler({
22482
- instanceName,
22483
- instance,
22484
- args: parsed.positionals,
22485
- flags: parsed.flags,
22486
- context
22487
- });
24092
+ return handler({
24093
+ instanceName,
24094
+ instance,
24095
+ args: parsed.positionals,
24096
+ flags: parsed.flags,
24097
+ context
24098
+ });
24099
+ }
24100
+
24101
+ // src/commands/deployment/index.ts
24102
+ var descriptions = {
24103
+ up: "Start the selected Compose deployment",
24104
+ down: "Stop the selected Compose deployment",
24105
+ logs: "Stream logs for the selected deployment",
24106
+ ps: "List the running containers or pods for the selected deployment",
24107
+ restart: "Restart the workloads of the selected deployment",
24108
+ pull: "Download the configured images without changing configuration"
24109
+ };
24110
+ var serviceScoped = /* @__PURE__ */ new Set([
24111
+ "logs",
24112
+ "restart",
24113
+ "pull"
24114
+ ]);
24115
+ function createDeploymentCommands(context, setExitCode) {
24116
+ return ["up", "down", "logs", "ps", "restart", "pull"].map(
24117
+ (name) => {
24118
+ const command = new Command(name).description(descriptions[name]).argument(
24119
+ "[args...]",
24120
+ "additional arguments passed to the deployment runtime"
24121
+ ).option("--instance <name>", "select a configured instance");
24122
+ if (serviceScoped.has(name)) {
24123
+ command.option(
24124
+ "--service <name>",
24125
+ "select a single service (Compose) or configured workload (Kubernetes)"
24126
+ );
24127
+ }
24128
+ if (name === "logs") {
24129
+ command.option("--follow", "stream new output as it arrives").option("--tail <lines>", "number of recent lines to show").option(
24130
+ "--since <time>",
24131
+ "show logs since a duration (10m, 2h) or timestamp (Compose instances)"
24132
+ );
24133
+ }
24134
+ if (name === "restart") {
24135
+ command.option(
24136
+ "--no-wait",
24137
+ "return without waiting for the rollout to finish (Kubernetes instances)"
24138
+ );
24139
+ }
24140
+ return command.action(async (args, options) => {
24141
+ const { config } = await loadCliState(context);
24142
+ setExitCode(
24143
+ await runDriverCommand(
24144
+ config,
24145
+ name,
24146
+ parsedArgs(name, void 0, args, options),
24147
+ context
24148
+ )
24149
+ );
24150
+ });
24151
+ }
24152
+ );
24153
+ }
24154
+
24155
+ // package.json
24156
+ var package_default = {
24157
+ name: "@eudiplo/cli",
24158
+ version: "8.1.0-main.3c4fbcd",
24159
+ description: "Deployment-aware command line tools for EUDIPLO",
24160
+ private: false,
24161
+ type: "module",
24162
+ bin: {
24163
+ eudiplo: "dist/index.js"
24164
+ },
24165
+ files: [
24166
+ "dist",
24167
+ "templates",
24168
+ "README.md"
24169
+ ],
24170
+ scripts: {
24171
+ "assets:sync": "node scripts/sync-cli-assets.cjs",
24172
+ "assets:check": "node scripts/check-cli-assets.cjs",
24173
+ build: "pnpm assets:sync && pnpm assets:check && tsc -p tsconfig.json",
24174
+ "build:publish": "pnpm build && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22.12 --alias:jsonc-parser=jsonc-parser/lib/esm/main.js --outfile=dist/index.js",
24175
+ "build:sea:bundle": "pnpm assets:sync && pnpm assets:check && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22.12 --alias:jsonc-parser=jsonc-parser/lib/esm/main.js --outfile=dist-sea/index.js",
24176
+ "build:sea": "node scripts/build-cli-sea.mjs",
24177
+ dev: "tsx src/index.ts",
24178
+ test: "vitest run --root ../.. --project cli",
24179
+ lint: "biome lint .",
24180
+ prepublishOnly: "pnpm build:publish"
24181
+ },
24182
+ keywords: [
24183
+ "eudiplo",
24184
+ "cli",
24185
+ "eudi",
24186
+ "wallet",
24187
+ "oid4vci",
24188
+ "oid4vp"
24189
+ ],
24190
+ repository: {
24191
+ type: "git",
24192
+ url: "git+https://github.com/openwallet-foundation/eudiplo.git",
24193
+ directory: "apps/cli"
24194
+ },
24195
+ homepage: "https://github.com/openwallet-foundation/eudiplo/tree/main/apps/cli#readme",
24196
+ bugs: {
24197
+ url: "https://github.com/openwallet-foundation/eudiplo/issues"
24198
+ },
24199
+ author: "OpenWallet Foundation",
24200
+ license: "Apache-2.0",
24201
+ engines: {
24202
+ node: ">=22.12.0"
24203
+ },
24204
+ publishConfig: {
24205
+ access: "public"
24206
+ },
24207
+ dependencies: {
24208
+ "@eudiplo/config-format": "workspace:*",
24209
+ ajv: "^8.20.0",
24210
+ "ajv-formats": "^3.0.1",
24211
+ commander: "^15.0.0",
24212
+ fflate: "^0.8.3",
24213
+ "jsonc-parser": "^3.3.1"
24214
+ },
24215
+ devDependencies: {
24216
+ "@biomejs/biome": "2.5.14",
24217
+ "@types/node": "^26.4.0",
24218
+ esbuild: "^0.28.2",
24219
+ tsx: "^4.23.13",
24220
+ typescript: "^7.0.2",
24221
+ vitest: "5.0.2"
24222
+ }
24223
+ };
24224
+
24225
+ // src/services/api-auth.ts
24226
+ async function requestAccessToken(baseUrl, context) {
24227
+ const clientId = context.env.EUDIPLO_CLIENT_ID;
24228
+ const clientSecret = context.env.EUDIPLO_CLIENT_SECRET;
24229
+ if (!clientId || !clientSecret) {
24230
+ return {
24231
+ kind: "unavailable",
24232
+ reason: "EUDIPLO_CLIENT_ID and EUDIPLO_CLIENT_SECRET are not set"
24233
+ };
24234
+ }
24235
+ const tokenUrl = new URL("api/oauth2/token", withTrailingSlash(baseUrl));
24236
+ try {
24237
+ const response = await context.fetch(tokenUrl, {
24238
+ method: "POST",
24239
+ headers: { "content-type": "application/x-www-form-urlencoded" },
24240
+ body: new URLSearchParams({
24241
+ grant_type: "client_credentials",
24242
+ client_id: clientId,
24243
+ client_secret: clientSecret
24244
+ }).toString()
24245
+ });
24246
+ if (!response.ok) {
24247
+ return {
24248
+ kind: "error",
24249
+ reason: `the token endpoint returned HTTP ${response.status}`
24250
+ };
24251
+ }
24252
+ const payload = await response.json();
24253
+ const accessToken = typeof payload === "object" && payload !== null && "access_token" in payload ? String(payload.access_token) : "";
24254
+ if (!accessToken) {
24255
+ return {
24256
+ kind: "error",
24257
+ reason: "the token response did not contain an access token"
24258
+ };
24259
+ }
24260
+ return { kind: "token", accessToken };
24261
+ } catch (error) {
24262
+ return {
24263
+ kind: "error",
24264
+ reason: `the token endpoint could not be reached: ${error instanceof Error ? error.message : String(error)}`
24265
+ };
24266
+ }
24267
+ }
24268
+ async function fetchAuthenticated(url, accessToken, context) {
24269
+ try {
24270
+ const response = await context.fetch(url, {
24271
+ method: "GET",
24272
+ headers: { authorization: `Bearer ${accessToken}` }
24273
+ });
24274
+ if (!response.ok) {
24275
+ return {
24276
+ ok: false,
24277
+ status: response.status,
24278
+ reason: `${url.pathname} returned HTTP ${response.status}`
24279
+ };
24280
+ }
24281
+ return { ok: true, body: await response.json() };
24282
+ } catch (error) {
24283
+ return {
24284
+ ok: false,
24285
+ reason: `${url.pathname} could not be reached: ${error instanceof Error ? error.message : String(error)}`
24286
+ };
24287
+ }
24288
+ }
24289
+ function withTrailingSlash(url) {
24290
+ return url.href.endsWith("/") ? url : new URL(`${url.href}/`);
24291
+ }
24292
+
24293
+ // src/services/backend-checks.ts
24294
+ function compareVersions(cliVersion, backendVersion) {
24295
+ const name = "version compatibility";
24296
+ const cli = parse4(cliVersion);
24297
+ const backend = parse4(backendVersion);
24298
+ if (!cli || !backend) {
24299
+ return {
24300
+ name,
24301
+ status: "warn",
24302
+ message: `CLI ${cliVersion} and backend ${backendVersion} cannot be compared as versions.`
24303
+ };
24304
+ }
24305
+ if (cli.major !== backend.major) {
24306
+ return {
24307
+ name,
24308
+ status: "fail",
24309
+ message: `CLI ${cliVersion} and backend ${backendVersion} are different major versions. Install a matching CLI with npm install -g @eudiplo/cli@${backend.major}.`
24310
+ };
24311
+ }
24312
+ if (cli.minor !== backend.minor) {
24313
+ return {
24314
+ name,
24315
+ status: "warn",
24316
+ message: `CLI ${cliVersion} and backend ${backendVersion} differ in minor version; some commands may not be supported.`
24317
+ };
24318
+ }
24319
+ return {
24320
+ name,
24321
+ status: "pass",
24322
+ message: `CLI ${cliVersion} matches backend ${backendVersion}.`
24323
+ };
24324
+ }
24325
+ function readBackendVersion(body) {
24326
+ if (typeof body === "object" && body !== null && "version" in body) {
24327
+ const version = body.version;
24328
+ return typeof version === "string" && version ? version : void 0;
24329
+ }
24330
+ return void 0;
24331
+ }
24332
+ function readProviderHealth(body) {
24333
+ if (!Array.isArray(body)) {
24334
+ throw new Error("the provider health response was not a list");
24335
+ }
24336
+ return body.map((entry) => {
24337
+ const record = entry;
24338
+ return {
24339
+ providerId: String(record.providerId ?? "unknown"),
24340
+ type: String(record.type ?? "unknown"),
24341
+ ok: record.ok === true,
24342
+ latencyMs: typeof record.latencyMs === "number" ? record.latencyMs : void 0,
24343
+ error: typeof record.error === "string" ? record.error : void 0
24344
+ };
24345
+ });
24346
+ }
24347
+ function summarizeProviderHealth(providers) {
24348
+ const name = "KMS providers";
24349
+ if (providers.length === 0) {
24350
+ return {
24351
+ name,
24352
+ status: "warn",
24353
+ message: "No KMS providers are registered for this tenant."
24354
+ };
24355
+ }
24356
+ const failing = providers.filter((provider) => !provider.ok);
24357
+ if (failing.length > 0) {
24358
+ return {
24359
+ name,
24360
+ status: "fail",
24361
+ message: `${failing.length}/${providers.length} provider(s) unhealthy: ${failing.map(
24362
+ (provider) => `${provider.providerId} (${provider.type})${provider.error ? ", health check reported an error" : ""}`
24363
+ ).join(", ")}`
24364
+ };
24365
+ }
24366
+ return {
24367
+ name,
24368
+ status: "pass",
24369
+ message: `${providers.length} provider(s) healthy: ${providers.map(
24370
+ (provider) => `${provider.providerId} (${provider.type}${provider.latencyMs === void 0 ? "" : `, ${provider.latencyMs}ms`})`
24371
+ ).join(", ")}`
24372
+ };
24373
+ }
24374
+ function parse4(version) {
24375
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
24376
+ if (!match) {
24377
+ return void 0;
24378
+ }
24379
+ return {
24380
+ major: Number(match[1]),
24381
+ minor: Number(match[2]),
24382
+ patch: Number(match[3])
24383
+ };
24384
+ }
24385
+
24386
+ // src/services/tls-expiry.ts
24387
+ import { connect } from "node:tls";
24388
+ var defaultExpiryWarningDays = 30;
24389
+ var dayInMs = 24 * 60 * 60 * 1e3;
24390
+ function evaluateCertificate(certificate, now, warningDays = defaultExpiryWarningDays) {
24391
+ const remainingDays = Math.floor(
24392
+ (certificate.validTo.getTime() - now.getTime()) / dayInMs
24393
+ );
24394
+ const expiresOn = certificate.validTo.toISOString().slice(0, 10);
24395
+ if (remainingDays < 0) {
24396
+ return {
24397
+ name: "TLS certificate",
24398
+ status: "fail",
24399
+ message: `The certificate issued by ${certificate.issuer} expired on ${expiresOn}.`
24400
+ };
24401
+ }
24402
+ if (remainingDays <= warningDays) {
24403
+ return {
24404
+ name: "TLS certificate",
24405
+ status: "warn",
24406
+ message: `The certificate expires on ${expiresOn}, in ${remainingDays} day(s). Renew it before then.`
24407
+ };
24408
+ }
24409
+ return {
24410
+ name: "TLS certificate",
24411
+ status: "pass",
24412
+ message: `Valid until ${expiresOn} (${remainingDays} day(s) left), issued by ${certificate.issuer}.`
24413
+ };
22488
24414
  }
22489
-
22490
- // src/commands/deployment/index.ts
22491
- var descriptions = {
22492
- up: "Start the selected Compose deployment",
22493
- down: "Stop the selected Compose deployment",
22494
- logs: "Stream logs for the selected deployment",
22495
- ps: "List the running containers or pods for the selected deployment",
22496
- restart: "Restart the workloads of the selected deployment",
22497
- pull: "Download the configured images without changing configuration"
22498
- };
22499
- var serviceScoped = /* @__PURE__ */ new Set([
22500
- "logs",
22501
- "restart",
22502
- "pull"
22503
- ]);
22504
- function createDeploymentCommands(context, setExitCode) {
22505
- return ["up", "down", "logs", "ps", "restart", "pull"].map(
22506
- (name) => {
22507
- const command = new Command(name).description(descriptions[name]).argument(
22508
- "[args...]",
22509
- "additional arguments passed to the deployment runtime"
22510
- ).option("--instance <name>", "select a configured instance");
22511
- if (serviceScoped.has(name)) {
22512
- command.option(
22513
- "--service <name>",
22514
- "select a single service (Compose) or configured workload (Kubernetes)"
22515
- );
22516
- }
22517
- if (name === "logs") {
22518
- command.option("--follow", "stream new output as it arrives").option("--tail <lines>", "number of recent lines to show").option(
22519
- "--since <time>",
22520
- "show logs since a duration (10m, 2h) or timestamp (Compose instances)"
22521
- );
22522
- }
22523
- if (name === "restart") {
22524
- command.option(
22525
- "--no-wait",
22526
- "return without waiting for the rollout to finish (Kubernetes instances)"
22527
- );
24415
+ var readPeerCertificate = (url) => new Promise((resolvePeer, rejectPeer) => {
24416
+ const socket = connect(
24417
+ {
24418
+ host: url.hostname,
24419
+ port: Number(url.port || 443),
24420
+ servername: url.hostname,
24421
+ rejectUnauthorized: true
24422
+ },
24423
+ () => {
24424
+ const certificate = socket.getPeerCertificate();
24425
+ socket.end();
24426
+ if (!certificate || !certificate.valid_to) {
24427
+ rejectPeer(new Error("no peer certificate was presented"));
24428
+ return;
22528
24429
  }
22529
- return command.action(async (args, options) => {
22530
- const { config } = await loadCliState(context);
22531
- setExitCode(
22532
- await runDriverCommand(
22533
- config,
22534
- name,
22535
- parsedArgs(name, void 0, args, options),
22536
- context
22537
- )
22538
- );
24430
+ resolvePeer({
24431
+ subject: String(certificate.subject?.CN ?? url.hostname),
24432
+ issuer: String(certificate.issuer?.CN ?? "unknown issuer"),
24433
+ validTo: new Date(certificate.valid_to)
22539
24434
  });
22540
24435
  }
22541
24436
  );
24437
+ socket.setTimeout(1e4, () => {
24438
+ socket.destroy();
24439
+ rejectPeer(new Error("the TLS handshake timed out"));
24440
+ });
24441
+ socket.once("error", (error) => {
24442
+ socket.destroy();
24443
+ rejectPeer(error);
24444
+ });
24445
+ });
24446
+ async function checkTlsCertificate(baseUrl, now, read = readPeerCertificate, warningDays = defaultExpiryWarningDays) {
24447
+ if (baseUrl.protocol !== "https:") {
24448
+ return {
24449
+ name: "TLS certificate",
24450
+ status: "skip",
24451
+ message: `${baseUrl.origin} does not use HTTPS, so there is no certificate to check.`
24452
+ };
24453
+ }
24454
+ try {
24455
+ return evaluateCertificate(await read(baseUrl), now, warningDays);
24456
+ } catch (error) {
24457
+ return {
24458
+ name: "TLS certificate",
24459
+ status: "fail",
24460
+ message: `The certificate of ${baseUrl.origin} could not be verified: ${error instanceof Error ? error.message : String(error)}`
24461
+ };
24462
+ }
22542
24463
  }
22543
24464
 
22544
24465
  // src/services/diagnostics.ts
22545
- async function runDoctor(instance, context, driverDiagnostics) {
22546
- const checks = [];
22547
- const baseUrl = parseUrl(instance.url);
22548
- if (!baseUrl) {
22549
- return [
22550
- {
24466
+ var publicUrlCheck = {
24467
+ id: "public-url",
24468
+ async run({ instance, baseUrl }) {
24469
+ if (!baseUrl) {
24470
+ return {
22551
24471
  name: "public URL",
22552
24472
  status: "fail",
22553
24473
  message: `${instance.url} is not a valid absolute URL`
22554
- }
22555
- ];
24474
+ };
24475
+ }
24476
+ return {
24477
+ name: "public URL",
24478
+ status: baseUrl.protocol === "https:" ? "pass" : "warn",
24479
+ message: baseUrl.protocol === "https:" ? `${baseUrl.href} uses HTTPS` : `${baseUrl.href} does not use HTTPS`
24480
+ };
22556
24481
  }
22557
- checks.push({
22558
- name: "public URL",
22559
- status: baseUrl.protocol === "https:" ? "pass" : "warn",
22560
- message: baseUrl.protocol === "https:" ? `${baseUrl.href} uses HTTPS` : `${baseUrl.href} does not use HTTPS`
22561
- });
22562
- checks.push(
22563
- await checkEndpoint("API reachability", baseUrl, "/api/docs", context)
22564
- );
22565
- checks.push(
22566
- await checkEndpoint("health endpoint", baseUrl, "/health", context)
22567
- );
22568
- checks.push(checkAuthentication(context));
22569
- if (instance.clientUrl) {
24482
+ };
24483
+ var apiReachabilityCheck = {
24484
+ id: "api-reachability",
24485
+ run: ({ baseUrl, context }) => requireBaseUrl(
24486
+ "API reachability",
24487
+ baseUrl,
24488
+ (url) => checkEndpoint("API reachability", url, "/api/docs", context)
24489
+ )
24490
+ };
24491
+ var healthEndpointCheck = {
24492
+ id: "health-endpoint",
24493
+ run: ({ baseUrl, context }) => requireBaseUrl(
24494
+ "health endpoint",
24495
+ baseUrl,
24496
+ (url) => checkEndpoint("health endpoint", url, "/health", context)
24497
+ )
24498
+ };
24499
+ var tlsCertificateCheck = {
24500
+ id: "tls-certificate",
24501
+ run: ({ baseUrl, now, readCertificate }) => requireBaseUrl(
24502
+ "TLS certificate",
24503
+ baseUrl,
24504
+ (url) => checkTlsCertificate(url, now, readCertificate)
24505
+ )
24506
+ };
24507
+ var authenticationCheck = {
24508
+ id: "authentication",
24509
+ async run({ context }) {
24510
+ const configured = Boolean(context.env.EUDIPLO_CLIENT_ID) && Boolean(context.env.EUDIPLO_CLIENT_SECRET);
24511
+ return {
24512
+ name: "authentication configuration",
24513
+ status: configured ? "pass" : "warn",
24514
+ message: configured ? "Client credentials are available in environment variables." : "Set EUDIPLO_CLIENT_ID and EUDIPLO_CLIENT_SECRET when commands need authenticated API access."
24515
+ };
24516
+ }
24517
+ };
24518
+ var clientConnectivityCheck = {
24519
+ id: "client-connectivity",
24520
+ async run({ instance, context }) {
24521
+ if (!instance.clientUrl) {
24522
+ return {
24523
+ name: "client connectivity",
24524
+ status: "skip",
24525
+ message: "No client URL configured for this instance."
24526
+ };
24527
+ }
22570
24528
  const clientUrl = parseUrl(instance.clientUrl);
22571
- checks.push(
22572
- clientUrl ? await checkEndpoint(
22573
- "client connectivity",
22574
- clientUrl,
22575
- "/",
22576
- context
22577
- ) : {
24529
+ if (!clientUrl) {
24530
+ return {
22578
24531
  name: "client connectivity",
22579
24532
  status: "fail",
22580
24533
  message: `${instance.clientUrl} is not a valid absolute URL`
24534
+ };
24535
+ }
24536
+ return checkEndpoint("client connectivity", clientUrl, "/", context);
24537
+ }
24538
+ };
24539
+ var versionCompatibilityCheck = {
24540
+ id: "version-compatibility",
24541
+ run: ({ baseUrl, context, accessToken }) => requireBaseUrl("version compatibility", baseUrl, async (url) => {
24542
+ const token = await accessToken();
24543
+ if (token.kind !== "token") {
24544
+ return authUnavailable("version compatibility", token);
24545
+ }
24546
+ const response = await fetchAuthenticated(
24547
+ new URL("version", withTrailingSlash(url)),
24548
+ token.accessToken,
24549
+ context
24550
+ );
24551
+ if (!response.ok) {
24552
+ if (response.status === 404) {
24553
+ return {
24554
+ name: "version compatibility",
24555
+ status: "skip",
24556
+ message: "This backend does not expose /version, so compatibility cannot be checked."
24557
+ };
22581
24558
  }
24559
+ return {
24560
+ name: "version compatibility",
24561
+ status: "fail",
24562
+ message: `The backend version could not be read: ${response.reason}`
24563
+ };
24564
+ }
24565
+ const backendVersion = readBackendVersion(response.body);
24566
+ if (!backendVersion) {
24567
+ return {
24568
+ name: "version compatibility",
24569
+ status: "warn",
24570
+ message: "The backend did not report a version."
24571
+ };
24572
+ }
24573
+ return compareVersions(package_default.version, backendVersion);
24574
+ })
24575
+ };
24576
+ var kmsHealthCheck = {
24577
+ id: "kms-health",
24578
+ run: ({ baseUrl, context, accessToken }) => requireBaseUrl("KMS providers", baseUrl, async (url) => {
24579
+ const token = await accessToken();
24580
+ if (token.kind !== "token") {
24581
+ return authUnavailable("KMS providers", token);
24582
+ }
24583
+ const response = await fetchAuthenticated(
24584
+ new URL("key-chain/providers/health", withTrailingSlash(url)),
24585
+ token.accessToken,
24586
+ context
22582
24587
  );
22583
- } else {
22584
- checks.push({
22585
- name: "client connectivity",
22586
- status: "warn",
22587
- message: "No client URL configured for this instance."
22588
- });
24588
+ if (!response.ok) {
24589
+ if (response.status === 404) {
24590
+ return {
24591
+ name: "KMS providers",
24592
+ status: "skip",
24593
+ message: "This backend does not expose the KMS provider health endpoint."
24594
+ };
24595
+ }
24596
+ return {
24597
+ name: "KMS providers",
24598
+ status: "fail",
24599
+ message: `Provider health could not be read: ${response.reason}`
24600
+ };
24601
+ }
24602
+ try {
24603
+ return summarizeProviderHealth(
24604
+ readProviderHealth(response.body)
24605
+ );
24606
+ } catch {
24607
+ return {
24608
+ name: "KMS providers",
24609
+ status: "warn",
24610
+ message: "The provider health response could not be read."
24611
+ };
24612
+ }
24613
+ })
24614
+ };
24615
+ function authUnavailable(name, token) {
24616
+ return {
24617
+ name,
24618
+ status: token.kind === "unavailable" ? "skip" : "fail",
24619
+ message: token.kind === "unavailable" ? `Needs authentication: ${token.reason}.` : `Could not authenticate: ${token.reason}.`
24620
+ };
24621
+ }
24622
+ var coreChecks = [
24623
+ publicUrlCheck,
24624
+ apiReachabilityCheck,
24625
+ healthEndpointCheck,
24626
+ tlsCertificateCheck,
24627
+ authenticationCheck,
24628
+ versionCompatibilityCheck,
24629
+ kmsHealthCheck,
24630
+ clientConnectivityCheck
24631
+ ];
24632
+ async function runDoctor(instance, context, driverDiagnostics, options = {}) {
24633
+ const baseUrl = parseUrl(instance.url);
24634
+ let token;
24635
+ const checkContext = {
24636
+ instance,
24637
+ context,
24638
+ baseUrl,
24639
+ now: options.now ?? /* @__PURE__ */ new Date(),
24640
+ readCertificate: options.readCertificate ?? context.readCertificate ?? readPeerCertificate,
24641
+ accessToken: () => {
24642
+ if (!baseUrl) {
24643
+ return Promise.resolve({
24644
+ kind: "unavailable",
24645
+ reason: "the instance URL could not be parsed"
24646
+ });
24647
+ }
24648
+ token ??= requestAccessToken(baseUrl, context);
24649
+ return token;
24650
+ }
24651
+ };
24652
+ const checks = [];
24653
+ for (const check of coreChecks) {
24654
+ checks.push(await check.run(checkContext));
22589
24655
  }
22590
24656
  checks.push(...driverDiagnostics);
22591
24657
  return checks;
22592
24658
  }
22593
- function hasFailedChecks(checks) {
22594
- return checks.some((check) => check.status === "fail");
24659
+ function summarizeChecks(checks) {
24660
+ return {
24661
+ pass: count(checks, "pass"),
24662
+ warn: count(checks, "warn"),
24663
+ fail: count(checks, "fail"),
24664
+ skip: count(checks, "skip")
24665
+ };
24666
+ }
24667
+ function hasFailedChecks(checks, strict = false) {
24668
+ return checks.some(
24669
+ (check) => check.status === "fail" || strict && check.status === "warn"
24670
+ );
22595
24671
  }
22596
24672
  function formatChecks(checks) {
22597
24673
  return checks.map(
22598
24674
  (check) => `${formatStatus(check.status)} ${check.name}: ${check.message}`
22599
24675
  ).join("\n");
22600
24676
  }
24677
+ function formatSummary(summary) {
24678
+ return `${summary.pass} passed, ${summary.warn} warning(s), ${summary.fail} failure(s), ${summary.skip} skipped`;
24679
+ }
24680
+ function count(checks, status) {
24681
+ return checks.filter((check) => check.status === status).length;
24682
+ }
24683
+ async function requireBaseUrl(name, baseUrl, run) {
24684
+ if (!baseUrl) {
24685
+ return {
24686
+ name,
24687
+ status: "skip",
24688
+ message: "The instance URL could not be parsed."
24689
+ };
24690
+ }
24691
+ return run(baseUrl);
24692
+ }
22601
24693
  async function checkEndpoint(name, baseUrl, path2, context) {
22602
24694
  const url = new URL(path2, baseUrl);
22603
24695
  try {
@@ -22622,22 +24714,6 @@ async function checkEndpoint(name, baseUrl, path2, context) {
22622
24714
  };
22623
24715
  }
22624
24716
  }
22625
- function checkAuthentication(context) {
22626
- const hasClientId = Boolean(context.env.EUDIPLO_CLIENT_ID);
22627
- const hasClientSecret = Boolean(context.env.EUDIPLO_CLIENT_SECRET);
22628
- if (hasClientId && hasClientSecret) {
22629
- return {
22630
- name: "authentication configuration",
22631
- status: "pass",
22632
- message: "Client credentials are available in environment variables."
22633
- };
22634
- }
22635
- return {
22636
- name: "authentication configuration",
22637
- status: "warn",
22638
- message: "Set EUDIPLO_CLIENT_ID and EUDIPLO_CLIENT_SECRET when commands need authenticated API access."
22639
- };
22640
- }
22641
24717
  function parseUrl(value) {
22642
24718
  try {
22643
24719
  const url = new URL(value);
@@ -22653,28 +24729,54 @@ function formatStatus(status) {
22653
24729
  if (status === "warn") {
22654
24730
  return "WARN";
22655
24731
  }
24732
+ if (status === "skip") {
24733
+ return "SKIP";
24734
+ }
22656
24735
  return "FAIL";
22657
24736
  }
22658
24737
 
22659
24738
  // src/commands/doctor/action.ts
22660
- async function runDoctorCommand(config, parsed, context) {
22661
- const [instanceName, instance] = resolveInstance(config, parsed);
24739
+ async function runDoctorCommand(config, parsed, context, options = {}) {
24740
+ const strict = parsed.flags.strict === true;
24741
+ const instances = parsed.flags.all === true ? Object.keys(config.instances) : [resolveInstance(config, parsed)[0]];
24742
+ if (instances.length === 0) {
24743
+ throw new Error("No instances are configured.");
24744
+ }
24745
+ let failed = false;
24746
+ for (const [index, instanceName] of instances.entries()) {
24747
+ const instance = config.instances[instanceName];
24748
+ if (index > 0) {
24749
+ context.stdout.write("\n");
24750
+ }
24751
+ const checks = await runDiagnostics(instance, context, options);
24752
+ context.stdout.write(
24753
+ `Doctor for ${instanceName} (${instance.target})
24754
+ `
24755
+ );
24756
+ context.stdout.write(`${formatChecks(checks)}
24757
+ `);
24758
+ context.stdout.write(`${formatSummary(summarizeChecks(checks))}
24759
+ `);
24760
+ failed = hasFailedChecks(checks, strict) || failed;
24761
+ }
24762
+ return failed ? 1 : 0;
24763
+ }
24764
+ async function runDiagnostics(instance, context, options) {
22662
24765
  const driver = drivers[instance.target];
22663
- const checks = await runDoctor(
24766
+ return runDoctor(
22664
24767
  instance,
22665
24768
  context,
22666
- await driver.diagnostics(instance, context)
24769
+ await driver.diagnostics(instance, context),
24770
+ options
22667
24771
  );
22668
- context.stdout.write(`Doctor for ${instanceName} (${instance.target})
22669
- `);
22670
- context.stdout.write(`${formatChecks(checks)}
22671
- `);
22672
- return hasFailedChecks(checks) ? 1 : 0;
22673
24772
  }
22674
24773
 
22675
24774
  // src/commands/doctor/index.ts
22676
24775
  function createDoctorCommand(context, setExitCode) {
22677
- return new Command("doctor").description("Run deployment diagnostics").option("--instance <name>", "select a configured instance").action(async (options) => {
24776
+ return new Command("doctor").description("Run deployment diagnostics").option("--instance <name>", "select a configured instance").option("--all", "run the checks for every configured instance").option(
24777
+ "--strict",
24778
+ "treat warnings as failures (skipped checks never fail)"
24779
+ ).action(async (options) => {
22678
24780
  const { config } = await loadCliState(context);
22679
24781
  setExitCode(
22680
24782
  await runDoctorCommand(
@@ -23458,10 +25560,10 @@ function parseVersion(tag) {
23458
25560
  patch: Number(match[3])
23459
25561
  };
23460
25562
  }
23461
- function compareVersions(a, b) {
25563
+ function compareVersions2(a, b) {
23462
25564
  return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
23463
25565
  }
23464
- var migrationBaseUrl = "https://docs.eudiplo.dev/migration";
25566
+ var migrationBaseUrl = "https://docs.eudiplo.dev/upgrade";
23465
25567
  function migrationNotes(from, to) {
23466
25568
  const fromVersion = parseVersion(from);
23467
25569
  const toVersion = parseVersion(to);
@@ -23470,7 +25572,7 @@ function migrationNotes(from, to) {
23470
25572
  `Cannot compare ${from} and ${to} as versions. Check the release notes and ${migrationBaseUrl} before continuing.`
23471
25573
  ];
23472
25574
  }
23473
- const order = compareVersions(fromVersion, toVersion);
25575
+ const order = compareVersions2(fromVersion, toVersion);
23474
25576
  if (order > 0) {
23475
25577
  return [
23476
25578
  `${to} is older than ${from}. Downgrades are not supported once database migrations have run; restore a backup instead if you need to go back.`
@@ -23608,78 +25710,6 @@ function createUpgradeCommand(context, setExitCode) {
23608
25710
 
23609
25711
  // src/commands/version/action.ts
23610
25712
  import { isSea as isSea2 } from "node:sea";
23611
-
23612
- // package.json
23613
- var package_default = {
23614
- name: "@eudiplo/cli",
23615
- version: "8.0.2-main.e4bc4d4",
23616
- description: "Deployment-aware command line tools for EUDIPLO",
23617
- private: false,
23618
- type: "module",
23619
- bin: {
23620
- eudiplo: "dist/index.js"
23621
- },
23622
- files: [
23623
- "dist",
23624
- "templates",
23625
- "README.md"
23626
- ],
23627
- scripts: {
23628
- "assets:sync": "node scripts/sync-cli-assets.cjs",
23629
- "assets:check": "node scripts/check-cli-assets.cjs",
23630
- build: "pnpm assets:sync && pnpm assets:check && tsc -p tsconfig.json",
23631
- "build:publish": "pnpm build && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22.12 --alias:jsonc-parser=jsonc-parser/lib/esm/main.js --outfile=dist/index.js",
23632
- "build:sea:bundle": "pnpm assets:sync && pnpm assets:check && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22.12 --alias:jsonc-parser=jsonc-parser/lib/esm/main.js --outfile=dist-sea/index.js",
23633
- "build:sea": "node scripts/build-cli-sea.mjs",
23634
- dev: "tsx src/index.ts",
23635
- test: "vitest run --project cli",
23636
- lint: "biome lint .",
23637
- prepublishOnly: "pnpm build:publish"
23638
- },
23639
- keywords: [
23640
- "eudiplo",
23641
- "cli",
23642
- "eudi",
23643
- "wallet",
23644
- "oid4vci",
23645
- "oid4vp"
23646
- ],
23647
- repository: {
23648
- type: "git",
23649
- url: "git+https://github.com/openwallet-foundation/eudiplo.git",
23650
- directory: "apps/cli"
23651
- },
23652
- homepage: "https://github.com/openwallet-foundation/eudiplo/tree/main/apps/cli#readme",
23653
- bugs: {
23654
- url: "https://github.com/openwallet-foundation/eudiplo/issues"
23655
- },
23656
- author: "OpenWallet Foundation",
23657
- license: "Apache-2.0",
23658
- engines: {
23659
- node: ">=22.12.0"
23660
- },
23661
- publishConfig: {
23662
- access: "public"
23663
- },
23664
- dependencies: {
23665
- "@eudiplo/config-format": "workspace:*",
23666
- ajv: "^8.20.0",
23667
- "ajv-formats": "^3.0.1",
23668
- commander: "^15.0.0",
23669
- fflate: "^0.8.3",
23670
- "jsonc-parser": "^3.3.1"
23671
- },
23672
- devDependencies: {
23673
- "@biomejs/biome": "2.5.14",
23674
- "@types/node": "^26.4.0",
23675
- esbuild: "^0.28.2",
23676
- tsx: "^4.23.13",
23677
- typescript: "^7.0.2",
23678
- vitest: "4.1.11"
23679
- }
23680
- };
23681
-
23682
- // src/commands/version/action.ts
23683
25713
  function versionText() {
23684
25714
  return `${package_default.name} ${package_default.version}`;
23685
25715
  }
@@ -23779,7 +25809,7 @@ function createProgram(context, setExitCode) {
23779
25809
  "Print the installed CLI version"
23780
25810
  ).helpCommand(true).addHelpText(
23781
25811
  "afterAll",
23782
- "\nFor more information, see https://docs.eudiplo.dev/getting-started/quick-start"
25812
+ "\nFor more information, see https://docs.eudiplo.dev/cookbooks/foundation"
23783
25813
  );
23784
25814
  program2.addCommand(createDemoCommand(context, setExitCode));
23785
25815
  program2.addCommand(createInitCommand(context, setExitCode));