@eudiplo/cli 8.0.2 → 8.1.0-main.545119e

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 (85) hide show
  1. package/README.md +6 -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/deployment/action.d.ts +1 -1
  5. package/dist/commands/deployment/action.d.ts.map +1 -1
  6. package/dist/commands/deployment/action.js.map +1 -1
  7. package/dist/commands/deployment/index.d.ts.map +1 -1
  8. package/dist/commands/deployment/index.js +7 -4
  9. package/dist/commands/deployment/index.js.map +1 -1
  10. package/dist/commands/doctor/action.d.ts +2 -1
  11. package/dist/commands/doctor/action.d.ts.map +1 -1
  12. package/dist/commands/doctor/action.js +25 -7
  13. package/dist/commands/doctor/action.js.map +1 -1
  14. package/dist/commands/doctor/index.d.ts.map +1 -1
  15. package/dist/commands/doctor/index.js +2 -0
  16. package/dist/commands/doctor/index.js.map +1 -1
  17. package/dist/commands/open/action.d.ts +9 -0
  18. package/dist/commands/open/action.d.ts.map +1 -0
  19. package/dist/commands/open/action.js +41 -0
  20. package/dist/commands/open/action.js.map +1 -0
  21. package/dist/commands/open/index.d.ts +5 -0
  22. package/dist/commands/open/index.d.ts.map +1 -0
  23. package/dist/commands/open/index.js +15 -0
  24. package/dist/commands/open/index.js.map +1 -0
  25. package/dist/commands/upgrade/action.d.ts +3 -0
  26. package/dist/commands/upgrade/action.d.ts.map +1 -0
  27. package/dist/commands/upgrade/action.js +95 -0
  28. package/dist/commands/upgrade/action.js.map +1 -0
  29. package/dist/commands/upgrade/index.d.ts +5 -0
  30. package/dist/commands/upgrade/index.d.ts.map +1 -0
  31. package/dist/commands/upgrade/index.js +15 -0
  32. package/dist/commands/upgrade/index.js.map +1 -0
  33. package/dist/index.js +3134 -548
  34. package/dist/runtime.d.ts.map +1 -1
  35. package/dist/runtime.js +4 -0
  36. package/dist/runtime.js.map +1 -1
  37. package/dist/services/api-auth.d.ts +31 -0
  38. package/dist/services/api-auth.d.ts.map +1 -0
  39. package/dist/services/api-auth.js +78 -0
  40. package/dist/services/api-auth.js.map +1 -0
  41. package/dist/services/backend-checks.d.ts +24 -0
  42. package/dist/services/backend-checks.d.ts.map +1 -0
  43. package/dist/services/backend-checks.js +105 -0
  44. package/dist/services/backend-checks.js.map +1 -0
  45. package/dist/services/browser.d.ts +30 -0
  46. package/dist/services/browser.d.ts.map +1 -0
  47. package/dist/services/browser.js +102 -0
  48. package/dist/services/browser.js.map +1 -0
  49. package/dist/services/compose-args.d.ts +28 -0
  50. package/dist/services/compose-args.d.ts.map +1 -0
  51. package/dist/services/compose-args.js +117 -0
  52. package/dist/services/compose-args.js.map +1 -0
  53. package/dist/services/compose-project.js +6 -6
  54. package/dist/services/compose-project.js.map +1 -1
  55. package/dist/services/compose-upgrade.d.ts +40 -0
  56. package/dist/services/compose-upgrade.d.ts.map +1 -0
  57. package/dist/services/compose-upgrade.js +102 -0
  58. package/dist/services/compose-upgrade.js.map +1 -0
  59. package/dist/services/container-state.d.ts +14 -0
  60. package/dist/services/container-state.d.ts.map +1 -0
  61. package/dist/services/container-state.js +70 -0
  62. package/dist/services/container-state.js.map +1 -0
  63. package/dist/services/deployment-drivers.d.ts +8 -1
  64. package/dist/services/deployment-drivers.d.ts.map +1 -1
  65. package/dist/services/deployment-drivers.js +162 -19
  66. package/dist/services/deployment-drivers.js.map +1 -1
  67. package/dist/services/diagnostics.d.ts +19 -2
  68. package/dist/services/diagnostics.d.ts.map +1 -1
  69. package/dist/services/diagnostics.js +213 -49
  70. package/dist/services/diagnostics.js.map +1 -1
  71. package/dist/services/tls-expiry.d.ts +16 -0
  72. package/dist/services/tls-expiry.d.ts.map +1 -0
  73. package/dist/services/tls-expiry.js +80 -0
  74. package/dist/services/tls-expiry.js.map +1 -0
  75. package/dist/types.d.ts +5 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +4 -4
  78. package/templates/demo-config/trust-lists/trustlist-580831bc-ef11-43f4-a3be-a2b6bf1b29a3-config.json +1 -97
  79. package/templates/docker-compose.yml +35 -28
  80. package/templates/schemas/IssuanceConfig.schema.json +13 -0
  81. package/templates/schemas/IssuanceConfigFile.schema.json +663 -1
  82. package/templates/schemas/KeyChainConfigFile.schema.json +7 -3
  83. package/templates/schemas/KeyChainImportDto.schema.json +7 -3
  84. package/templates/schemas/PresentationConfigCreateDto.schema.json +3 -6
  85. 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)
@@ -2980,7 +2980,7 @@ var require_compile = __commonJS({
2980
2980
  const schOrFunc = root.refs[ref];
2981
2981
  if (schOrFunc)
2982
2982
  return schOrFunc;
2983
- let _sch = resolve7.call(this, root, ref);
2983
+ let _sch = resolve9.call(this, root, ref);
2984
2984
  if (_sch === void 0) {
2985
2985
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
2986
2986
  const { schemaId } = this.opts;
@@ -3007,7 +3007,7 @@ var require_compile = __commonJS({
3007
3007
  function sameSchemaEnv(s1, s2) {
3008
3008
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3009
3009
  }
3010
- function resolve7(root, ref) {
3010
+ function resolve9(root, ref) {
3011
3011
  let sch;
3012
3012
  while (typeof (sch = this.refs[ref]) == "string")
3013
3013
  ref = sch;
@@ -3828,11 +3828,11 @@ 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
  }
3835
- function resolve7(baseURI, relativeURI, options) {
3835
+ function resolve9(baseURI, relativeURI, options) {
3836
3836
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3837
3837
  const {
3838
3838
  parsed: baseParsed,
@@ -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) {
@@ -4194,11 +4194,11 @@ var require_fast_uri = __commonJS({
4194
4194
  var fastUri = {
4195
4195
  SCHEMES,
4196
4196
  normalize,
4197
- resolve: resolve7,
4197
+ resolve: resolve9,
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
@@ -21550,8 +23010,169 @@ function createConfigCommand(context, setExitCode) {
21550
23010
 
21551
23011
  // src/services/deployment-drivers.ts
21552
23012
  import { access as access2, mkdir as mkdir6, rm as rm4, stat as stat2, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
21553
- import { delimiter, join as join10, resolve as resolve5 } from "node:path";
23013
+ import { delimiter, join as join10, resolve as resolve6 } from "node:path";
21554
23014
  import { spawn } from "node:child_process";
23015
+
23016
+ // src/services/compose-args.ts
23017
+ import { resolve as resolve5 } from "node:path";
23018
+ var SERVICE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
23019
+ var DURATION = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
23020
+ var TIMESTAMP = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/;
23021
+ function assertServiceName(service) {
23022
+ if (!SERVICE_NAME.test(service)) {
23023
+ throw new Error(
23024
+ `Invalid service name ${JSON.stringify(service)}. Service names start with a letter or digit and contain only letters, digits, ".", "_" and "-".`
23025
+ );
23026
+ }
23027
+ }
23028
+ function assertSince(since) {
23029
+ const isDuration = DURATION.test(since);
23030
+ const isTimestamp = TIMESTAMP.test(since) && !Number.isNaN(Date.parse(since));
23031
+ if (!isDuration && !isTimestamp) {
23032
+ throw new Error(
23033
+ `Invalid --since value ${JSON.stringify(since)}. Use a duration such as 10m or 2h30m, or a timestamp such as 2026-09-01T12:00:00Z.`
23034
+ );
23035
+ }
23036
+ }
23037
+ function assertTail(tail) {
23038
+ if (!Number.isInteger(tail) || tail < 0) {
23039
+ throw new Error("--tail must be a whole number of lines, 0 or more.");
23040
+ }
23041
+ }
23042
+ function buildComposeBaseArgs(instance, projectDirectory, composeFiles) {
23043
+ const args = ["compose"];
23044
+ if (instance.envFile) {
23045
+ args.push("--env-file", resolve5(projectDirectory, instance.envFile));
23046
+ }
23047
+ for (const composeFile of composeFiles) {
23048
+ args.push("-f", resolve5(projectDirectory, composeFile));
23049
+ }
23050
+ for (const profile of instance.composeProfiles ?? []) {
23051
+ args.push("--profile", profile);
23052
+ }
23053
+ if (instance.projectName) {
23054
+ args.push("--project-name", instance.projectName);
23055
+ }
23056
+ return args;
23057
+ }
23058
+ function buildComposePsArgs() {
23059
+ return ["ps"];
23060
+ }
23061
+ function buildComposeListServicesArgs() {
23062
+ return ["config", "--services"];
23063
+ }
23064
+ function buildComposeLogsArgs(service, options = {}) {
23065
+ const args = ["logs"];
23066
+ if (options.follow === true) {
23067
+ args.push("--follow");
23068
+ }
23069
+ if (options.tail !== void 0) {
23070
+ assertTail(options.tail);
23071
+ args.push("--tail", String(options.tail));
23072
+ }
23073
+ if (options.since !== void 0) {
23074
+ assertSince(options.since);
23075
+ args.push("--since", options.since);
23076
+ }
23077
+ if (service !== void 0) {
23078
+ assertServiceName(service);
23079
+ args.push(service);
23080
+ }
23081
+ return args;
23082
+ }
23083
+ function buildComposePullArgs(service) {
23084
+ const args = ["pull"];
23085
+ if (service !== void 0) {
23086
+ assertServiceName(service);
23087
+ args.push(service);
23088
+ }
23089
+ return args;
23090
+ }
23091
+ function buildComposeUpArgs() {
23092
+ return ["up", "-d"];
23093
+ }
23094
+ function buildComposeRestartArgs(service) {
23095
+ const args = ["restart"];
23096
+ if (service !== void 0) {
23097
+ assertServiceName(service);
23098
+ args.push(service);
23099
+ }
23100
+ return args;
23101
+ }
23102
+ function parseServiceList(stdout) {
23103
+ return stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => SERVICE_NAME.test(line)).sort();
23104
+ }
23105
+ function assertKnownService(service, known) {
23106
+ if (!known.includes(service)) {
23107
+ throw new Error(
23108
+ `Unknown service ${service}. Services in this project: ${known.join(", ") || "none"}.`
23109
+ );
23110
+ }
23111
+ }
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
+
23175
+ // src/services/deployment-drivers.ts
21555
23176
  var drivers = {
21556
23177
  compose: {
21557
23178
  target: "compose",
@@ -21564,10 +23185,16 @@ var drivers = {
21564
23185
  status: runtime ? "pass" : "warn",
21565
23186
  message: runtime ? `${runtime.name} found at ${runtime.command}` : "Docker or Podman was not found in a supported install location."
21566
23187
  });
23188
+ if (runtime) {
23189
+ checks.push(await composeProviderCheck(runtime, context));
23190
+ checks.push(
23191
+ await composeServicesCheck(runtime, instance, context)
23192
+ );
23193
+ }
21567
23194
  for (const composeFile of getComposeFiles(instance)) {
21568
23195
  let found = true;
21569
23196
  try {
21570
- await access2(resolve5(projectDirectory, composeFile));
23197
+ await access2(resolve6(projectDirectory, composeFile));
21571
23198
  } catch {
21572
23199
  found = false;
21573
23200
  }
@@ -21585,8 +23212,41 @@ var drivers = {
21585
23212
  down(options) {
21586
23213
  return runCompose(["down", ...options.args], options);
21587
23214
  },
21588
- logs(options) {
21589
- return runCompose(["logs", "-f", ...options.args], options);
23215
+ async logs(options) {
23216
+ const service = await resolveComposeService(options);
23217
+ const { follow, tail } = readLogOptions(options.flags);
23218
+ const since = typeof options.flags.since === "string" ? options.flags.since : void 0;
23219
+ return runCompose(
23220
+ [
23221
+ ...buildComposeLogsArgs(service, { follow, tail, since }),
23222
+ ...options.args
23223
+ ],
23224
+ options
23225
+ );
23226
+ },
23227
+ async ps(options) {
23228
+ return runCompose(
23229
+ [...buildComposePsArgs(), ...options.args],
23230
+ options
23231
+ );
23232
+ },
23233
+ async pull(options) {
23234
+ const service = await resolveComposeService(options);
23235
+ return runCompose(
23236
+ [...buildComposePullArgs(service), ...options.args],
23237
+ options
23238
+ );
23239
+ },
23240
+ async restart(options) {
23241
+ assertWritable(options);
23242
+ const service = await resolveComposeService(options);
23243
+ const restartArgs = buildComposeRestartArgs(service);
23244
+ const runtime = await resolveComposeRuntime(options.context.env);
23245
+ options.context.stdout.write(
23246
+ `Restarting ${service ?? "all services"} of ${options.instanceName}${runtime ? ` with ${runtime.name} compose` : ""}
23247
+ `
23248
+ );
23249
+ return runCompose([...restartArgs, ...options.args], options);
21590
23250
  }
21591
23251
  },
21592
23252
  external: {
@@ -21606,7 +23266,11 @@ var drivers = {
21606
23266
  );
21607
23267
  return runKubectl(
21608
23268
  [
21609
- ...buildLogsArgs(scope, workload, readLogOptions(options.flags)),
23269
+ ...buildLogsArgs(
23270
+ scope,
23271
+ workload,
23272
+ readLogOptions(options.flags)
23273
+ ),
21610
23274
  ...options.args
21611
23275
  ],
21612
23276
  options
@@ -21634,14 +23298,17 @@ var drivers = {
21634
23298
  [...buildRestartArgs(scope, workload), ...options.args],
21635
23299
  options
21636
23300
  );
21637
- if (restarted !== 0 || options.flags.wait === false) {
23301
+ if (restarted !== 0 || options.flags["no-wait"] === true) {
21638
23302
  return restarted;
21639
23303
  }
21640
23304
  return runKubectl(buildRolloutStatusArgs(scope, workload), options);
21641
23305
  }
21642
23306
  }
21643
23307
  };
21644
- function assertWritable({ instance, instanceName }) {
23308
+ function assertWritable({
23309
+ instance,
23310
+ instanceName
23311
+ }) {
21645
23312
  if (instance.readOnly === true) {
21646
23313
  throw new Error(
21647
23314
  `Instance ${instanceName} is registered read-only. Re-register it without --read-only to allow changes.`
@@ -21847,7 +23514,10 @@ function unreadyEndpoints(stdout) {
21847
23514
  (endpoint) => isRecord3(endpoint) && // An omitted ready condition means ready, per the API contract.
21848
23515
  (!isRecord3(endpoint.conditions) || endpoint.conditions.ready !== false)
21849
23516
  );
21850
- readyByService.set(service, (readyByService.get(service) ?? false) || ready);
23517
+ readyByService.set(
23518
+ service,
23519
+ (readyByService.get(service) ?? false) || ready
23520
+ );
21851
23521
  }
21852
23522
  return [...readyByService.entries()].filter(([, ready]) => !ready).map(([service]) => service).sort();
21853
23523
  }
@@ -21975,7 +23645,7 @@ async function ensureComposeProject(cwd, options) {
21975
23645
  composeProfiles: composeProfiles(options),
21976
23646
  envFile: envFileName,
21977
23647
  projectName: mode === "demo" ? "eudiplo-demo" : "eudiplo",
21978
- projectDirectory: resolve5(cwd)
23648
+ projectDirectory: resolve6(cwd)
21979
23649
  };
21980
23650
  }
21981
23651
  async function ensureProjectDirectory(directory) {
@@ -22029,23 +23699,14 @@ function unsupportedCommand(command, target) {
22029
23699
  }
22030
23700
  async function runCompose(args, { instance, context }) {
22031
23701
  const projectDirectory = instance.projectDirectory ?? context.cwd;
22032
- const composeArgs = ["compose"];
22033
- if (instance.envFile) {
22034
- composeArgs.push(
22035
- "--env-file",
22036
- resolve5(projectDirectory, instance.envFile)
22037
- );
22038
- }
22039
- for (const composeFile of getComposeFiles(instance)) {
22040
- composeArgs.push("-f", resolve5(projectDirectory, composeFile));
22041
- }
22042
- for (const profile of instance.composeProfiles ?? []) {
22043
- composeArgs.push("--profile", profile);
22044
- }
22045
- if (instance.projectName) {
22046
- composeArgs.push("--project-name", instance.projectName);
22047
- }
22048
- composeArgs.push(...args);
23702
+ const composeArgs = [
23703
+ ...buildComposeBaseArgs(
23704
+ instance,
23705
+ projectDirectory,
23706
+ getComposeFiles(instance)
23707
+ ),
23708
+ ...args
23709
+ ];
22049
23710
  const composeRuntime = await resolveComposeRuntime(context.env);
22050
23711
  if (!composeRuntime) {
22051
23712
  context.stderr.write(
@@ -22054,17 +23715,158 @@ async function runCompose(args, { instance, context }) {
22054
23715
  return 1;
22055
23716
  }
22056
23717
  return new Promise((resolveProcess) => {
22057
- const child = spawn(composeRuntime.command, composeArgs, {
22058
- cwd: projectDirectory,
23718
+ const child = spawn(composeRuntime.command, composeArgs, {
23719
+ cwd: projectDirectory,
23720
+ env: context.env,
23721
+ stdio: "inherit"
23722
+ });
23723
+ child.on("error", (error) => {
23724
+ context.stderr.write(`${error.message}
23725
+ `);
23726
+ resolveProcess(1);
23727
+ });
23728
+ child.on("close", (code) => resolveProcess(code ?? 1));
23729
+ });
23730
+ }
23731
+ async function captureCompose(args, { instance, context }) {
23732
+ const projectDirectory = instance.projectDirectory ?? context.cwd;
23733
+ const composeRuntime = await resolveComposeRuntime(context.env);
23734
+ if (!composeRuntime) {
23735
+ throw new Error(
23736
+ "Docker or Podman was not found in a supported install location."
23737
+ );
23738
+ }
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) {
23849
+ return new Promise((resolveProcess) => {
23850
+ const child = spawn(command, args, {
22059
23851
  env: context.env,
22060
- stdio: "inherit"
23852
+ stdio: ["ignore", "pipe", "pipe"]
22061
23853
  });
22062
- child.on("error", (error) => {
22063
- context.stderr.write(`${error.message}
22064
- `);
22065
- resolveProcess(1);
23854
+ let stdout = "";
23855
+ let stderr = "";
23856
+ child.stdout?.on("data", (chunk) => {
23857
+ stdout += String(chunk);
22066
23858
  });
22067
- child.on("close", (code) => resolveProcess(code ?? 1));
23859
+ child.stderr?.on("data", (chunk) => {
23860
+ stderr += String(chunk);
23861
+ });
23862
+ child.on(
23863
+ "error",
23864
+ (error) => resolveProcess({ code: 1, stdout, stderr: error.message })
23865
+ );
23866
+ child.on(
23867
+ "close",
23868
+ (code) => resolveProcess({ code: code ?? 1, stdout, stderr })
23869
+ );
22068
23870
  });
22069
23871
  }
22070
23872
  async function resolveComposeRuntime(env) {
@@ -22164,7 +23966,7 @@ async function removeIfExists(path2) {
22164
23966
  }
22165
23967
 
22166
23968
  // src/services/project-directory.ts
22167
- import { resolve as resolve6 } from "node:path";
23969
+ import { resolve as resolve7 } from "node:path";
22168
23970
  function resolveProjectDirectory(parsed, context, promptedDirectory) {
22169
23971
  const positionalDirectory = parsed.subject;
22170
23972
  const flagDirectory = readStringFlag(parsed.flags, "directory");
@@ -22176,7 +23978,7 @@ function resolveProjectDirectory(parsed, context, promptedDirectory) {
22176
23978
  "Specify the project directory either positionally or with --directory."
22177
23979
  );
22178
23980
  }
22179
- return resolve6(
23981
+ return resolve7(
22180
23982
  context.cwd,
22181
23983
  positionalDirectory ?? flagDirectory ?? promptedDirectory ?? "."
22182
23984
  );
@@ -22197,209 +23999,697 @@ async function runDemo(configPath, config, parsed, context) {
22197
23999
  } finally {
22198
24000
  prompter.close();
22199
24001
  }
22200
- const force = parsed.flags.force === true;
22201
- const reset = parsed.flags.reset === true;
22202
- const imageTagOverride = readStringFlag(parsed.flags, "image-tag") ?? context.env.EUDIPLO_IMAGE_TAG;
22203
- if (reset && !force) {
22204
- throw new Error(
22205
- "demo --reset requires --force to remove managed demo data."
22206
- );
24002
+ const force = parsed.flags.force === true;
24003
+ const reset = parsed.flags.reset === true;
24004
+ const imageTagOverride = readStringFlag(parsed.flags, "image-tag") ?? context.env.EUDIPLO_IMAGE_TAG;
24005
+ if (reset && !force) {
24006
+ throw new Error(
24007
+ "demo --reset requires --force to remove managed demo data."
24008
+ );
24009
+ }
24010
+ if (reset && await demoProjectExists(projectDirectory)) {
24011
+ context.stdout.write(
24012
+ "Resetting demo deployment and removing managed demo volumes...\n"
24013
+ );
24014
+ await drivers.compose.down?.({
24015
+ instanceName: "local",
24016
+ instance: {
24017
+ target: "compose",
24018
+ url: "http://localhost:3000",
24019
+ clientUrl: "http://localhost:4200",
24020
+ composeFiles: ["eudiplo.demo.compose.yaml"],
24021
+ envFile: ".eudiplo.demo.env",
24022
+ projectName: "eudiplo-demo",
24023
+ projectDirectory
24024
+ },
24025
+ args: ["--volumes", "--remove-orphans"],
24026
+ flags: {},
24027
+ context
24028
+ });
24029
+ }
24030
+ const instance = await ensureComposeProject(projectDirectory, {
24031
+ mode: "demo",
24032
+ force,
24033
+ reset,
24034
+ imageTagOverride
24035
+ });
24036
+ const nextConfig = upsertInstance(config, "local", instance);
24037
+ await saveConfig(configPath, nextConfig);
24038
+ context.stdout.write("Starting EUDIPLO demo with the Compose runtime...\n");
24039
+ const exitCode2 = await drivers.compose.up?.({
24040
+ instanceName: "local",
24041
+ instance,
24042
+ args: [],
24043
+ flags: {},
24044
+ context
24045
+ }) ?? 1;
24046
+ if (exitCode2 === 0) {
24047
+ context.stdout.write("\nDemo mode - not for production.\n");
24048
+ context.stdout.write("API URL: http://localhost:3000\n");
24049
+ context.stdout.write("Client URL: http://localhost:4200\n");
24050
+ context.stdout.write("Demo credentials:\n");
24051
+ context.stdout.write(" Client ID: root\n");
24052
+ context.stdout.write(" Client Secret: root\n");
24053
+ context.stdout.write(
24054
+ `Editable demo config: ${projectDirectory}/config/demo
24055
+ `
24056
+ );
24057
+ }
24058
+ return exitCode2;
24059
+ }
24060
+
24061
+ // src/commands/demo/index.ts
24062
+ function createDemoCommand(context, setExitCode) {
24063
+ return new Command("demo").description("Start the minimal local demo").argument("[directory]", "project directory").option("--directory <path>", "set the project directory").option("--reset", "recreate managed demo data and configuration").option("--force", "allow replacement of managed demo files").option("--yes", "accept the default directory without prompting").option("--no-interactive", "disable the directory prompt").option(
24064
+ "--image-tag <tag>",
24065
+ "override the backend and client image tag"
24066
+ ).action(async (directory, options) => {
24067
+ const { configPath, config } = await loadCliState(context);
24068
+ setExitCode(
24069
+ await runDemo(
24070
+ configPath,
24071
+ config,
24072
+ parsedArgs("demo", directory, [], options),
24073
+ context
24074
+ )
24075
+ );
24076
+ });
24077
+ }
24078
+
24079
+ // src/commands/deployment/action.ts
24080
+ async function runDriverCommand(config, command, parsed, context) {
24081
+ const [instanceName, instance] = resolveInstance(config, parsed);
24082
+ const driver = drivers[instance.target];
24083
+ const handler = driver[command];
24084
+ if (!handler) {
24085
+ throw new Error(unsupportedCommand(command, instance.target));
24086
+ }
24087
+ if (instance.target === "compose") {
24088
+ const clientMode = instance.clientUrl ? "client enabled" : "client disabled";
24089
+ context.stdout.write(`${command} ${instanceName} (${clientMode})
24090
+ `);
24091
+ }
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.545119e",
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
+ };
22207
24318
  }
22208
- if (reset && await demoProjectExists(projectDirectory)) {
22209
- context.stdout.write(
22210
- "Resetting demo deployment and removing managed demo volumes...\n"
22211
- );
22212
- await drivers.compose.down?.({
22213
- instanceName: "local",
22214
- instance: {
22215
- target: "compose",
22216
- url: "http://localhost:3000",
22217
- clientUrl: "http://localhost:4200",
22218
- composeFiles: ["eudiplo.demo.compose.yaml"],
22219
- envFile: ".eudiplo.demo.env",
22220
- projectName: "eudiplo-demo",
22221
- projectDirectory
22222
- },
22223
- args: ["--volumes", "--remove-orphans"],
22224
- flags: {},
22225
- context
22226
- });
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;
22227
24329
  }
22228
- const instance = await ensureComposeProject(projectDirectory, {
22229
- mode: "demo",
22230
- force,
22231
- reset,
22232
- imageTagOverride
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
+ };
22233
24345
  });
22234
- const nextConfig = upsertInstance(config, "local", instance);
22235
- await saveConfig(configPath, nextConfig);
22236
- context.stdout.write("Starting EUDIPLO demo with the Compose runtime...\n");
22237
- const exitCode2 = await drivers.compose.up?.({
22238
- instanceName: "local",
22239
- instance,
22240
- args: [],
22241
- flags: {},
22242
- context
22243
- }) ?? 1;
22244
- if (exitCode2 === 0) {
22245
- context.stdout.write("\nDemo mode - not for production.\n");
22246
- context.stdout.write("API URL: http://localhost:3000\n");
22247
- context.stdout.write("Client URL: http://localhost:4200\n");
22248
- context.stdout.write("Demo credentials:\n");
22249
- context.stdout.write(" Client ID: root\n");
22250
- context.stdout.write(" Client Secret: root\n");
22251
- context.stdout.write(
22252
- `Editable demo config: ${projectDirectory}/config/demo
22253
- `
22254
- );
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
+ };
22255
24355
  }
22256
- return exitCode2;
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
+ };
22257
24373
  }
22258
-
22259
- // src/commands/demo/index.ts
22260
- function createDemoCommand(context, setExitCode) {
22261
- return new Command("demo").description("Start the minimal local demo").argument("[directory]", "project directory").option("--directory <path>", "set the project directory").option("--reset", "recreate managed demo data and configuration").option("--force", "allow replacement of managed demo files").option("--yes", "accept the default directory without prompting").option("--no-interactive", "disable the directory prompt").option(
22262
- "--image-tag <tag>",
22263
- "override the backend and client image tag"
22264
- ).action(async (directory, options) => {
22265
- const { configPath, config } = await loadCliState(context);
22266
- setExitCode(
22267
- await runDemo(
22268
- configPath,
22269
- config,
22270
- parsedArgs("demo", directory, [], options),
22271
- context
22272
- )
22273
- );
22274
- });
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
+ };
22275
24384
  }
22276
24385
 
22277
- // src/commands/deployment/action.ts
22278
- async function runDriverCommand(config, command, parsed, context) {
22279
- const [instanceName, instance] = resolveInstance(config, parsed);
22280
- const driver = drivers[instance.target];
22281
- const handler = driver[command];
22282
- if (!handler) {
22283
- throw new Error(unsupportedCommand(command, instance.target));
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
+ };
22284
24401
  }
22285
- if (instance.target === "compose") {
22286
- const clientMode = instance.clientUrl ? "client enabled" : "client disabled";
22287
- context.stdout.write(`${command} ${instanceName} (${clientMode})
22288
- `);
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
+ };
22289
24408
  }
22290
- return handler({
22291
- instanceName,
22292
- instance,
22293
- args: parsed.positionals,
22294
- flags: parsed.flags,
22295
- context
22296
- });
24409
+ return {
24410
+ name: "TLS certificate",
24411
+ status: "pass",
24412
+ message: `Valid until ${expiresOn} (${remainingDays} day(s) left), issued by ${certificate.issuer}.`
24413
+ };
22297
24414
  }
22298
-
22299
- // src/commands/deployment/index.ts
22300
- var descriptions = {
22301
- up: "Start the selected Compose deployment",
22302
- down: "Stop the selected Compose deployment",
22303
- logs: "Stream logs for the selected deployment",
22304
- ps: "List the running containers or pods for the selected deployment",
22305
- restart: "Restart the workloads of the selected deployment"
22306
- };
22307
- var serviceScoped = /* @__PURE__ */ new Set([
22308
- "logs",
22309
- "restart"
22310
- ]);
22311
- function createDeploymentCommands(context, setExitCode) {
22312
- return ["up", "down", "logs", "ps", "restart"].map((name) => {
22313
- const command = new Command(name).description(descriptions[name]).argument(
22314
- "[args...]",
22315
- "additional arguments passed to the deployment runtime"
22316
- ).option("--instance <name>", "select a configured instance");
22317
- if (serviceScoped.has(name)) {
22318
- command.option(
22319
- "--service <name>",
22320
- "select a configured workload (Kubernetes instances)"
22321
- );
22322
- }
22323
- if (name === "logs") {
22324
- command.option("--follow", "stream new output as it arrives").option("--tail <lines>", "number of recent lines to show");
22325
- }
22326
- if (name === "restart") {
22327
- command.option(
22328
- "--no-wait",
22329
- "return without waiting for the rollout to finish"
22330
- );
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;
24429
+ }
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)
24434
+ });
22331
24435
  }
22332
- return command.action(async (args, options) => {
22333
- const { config } = await loadCliState(context);
22334
- setExitCode(
22335
- await runDriverCommand(
22336
- config,
22337
- name,
22338
- parsedArgs(name, void 0, args, options),
22339
- context
22340
- )
22341
- );
22342
- });
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);
22343
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
+ }
22344
24463
  }
22345
24464
 
22346
24465
  // src/services/diagnostics.ts
22347
- async function runDoctor(instance, context, driverDiagnostics) {
22348
- const checks = [];
22349
- const baseUrl = parseUrl(instance.url);
22350
- if (!baseUrl) {
22351
- return [
22352
- {
24466
+ var publicUrlCheck = {
24467
+ id: "public-url",
24468
+ async run({ instance, baseUrl }) {
24469
+ if (!baseUrl) {
24470
+ return {
22353
24471
  name: "public URL",
22354
24472
  status: "fail",
22355
24473
  message: `${instance.url} is not a valid absolute URL`
22356
- }
22357
- ];
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
+ };
22358
24481
  }
22359
- checks.push({
22360
- name: "public URL",
22361
- status: baseUrl.protocol === "https:" ? "pass" : "warn",
22362
- message: baseUrl.protocol === "https:" ? `${baseUrl.href} uses HTTPS` : `${baseUrl.href} does not use HTTPS`
22363
- });
22364
- checks.push(
22365
- await checkEndpoint("API reachability", baseUrl, "/api/docs", context)
22366
- );
22367
- checks.push(
22368
- await checkEndpoint("health endpoint", baseUrl, "/health", context)
22369
- );
22370
- checks.push(checkAuthentication(context));
22371
- 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
+ }
22372
24528
  const clientUrl = parseUrl(instance.clientUrl);
22373
- checks.push(
22374
- clientUrl ? await checkEndpoint(
22375
- "client connectivity",
22376
- clientUrl,
22377
- "/",
22378
- context
22379
- ) : {
24529
+ if (!clientUrl) {
24530
+ return {
22380
24531
  name: "client connectivity",
22381
24532
  status: "fail",
22382
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
+ };
22383
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
22384
24587
  );
22385
- } else {
22386
- checks.push({
22387
- name: "client connectivity",
22388
- status: "warn",
22389
- message: "No client URL configured for this instance."
22390
- });
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));
22391
24655
  }
22392
24656
  checks.push(...driverDiagnostics);
22393
24657
  return checks;
22394
24658
  }
22395
- function hasFailedChecks(checks) {
22396
- 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
+ );
22397
24671
  }
22398
24672
  function formatChecks(checks) {
22399
24673
  return checks.map(
22400
24674
  (check) => `${formatStatus(check.status)} ${check.name}: ${check.message}`
22401
24675
  ).join("\n");
22402
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
+ }
22403
24693
  async function checkEndpoint(name, baseUrl, path2, context) {
22404
24694
  const url = new URL(path2, baseUrl);
22405
24695
  try {
@@ -22424,22 +24714,6 @@ async function checkEndpoint(name, baseUrl, path2, context) {
22424
24714
  };
22425
24715
  }
22426
24716
  }
22427
- function checkAuthentication(context) {
22428
- const hasClientId = Boolean(context.env.EUDIPLO_CLIENT_ID);
22429
- const hasClientSecret = Boolean(context.env.EUDIPLO_CLIENT_SECRET);
22430
- if (hasClientId && hasClientSecret) {
22431
- return {
22432
- name: "authentication configuration",
22433
- status: "pass",
22434
- message: "Client credentials are available in environment variables."
22435
- };
22436
- }
22437
- return {
22438
- name: "authentication configuration",
22439
- status: "warn",
22440
- message: "Set EUDIPLO_CLIENT_ID and EUDIPLO_CLIENT_SECRET when commands need authenticated API access."
22441
- };
22442
- }
22443
24717
  function parseUrl(value) {
22444
24718
  try {
22445
24719
  const url = new URL(value);
@@ -22455,28 +24729,54 @@ function formatStatus(status) {
22455
24729
  if (status === "warn") {
22456
24730
  return "WARN";
22457
24731
  }
24732
+ if (status === "skip") {
24733
+ return "SKIP";
24734
+ }
22458
24735
  return "FAIL";
22459
24736
  }
22460
24737
 
22461
24738
  // src/commands/doctor/action.ts
22462
- async function runDoctorCommand(config, parsed, context) {
22463
- 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) {
22464
24765
  const driver = drivers[instance.target];
22465
- const checks = await runDoctor(
24766
+ return runDoctor(
22466
24767
  instance,
22467
24768
  context,
22468
- await driver.diagnostics(instance, context)
24769
+ await driver.diagnostics(instance, context),
24770
+ options
22469
24771
  );
22470
- context.stdout.write(`Doctor for ${instanceName} (${instance.target})
22471
- `);
22472
- context.stdout.write(`${formatChecks(checks)}
22473
- `);
22474
- return hasFailedChecks(checks) ? 1 : 0;
22475
24772
  }
22476
24773
 
22477
24774
  // src/commands/doctor/index.ts
22478
24775
  function createDoctorCommand(context, setExitCode) {
22479
- 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) => {
22480
24780
  const { config } = await loadCliState(context);
22481
24781
  setExitCode(
22482
24782
  await runDoctorCommand(
@@ -23020,6 +25320,146 @@ function createInstanceCommand(context, setExitCode) {
23020
25320
  return instance;
23021
25321
  }
23022
25322
 
25323
+ // src/services/browser.ts
25324
+ import { spawn as spawn2 } from "node:child_process";
25325
+ import { access as access3 } from "node:fs/promises";
25326
+ import { delimiter as delimiter2, join as join11 } from "node:path";
25327
+ function assertWebUrl(value) {
25328
+ let url;
25329
+ try {
25330
+ url = new URL(value);
25331
+ } catch {
25332
+ throw new Error(`${value} is not a valid absolute URL.`);
25333
+ }
25334
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
25335
+ throw new Error(
25336
+ `Refusing to open ${value}: only http and https URLs are supported.`
25337
+ );
25338
+ }
25339
+ return url;
25340
+ }
25341
+ async function chooseLauncher(url, { platform, env, which }) {
25342
+ const browser = env.BROWSER?.trim();
25343
+ if (browser && !/\s|%s/.test(browser)) {
25344
+ const command = await which(browser);
25345
+ if (command) {
25346
+ return { command, args: [url] };
25347
+ }
25348
+ }
25349
+ if (platform === "darwin") {
25350
+ return { command: "open", args: [url] };
25351
+ }
25352
+ if (platform === "win32") {
25353
+ return {
25354
+ command: "rundll32.exe",
25355
+ args: ["url.dll,FileProtocolHandler", url]
25356
+ };
25357
+ }
25358
+ if (env.WSL_DISTRO_NAME) {
25359
+ const wslview = await which("wslview");
25360
+ if (wslview) {
25361
+ return { command: wslview, args: [url] };
25362
+ }
25363
+ }
25364
+ if (env.DISPLAY || env.WAYLAND_DISPLAY) {
25365
+ const xdgOpen = await which("xdg-open");
25366
+ if (xdgOpen) {
25367
+ return { command: xdgOpen, args: [url] };
25368
+ }
25369
+ }
25370
+ return void 0;
25371
+ }
25372
+ function launchBrowser(launcher) {
25373
+ return new Promise((resolveLaunch) => {
25374
+ const child = spawn2(launcher.command, launcher.args, {
25375
+ detached: true,
25376
+ stdio: "ignore"
25377
+ });
25378
+ child.once("error", () => resolveLaunch(false));
25379
+ child.once("spawn", () => {
25380
+ child.unref();
25381
+ resolveLaunch(true);
25382
+ });
25383
+ });
25384
+ }
25385
+ function pathLookup(env, platform = process.platform) {
25386
+ const extensions = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
25387
+ return async (name) => {
25388
+ const entries = (env.PATH ?? "").split(delimiter2).filter(Boolean);
25389
+ for (const entry of entries) {
25390
+ for (const extension of extensions) {
25391
+ const candidate = join11(entry, `${name}${extension}`);
25392
+ try {
25393
+ await access3(candidate);
25394
+ return candidate;
25395
+ } catch {
25396
+ }
25397
+ }
25398
+ }
25399
+ return void 0;
25400
+ };
25401
+ }
25402
+
25403
+ // src/commands/open/action.ts
25404
+ var defaultDependencies = {
25405
+ platform: process.platform,
25406
+ launch: launchBrowser
25407
+ };
25408
+ function resolveOpenTarget(instanceName, instance, docs) {
25409
+ if (docs) {
25410
+ const base = instance.url.endsWith("/") ? instance.url : `${instance.url}/`;
25411
+ return assertWebUrl(new URL("api/docs", base).href).href;
25412
+ }
25413
+ if (!instance.clientUrl) {
25414
+ throw new Error(
25415
+ `Instance ${instanceName} has no web client URL. Use --docs to open the API documentation instead.`
25416
+ );
25417
+ }
25418
+ return assertWebUrl(instance.clientUrl).href;
25419
+ }
25420
+ async function runOpenCommand(config, parsed, context, dependencies = defaultDependencies) {
25421
+ const [instanceName, instance] = resolveInstance(config, parsed);
25422
+ const url = resolveOpenTarget(
25423
+ instanceName,
25424
+ instance,
25425
+ parsed.flags.docs === true
25426
+ );
25427
+ if (parsed.flags.print === true || context.interactive !== true) {
25428
+ context.stdout.write(`${url}
25429
+ `);
25430
+ return 0;
25431
+ }
25432
+ const launcher = await chooseLauncher(url, {
25433
+ platform: dependencies.platform,
25434
+ env: context.env,
25435
+ which: pathLookup(context.env, dependencies.platform)
25436
+ });
25437
+ if (!launcher || !await dependencies.launch(launcher)) {
25438
+ context.stdout.write(`No browser is available here. Open: ${url}
25439
+ `);
25440
+ return 0;
25441
+ }
25442
+ context.stdout.write(`Opening ${url}
25443
+ `);
25444
+ return 0;
25445
+ }
25446
+
25447
+ // src/commands/open/index.ts
25448
+ function createOpenCommand(context, setExitCode) {
25449
+ return new Command("open").description(
25450
+ "Open the web client of the selected instance in a browser"
25451
+ ).option("--instance <name>", "select a configured instance").option("--docs", "open the management API documentation instead").option("--print", "print the URL instead of opening a browser").action(async (options) => {
25452
+ const { config } = await loadCliState(context);
25453
+ setExitCode(
25454
+ await runOpenCommand(
25455
+ config,
25456
+ parsedArgs("open", void 0, [], options),
25457
+ context
25458
+ )
25459
+ );
25460
+ });
25461
+ }
25462
+
23023
25463
  // src/commands/status/action.ts
23024
25464
  async function runStatusCommand(config, parsed, context) {
23025
25465
  const [instanceName, instance] = resolveInstance(config, parsed);
@@ -23052,80 +25492,224 @@ function createStatusCommand(context, setExitCode) {
23052
25492
  });
23053
25493
  }
23054
25494
 
23055
- // src/commands/version/action.ts
23056
- import { isSea as isSea2 } from "node:sea";
25495
+ // src/commands/upgrade/action.ts
25496
+ import { readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
25497
+ import { resolve as resolve8 } from "node:path";
23057
25498
 
23058
- // package.json
23059
- var package_default = {
23060
- name: "@eudiplo/cli",
23061
- version: "8.0.2",
23062
- description: "Deployment-aware command line tools for EUDIPLO",
23063
- private: false,
23064
- type: "module",
23065
- bin: {
23066
- eudiplo: "dist/index.js"
23067
- },
23068
- files: [
23069
- "dist",
23070
- "templates",
23071
- "README.md"
23072
- ],
23073
- scripts: {
23074
- "assets:sync": "node scripts/sync-cli-assets.cjs",
23075
- "assets:check": "node scripts/check-cli-assets.cjs",
23076
- build: "pnpm assets:sync && pnpm assets:check && tsc -p tsconfig.json",
23077
- "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",
23078
- "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",
23079
- "build:sea": "node scripts/build-cli-sea.mjs",
23080
- dev: "tsx src/index.ts",
23081
- test: "vitest run --project cli",
23082
- lint: "biome lint .",
23083
- prepublishOnly: "pnpm build:publish"
23084
- },
23085
- keywords: [
23086
- "eudiplo",
23087
- "cli",
23088
- "eudi",
23089
- "wallet",
23090
- "oid4vci",
23091
- "oid4vp"
23092
- ],
23093
- repository: {
23094
- type: "git",
23095
- url: "git+https://github.com/openwallet-foundation/eudiplo.git",
23096
- directory: "apps/cli"
23097
- },
23098
- homepage: "https://github.com/openwallet-foundation/eudiplo/tree/main/apps/cli#readme",
23099
- bugs: {
23100
- url: "https://github.com/openwallet-foundation/eudiplo/issues"
23101
- },
23102
- author: "OpenWallet Foundation",
23103
- license: "Apache-2.0",
23104
- engines: {
23105
- node: ">=22.12.0"
23106
- },
23107
- publishConfig: {
23108
- access: "public"
23109
- },
23110
- dependencies: {
23111
- "@eudiplo/config-format": "workspace:*",
23112
- ajv: "^8.20.0",
23113
- "ajv-formats": "^3.0.1",
23114
- commander: "^15.0.0",
23115
- fflate: "^0.8.3",
23116
- "jsonc-parser": "^3.3.1"
23117
- },
23118
- devDependencies: {
23119
- "@biomejs/biome": "2.5.13",
23120
- "@types/node": "^26.4.0",
23121
- esbuild: "^0.28.2",
23122
- tsx: "^4.23.13",
23123
- typescript: "^7.0.2",
23124
- vitest: "4.1.11"
23125
- }
25499
+ // src/services/compose-upgrade.ts
25500
+ var managedImages = {
25501
+ EUDIPLO_IMAGE: "ghcr.io/openwallet-foundation/eudiplo",
25502
+ EUDIPLO_CLIENT_IMAGE: "ghcr.io/openwallet-foundation/eudiplo-client"
23126
25503
  };
25504
+ var TAG = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
25505
+ function assertImageTag(tag) {
25506
+ if (!TAG.test(tag)) {
25507
+ throw new Error(
25508
+ `Invalid image tag ${JSON.stringify(tag)}. Tags contain letters, digits, "_", "." and "-", and cannot start with "." or "-".`
25509
+ );
25510
+ }
25511
+ }
25512
+ function planImageUpgrade(content, tag) {
25513
+ assertImageTag(tag);
25514
+ const found = /* @__PURE__ */ new Set();
25515
+ const changes = [];
25516
+ const lines = content.split("\n").map((line) => {
25517
+ const match = /^(EUDIPLO_IMAGE|EUDIPLO_CLIENT_IMAGE)=(.*?)(\r?)$/.exec(
25518
+ line
25519
+ );
25520
+ if (!match) {
25521
+ return line;
25522
+ }
25523
+ const key = match[1];
25524
+ const value = match[2];
25525
+ const lineEnding = match[3];
25526
+ const repository = managedImages[key];
25527
+ if (found.has(key)) {
25528
+ throw new Error(
25529
+ `${key} is defined more than once in the env file.`
25530
+ );
25531
+ }
25532
+ found.add(key);
25533
+ const prefix = `${repository}:`;
25534
+ const current = value.startsWith(prefix) ? value.slice(prefix.length) : void 0;
25535
+ if (current === void 0 || !TAG.test(current)) {
25536
+ throw new Error(
25537
+ `${key} is set to ${value}, which is not a CLI-managed image. Update it yourself, or restore ${prefix}<tag> to let the CLI manage it.`
25538
+ );
25539
+ }
25540
+ if (current !== tag) {
25541
+ changes.push({ key, repository, from: current, to: tag });
25542
+ }
25543
+ return `${key}=${prefix}${tag}${lineEnding}`;
25544
+ });
25545
+ if (!found.has("EUDIPLO_IMAGE")) {
25546
+ throw new Error(
25547
+ "The env file does not define EUDIPLO_IMAGE, so there is no CLI-managed image to upgrade."
25548
+ );
25549
+ }
25550
+ return { changes, content: lines.join("\n") };
25551
+ }
25552
+ function parseVersion(tag) {
25553
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag);
25554
+ if (!match) {
25555
+ return void 0;
25556
+ }
25557
+ return {
25558
+ major: Number(match[1]),
25559
+ minor: Number(match[2]),
25560
+ patch: Number(match[3])
25561
+ };
25562
+ }
25563
+ function compareVersions2(a, b) {
25564
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
25565
+ }
25566
+ var migrationBaseUrl = "https://docs.eudiplo.dev/migration";
25567
+ function migrationNotes(from, to) {
25568
+ const fromVersion = parseVersion(from);
25569
+ const toVersion = parseVersion(to);
25570
+ if (!fromVersion || !toVersion) {
25571
+ return [
25572
+ `Cannot compare ${from} and ${to} as versions. Check the release notes and ${migrationBaseUrl} before continuing.`
25573
+ ];
25574
+ }
25575
+ const order = compareVersions2(fromVersion, toVersion);
25576
+ if (order > 0) {
25577
+ return [
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.`
25579
+ ];
25580
+ }
25581
+ const notes = [];
25582
+ for (let major = fromVersion.major + 1; major <= toVersion.major; major++) {
25583
+ notes.push(
25584
+ `Major version ${major}: read ${migrationBaseUrl}/${major - 1}.x-to-${major}.0 first.`
25585
+ );
25586
+ }
25587
+ return notes;
25588
+ }
25589
+
25590
+ // src/commands/upgrade/action.ts
25591
+ async function runUpgradeCommand(config, parsed, context) {
25592
+ const [instanceName, instance] = resolveInstance(config, parsed);
25593
+ if (instance.target !== "compose") {
25594
+ throw new Error(unsupportedCommand("upgrade", instance.target));
25595
+ }
25596
+ const options = {
25597
+ instanceName,
25598
+ instance,
25599
+ args: [],
25600
+ flags: parsed.flags,
25601
+ context
25602
+ };
25603
+ assertWritable(options);
25604
+ const tag = readStringFlag(parsed.flags, "image-tag");
25605
+ if (!tag) {
25606
+ throw new Error("upgrade requires --image-tag <tag>.");
25607
+ }
25608
+ if (!instance.envFile) {
25609
+ throw new Error(
25610
+ `Instance ${instanceName} has no env file, so its images are not managed by the CLI.`
25611
+ );
25612
+ }
25613
+ const envPath = resolve8(
25614
+ instance.projectDirectory ?? context.cwd,
25615
+ instance.envFile
25616
+ );
25617
+ const original = await readEnvFile(envPath);
25618
+ const plan = planImageUpgrade(original, tag);
25619
+ if (plan.changes.length === 0) {
25620
+ context.stdout.write(
25621
+ `${instanceName} already uses image tag ${tag}.
25622
+ `
25623
+ );
25624
+ return 0;
25625
+ }
25626
+ context.stdout.write(`Upgrade ${instanceName}:
25627
+ `);
25628
+ for (const change of plan.changes) {
25629
+ context.stdout.write(
25630
+ ` ${change.repository}: ${change.from} -> ${change.to}
25631
+ `
25632
+ );
25633
+ }
25634
+ const notes = new Set(
25635
+ plan.changes.flatMap(
25636
+ (change) => migrationNotes(change.from, change.to)
25637
+ )
25638
+ );
25639
+ for (const note of notes) {
25640
+ context.stdout.write(`Note: ${note}
25641
+ `);
25642
+ }
25643
+ if (!await confirmUpgrade(parsed, context)) {
25644
+ context.stdout.write("Upgrade cancelled. Nothing was changed.\n");
25645
+ return 0;
25646
+ }
25647
+ await writeFile6(envPath, plan.content, "utf8");
25648
+ const pulled = await runCompose(buildComposePullArgs(void 0), options);
25649
+ if (pulled !== 0) {
25650
+ await writeFile6(envPath, original, "utf8");
25651
+ context.stderr.write(
25652
+ `Pulling the new images failed. Restored the previous image tags in ${envPath}.
25653
+ `
25654
+ );
25655
+ return pulled;
25656
+ }
25657
+ const recreated = await runCompose(buildComposeUpArgs(), options);
25658
+ if (recreated !== 0) {
25659
+ context.stderr.write(
25660
+ `The new images were pulled and ${envPath} now uses ${tag}, but recreating the services failed. Fix the error above and run eudiplo up.
25661
+ `
25662
+ );
25663
+ return recreated;
25664
+ }
25665
+ context.stdout.write(`Upgraded ${instanceName} to ${tag}.
25666
+ `);
25667
+ return 0;
25668
+ }
25669
+ async function readEnvFile(path2) {
25670
+ try {
25671
+ return await readFile9(path2, "utf8");
25672
+ } catch (error) {
25673
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
25674
+ throw new Error(`Env file not found: ${path2}`);
25675
+ }
25676
+ throw error;
25677
+ }
25678
+ }
25679
+ async function confirmUpgrade(parsed, context) {
25680
+ if (parsed.flags.yes === true) {
25681
+ return true;
25682
+ }
25683
+ if (context.interactive !== true) {
25684
+ throw new Error("upgrade requires --yes in non-interactive mode.");
25685
+ }
25686
+ const prompter = createPrompter(context);
25687
+ try {
25688
+ const answer = (await prompter.ask("Continue? [y/N]: ")).trim().toLowerCase();
25689
+ return answer === "y" || answer === "yes";
25690
+ } finally {
25691
+ prompter.close();
25692
+ }
25693
+ }
25694
+
25695
+ // src/commands/upgrade/index.ts
25696
+ function createUpgradeCommand(context, setExitCode) {
25697
+ return new Command("upgrade").description(
25698
+ "Upgrade the EUDIPLO application images of a Compose deployment"
25699
+ ).requiredOption("--image-tag <tag>", "image tag to upgrade to").option("--instance <name>", "select a configured instance").option("--yes", "upgrade without asking for confirmation").action(async (options) => {
25700
+ const { config } = await loadCliState(context);
25701
+ setExitCode(
25702
+ await runUpgradeCommand(
25703
+ config,
25704
+ parsedArgs("upgrade", void 0, [], options),
25705
+ context
25706
+ )
25707
+ );
25708
+ });
25709
+ }
23127
25710
 
23128
25711
  // src/commands/version/action.ts
25712
+ import { isSea as isSea2 } from "node:sea";
23129
25713
  function versionText() {
23130
25714
  return `${package_default.name} ${package_default.version}`;
23131
25715
  }
@@ -23232,6 +25816,8 @@ function createProgram(context, setExitCode) {
23232
25816
  for (const command of createDeploymentCommands(context, setExitCode)) {
23233
25817
  program2.addCommand(command);
23234
25818
  }
25819
+ program2.addCommand(createUpgradeCommand(context, setExitCode));
25820
+ program2.addCommand(createOpenCommand(context, setExitCode));
23235
25821
  program2.addCommand(createInstanceCommand(context, setExitCode));
23236
25822
  program2.addCommand(createConfigCommand(context, setExitCode));
23237
25823
  program2.addCommand(createDoctorCommand(context, setExitCode));