hanzio 3.2.0 → 3.3.0

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.
@@ -7,8 +7,38 @@ import {
7
7
  import { parseArgs } from "node:util";
8
8
 
9
9
  // src/cli/fields.ts
10
- import { z } from "zod";
11
10
  class UsageError extends Error {
11
+ kind;
12
+ constructor(message, kind = "invalid-arguments") {
13
+ super(message);
14
+ this.kind = kind;
15
+ }
16
+ }
17
+ var optionName = (field) => field.flag.startsWith("no-") ? field.key : field.flag;
18
+ var strings = (value, what, key) => {
19
+ const list = value === undefined ? [] : Array.isArray(value) ? value : [value];
20
+ if (list.some((item) => typeof item !== "string" || !item))
21
+ throw new Error(`hanzio/cli: ${what} ${JSON.stringify(value)} of "${key}" must be a string or string[]`);
22
+ return list;
23
+ };
24
+ var kindOf = (def) => def.type === "boolean" ? "boolean" : def.type === "number" ? "number" : "string";
25
+ function choicesOf(def) {
26
+ if (def.type === "enum")
27
+ return Object.values(def.entries).map(String);
28
+ if (def.type === "literal")
29
+ return def.values.map(String);
30
+ return;
31
+ }
32
+ function optionalValueOf(def) {
33
+ if (def.type !== "union" || def.options.length !== 2)
34
+ return;
35
+ const [a, b] = def.options.map(defOf);
36
+ const flag = (d) => d.type === "boolean" || d.type === "literal" && d.values.includes(true);
37
+ if (flag(a) && kindOf(b) === "string" && b.type !== "literal")
38
+ return b;
39
+ if (flag(b) && kindOf(a) === "string" && a.type !== "literal")
40
+ return a;
41
+ return;
12
42
  }
13
43
  var kebab = (key) => key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
14
44
  function fieldsOf(schema) {
@@ -23,7 +53,7 @@ function toField(key, schema) {
23
53
  let defaultValue;
24
54
  let current = schema;
25
55
  for (;; ) {
26
- const meta = z.globalRegistry.get(current);
56
+ const meta = current.meta?.();
27
57
  if (meta)
28
58
  metas.unshift(meta);
29
59
  const def = defOf(current);
@@ -49,22 +79,23 @@ function toField(key, schema) {
49
79
  }
50
80
  def = defOf(element);
51
81
  }
52
- const alias = meta.alias;
53
- if (alias !== undefined && (typeof alias !== "string" || alias.length !== 1))
54
- throw new Error(`hanzio/cli: alias ${JSON.stringify(alias)} of "${key}" must be one character`);
82
+ const valueDef = multiple ? undefined : optionalValueOf(def);
83
+ const names = strings(meta.alias, "alias", key);
55
84
  return {
56
85
  key,
57
86
  flag: kebab(key),
58
- kind: def.type === "boolean" ? "boolean" : def.type === "number" ? "number" : "string",
87
+ kind: valueDef ? "string" : kindOf(def),
59
88
  multiple,
60
89
  positional: meta.positional === true,
61
90
  optional,
62
91
  hasDefault,
63
92
  defaultValue,
64
93
  description: typeof meta.description === "string" ? meta.description : undefined,
65
- alias,
66
- env: typeof meta.env === "string" ? meta.env : undefined,
67
- choices: def.type === "enum" ? Object.values(def.entries).map(String) : def.type === "literal" ? def.values.map(String) : undefined
94
+ alias: names.find((name) => name.length === 1),
95
+ aliases: names.filter((name) => name.length > 1).map(kebab),
96
+ env: strings(meta.env, "env", key),
97
+ optionalValue: valueDef !== undefined,
98
+ choices: choicesOf(valueDef ?? def)
68
99
  };
69
100
  }
70
101
  function coerce(field, value) {
@@ -82,7 +113,7 @@ function coerce(field, value) {
82
113
  }
83
114
  return value;
84
115
  }
85
- function readFields(fields, values, positionals, env) {
116
+ function readFields(fields, values, positionals, env, strict = true) {
86
117
  const result = {};
87
118
  const queue = [...positionals];
88
119
  for (const field of fields) {
@@ -90,11 +121,12 @@ function readFields(fields, values, positionals, env) {
90
121
  if (field.positional)
91
122
  raw = field.multiple ? queue.length ? queue.splice(0) : undefined : queue.shift();
92
123
  else
93
- raw = values[field.flag];
94
- if (raw === undefined && field.env && env[field.env] !== undefined) {
95
- const value = env[field.env];
124
+ raw = values[optionName(field)];
125
+ if (field.optionalValue && raw === "")
126
+ raw = true;
127
+ const value = raw === undefined ? field.env.map((name) => env[name]).find((value) => value?.trim()) : undefined;
128
+ if (value !== undefined)
96
129
  raw = field.multiple ? [value] : value;
97
- }
98
130
  if (raw === undefined) {
99
131
  if (field.optional || field.hasDefault)
100
132
  continue;
@@ -107,23 +139,25 @@ function readFields(fields, values, positionals, env) {
107
139
  }
108
140
  result[field.key] = Array.isArray(raw) ? raw.map((item) => coerce(field, item)) : coerce(field, raw);
109
141
  }
110
- if (queue.length)
142
+ if (strict && queue.length)
111
143
  throw new UsageError(`Unexpected argument "${queue[0]}"`);
112
144
  return result;
113
145
  }
114
146
  function fieldLabel(field) {
115
147
  if (field.positional)
116
148
  return field.multiple ? `<${field.flag}...>` : field.optional || field.hasDefault ? `[${field.flag}]` : `<${field.flag}>`;
117
- const name = field.kind === "boolean" && field.defaultValue === true ? `--[no-]${field.flag}` : `--${field.flag}`;
118
- const value = field.kind === "boolean" ? "" : ` <${field.choices?.join("|") ?? field.kind}>${field.multiple ? "..." : ""}`;
119
- return `${field.alias ? `-${field.alias}, ` : ""}${name}${value}`;
149
+ const name = field.kind === "boolean" && field.defaultValue === true && !field.flag.startsWith("no-") ? `--[no-]${field.flag}` : `--${field.flag}`;
150
+ const type = `<${field.choices?.join("|") ?? field.kind}>`;
151
+ const value = field.kind === "boolean" ? "" : field.optionalValue ? `[=${type}]` : ` ${type}${field.multiple ? "..." : ""}`;
152
+ const aliases = field.aliases.map((alias) => `, --${alias}`).join("");
153
+ return `${field.alias ? `-${field.alias}, ` : ""}${name}${aliases}${value}`;
120
154
  }
121
155
  function fieldHelp(field) {
122
156
  const showDefault = field.hasDefault && field.defaultValue !== undefined && !(field.kind === "boolean" && field.defaultValue === false);
123
157
  return [
124
158
  field.description,
125
159
  showDefault && `(default: ${JSON.stringify(field.defaultValue)})`,
126
- field.env && `[env: ${field.env}]`
160
+ field.env.length > 0 && `[env: ${field.env.join(", ")}]`
127
161
  ].filter(Boolean).join(" ");
128
162
  }
129
163
  function closest(input, candidates) {
@@ -160,6 +194,19 @@ class CliError2 extends Error {
160
194
  this.exitCode = options?.exitCode ?? 1;
161
195
  }
162
196
  }
197
+
198
+ class CliUsageError2 extends CliError2 {
199
+ kind;
200
+ path;
201
+ help;
202
+ constructor(message, details) {
203
+ super(message, { exitCode: 2 });
204
+ this.name = "CliUsageError";
205
+ this.kind = details.kind;
206
+ this.path = details.path;
207
+ this.help = details.help;
208
+ }
209
+ }
163
210
  function defineCommand2(definition) {
164
211
  return { ...definition, [commandKey]: true };
165
212
  }
@@ -174,12 +221,14 @@ async function loadCommand(node) {
174
221
  return loadCommand(command);
175
222
  }
176
223
  function listCommands(group, prefix = []) {
177
- return Object.entries(group).flatMap(([name, node]) => typeof node === "function" || isCommand(node) ? [
178
- [
179
- [...prefix, name],
180
- typeof node === "function" ? undefined : node.description
181
- ]
182
- ] : listCommands(node, [...prefix, name]));
224
+ return Object.entries(group).flatMap(([name, node]) => {
225
+ if (typeof node !== "function" && !isCommand(node))
226
+ return listCommands(node, [...prefix, name]);
227
+ const command = typeof node === "function" ? undefined : node;
228
+ const names = [...prefix, name, command?.usage].filter(Boolean).join(" ");
229
+ return [[names, command?.description?.split(`
230
+ `)[0]]];
231
+ });
183
232
  }
184
233
  function table(rows) {
185
234
  const width = Math.max(...rows.map(([left]) => left.length));
@@ -190,14 +239,20 @@ var optionRows = (fields) => fields.map((field) => [fieldLabel(field), fieldHelp
190
239
  var helpRow = ["-h, --help", "Show help"];
191
240
  function defineCli2(config) {
192
241
  const root = config.commands;
242
+ const unknownCommand = config.unknownCommand;
193
243
  const builtIn = (group) => group === root && !Object.hasOwn(group, "completion");
194
244
  const versionRow = config.version ? [["--version", "Show version"]] : [];
245
+ const section = (title, fields) => fields.length ? [`${title}:
246
+ ${table(optionRows(fields))}`] : [];
247
+ const positionalsOf = (fields) => fields.filter((field) => field.positional);
248
+ const optionsOf = (fields) => fields.filter((field) => !field.positional);
195
249
  function groupHelp(group, path, globals) {
196
- const usage = [config.name, ...path].join(" ");
197
- const commands = listCommands(group).map(([names, description]) => [
198
- names.join(" "),
199
- description
200
- ]);
250
+ const usage = [
251
+ config.name,
252
+ ...positionalsOf(globals).map(fieldLabel),
253
+ ...path
254
+ ].join(" ");
255
+ const commands = listCommands(group);
201
256
  if (builtIn(group))
202
257
  commands.push([
203
258
  "completion <bash|zsh>",
@@ -210,13 +265,15 @@ function defineCli2(config) {
210
265
  ""
211
266
  ],
212
267
  `Usage: ${usage} <command> [options]`,
268
+ ...section(`
269
+ Arguments`, positionalsOf(globals)),
213
270
  "",
214
271
  "Commands:",
215
272
  table(commands),
216
273
  "",
217
274
  "Options:",
218
275
  table([
219
- ...optionRows(globals),
276
+ ...optionRows(optionsOf(globals)),
220
277
  helpRow,
221
278
  ...path.length ? [] : versionRow
222
279
  ]),
@@ -225,34 +282,42 @@ function defineCli2(config) {
225
282
  ].join(`
226
283
  `);
227
284
  }
228
- function commandHelp(command, path, fields, globals) {
229
- const positionals = fields.filter((field) => field.positional);
230
- const options = fields.filter((field) => !field.positional);
231
- const sections = [
232
- `Usage: ${[config.name, ...path, ...positionals.map(fieldLabel)].join(" ")} [options]`
285
+ function commandHelp(command, path, globals) {
286
+ const fields = command.raw ? [] : fieldsOf(command.input);
287
+ const positionals = [...positionalsOf(globals), ...positionalsOf(fields)];
288
+ const usage = [
289
+ config.name,
290
+ ...positionalsOf(globals).map(fieldLabel),
291
+ ...path,
292
+ command.usage ?? [...positionalsOf(fields).map(fieldLabel), "[options]"]
233
293
  ];
294
+ const sections = [`Usage: ${usage.flat().join(" ")}`];
234
295
  const description = command.description ?? config.description;
235
296
  if (description)
236
297
  sections.push(description);
237
- if (positionals.length)
238
- sections.push(`Arguments:
239
- ${table(optionRows(positionals))}`);
298
+ sections.push(...section("Arguments", positionals));
240
299
  sections.push(`Options:
241
300
  ${table([
242
- ...optionRows(options),
301
+ ...optionRows(optionsOf(fields)),
243
302
  helpRow,
244
303
  ...path.length ? [] : versionRow
245
304
  ])}`);
246
- if (globals.length)
247
- sections.push(`Global options:
248
- ${table(optionRows(globals))}`);
305
+ sections.push(...section("Global options", optionsOf(globals)));
306
+ if (command.examples?.length)
307
+ sections.push(`Examples:
308
+ ${command.examples.map((line) => ` ${line}`).join(`
309
+ `)}`);
249
310
  return sections.join(`
250
311
 
251
312
  `);
252
313
  }
314
+ async function helpText(node, path, globals) {
315
+ const text = isCommand(node) ? commandHelp(node, path, globals) : groupHelp(node, path, globals);
316
+ return config.help ? config.help(text, { path }) : text;
317
+ }
253
318
  async function completion(shell, globals) {
254
319
  const fn = `_${config.name.replace(/\W/g, "_")}_completion`;
255
- const flags = (fields) => [...fields, ...globals].filter((field) => !field.positional).map((field) => `--${field.flag}`);
320
+ const flags = (fields) => optionsOf([...fields, ...globals]).map((field) => `--${field.flag}`);
256
321
  const cases = [];
257
322
  const visit = async (node, path) => {
258
323
  if (typeof node === "function" || isCommand(node)) {
@@ -301,69 +366,112 @@ ${script}`;
301
366
  const env = io.env ?? process.env;
302
367
  let usage = config.name;
303
368
  let json = false;
369
+ let globalFields = [];
370
+ let current = {
371
+ node: {},
372
+ path: []
373
+ };
304
374
  try {
305
- const globalFields = fieldsOf(config.globals);
306
- const positional = globalFields.find((field) => field.positional);
307
- if (positional)
308
- throw new Error(`hanzio/cli: global "${positional.key}" can't be positional`);
309
- const valueFlags = new Set(globalFields.filter((field) => field.kind !== "boolean").flatMap((field) => [
375
+ globalFields = fieldsOf(config.globals);
376
+ const globalPositionals = positionalsOf(globalFields);
377
+ const array = globalPositionals.find((field) => field.multiple);
378
+ if (array)
379
+ throw new Error(`hanzio/cli: positional global "${array.key}" can't be an array`);
380
+ const longNames = (field) => [
310
381
  `--${field.flag}`,
382
+ ...field.aliases.map((alias) => `--${alias}`)
383
+ ];
384
+ const names = (field) => [
385
+ ...longNames(field),
311
386
  ...field.alias ? [`-${field.alias}`] : []
387
+ ];
388
+ const globalOptions = optionsOf(globalFields);
389
+ const valueFlags = new Set(globalOptions.filter((field) => field.kind !== "boolean" && !field.optionalValue).flatMap(names));
390
+ const globalFlags = new Set(globalOptions.flatMap((field) => [
391
+ ...names(field),
392
+ `--no-${field.flag}`
312
393
  ]));
313
394
  const isHelp = (token) => token === "--help" || token === "-h";
395
+ const isVersion = (token) => token === "--version" || token === "-v" && !globalOptions.some((field) => field.alias === "v");
396
+ const isFlag = (token) => token.startsWith("-") && token !== "-";
397
+ const args = [...argv];
398
+ let defaultCommand = typeof config.defaultCommand === "function" ? config.defaultCommand(env) : config.defaultCommand;
399
+ let injected = -1;
314
400
  let node = root;
315
401
  const path = [];
316
402
  const pre = [];
403
+ const positionals = [];
317
404
  let index = 0;
318
405
  while (typeof node !== "function" && !isCommand(node)) {
319
406
  const group = node;
320
- const token = argv[index];
321
- if (token === undefined || token === "--") {
322
- if (pre.some(isHelp)) {
323
- stdout.write(`${groupHelp(group, path, globalFields)}
407
+ current = { node: group, path: [...path] };
408
+ const token = args[index];
409
+ const end = token === undefined || token === "--";
410
+ if (end && pre.some(isHelp)) {
411
+ stdout.write(`${await helpText(group, path, globalFields)}
324
412
  `);
325
- return 0;
413
+ return 0;
414
+ }
415
+ if (end && !path.length && pre.some(isVersion)) {
416
+ if (Object.hasOwn(group, "version")) {
417
+ pre.splice(0, pre.length, ...pre.filter((t) => !isVersion(t)));
418
+ args.splice(index, 0, "version");
419
+ injected = index;
420
+ continue;
326
421
  }
327
- if (!path.length && config.version && pre.includes("--version")) {
422
+ if (config.version) {
328
423
  stdout.write(`${config.version}
329
424
  `);
330
425
  return 0;
331
426
  }
427
+ }
428
+ if (!path.length && defaultCommand !== undefined && (end || isFlag(token) && !isHelp(token) && !isVersion(token) && !globalFlags.has(token.split("=")[0]))) {
429
+ args.splice(index, 0, defaultCommand);
430
+ injected = index;
431
+ defaultCommand = undefined;
432
+ continue;
433
+ }
434
+ if (end)
332
435
  throw new UsageError(`${path.length ? `${usage}: missing` : "Missing"} command
333
436
 
334
- ${groupHelp(group, path, globalFields)}`);
335
- }
437
+ ${await helpText(group, path, globalFields)}`, "missing-command");
336
438
  index++;
337
- if (token.startsWith("-") && token !== "-") {
439
+ if (isFlag(token)) {
338
440
  pre.push(token);
339
- if (valueFlags.has(token) && argv[index] !== undefined)
340
- pre.push(argv[index++]);
441
+ if (valueFlags.has(token) && args[index] !== undefined)
442
+ pre.push(args[index++]);
341
443
  continue;
342
444
  }
343
- if (token === "completion" && builtIn(group)) {
344
- const [shell = "bash"] = argv.slice(index);
445
+ if (token === "completion" && builtIn(group) && !positionals.length) {
446
+ const [shell = "bash"] = args.slice(index);
345
447
  stdout.write(await completion(shell, globalFields));
346
448
  return 0;
347
449
  }
348
- const child = Object.hasOwn(group, token) ? group[token] : undefined;
450
+ if (!path.length && index - 1 !== injected && positionals.length < globalPositionals.length) {
451
+ positionals.push(token);
452
+ usage = `${usage} ${token}`;
453
+ continue;
454
+ }
455
+ const child = (Object.hasOwn(group, token) ? group[token] : undefined) ?? await unknownCommand?.(token, path);
349
456
  if (!child) {
350
457
  const names = Object.keys(group);
351
458
  if (builtIn(group))
352
459
  names.push("completion");
353
460
  const guess = closest(token, names);
354
- throw new UsageError(`Unknown command "${[...path, token].join(" ")}".${guess ? ` Did you mean "${[...path, guess].join(" ")}"?` : ` Commands: ${names.join(", ")}`}`);
461
+ throw new UsageError(`Unknown command "${[...path, token].join(" ")}".${guess ? ` Did you mean "${[...path, guess].join(" ")}"?` : ` Commands: ${names.join(", ")}`}`, "unknown-command");
355
462
  }
356
463
  node = child;
357
464
  path.push(token);
358
465
  usage = `${usage} ${token}`;
359
466
  }
360
467
  const command = await loadCommand(node);
361
- const rest = argv.slice(index);
468
+ current = { node: command, path };
469
+ const rest = args.slice(index);
362
470
  const fields = command.raw ? [] : fieldsOf(command.input);
363
471
  const dash = rest.indexOf("--");
364
472
  const tokens = command.raw ? pre : [...pre, ...dash < 0 ? rest : rest.slice(0, dash)];
365
473
  if (!command.raw && tokens.some(isHelp)) {
366
- stdout.write(`${commandHelp(command, path, fields, globalFields)}
474
+ stdout.write(`${await helpText(command, path, globalFields)}
367
475
  `);
368
476
  return 0;
369
477
  }
@@ -373,37 +481,50 @@ ${groupHelp(group, path, globalFields)}`);
373
481
  return 0;
374
482
  }
375
483
  const options = {};
376
- for (const field of [...globalFields, ...fields]) {
377
- if (field.positional)
378
- continue;
379
- if (field.flag === "help" || Object.hasOwn(options, field.flag))
380
- throw new Error(`hanzio/cli: --${field.flag} is declared twice`);
381
- options[field.flag] = {
484
+ const canonical = new Map;
485
+ const all = optionsOf([...globalFields, ...fields]);
486
+ for (const field of all) {
487
+ const name = optionName(field);
488
+ for (const long of longNames(field)) {
489
+ if (long === "--help" || canonical.has(long))
490
+ throw new Error(`hanzio/cli: ${long} is declared twice`);
491
+ canonical.set(long, field);
492
+ }
493
+ options[name] = {
382
494
  type: field.kind === "boolean" ? "boolean" : "string",
383
495
  multiple: field.multiple,
384
496
  ...field.alias ? { short: field.alias } : {}
385
497
  };
386
498
  }
499
+ const rewritten = tokens.map((token) => {
500
+ const eq = token.indexOf("=");
501
+ const field = canonical.get(eq < 0 ? token : token.slice(0, eq));
502
+ if (!field)
503
+ return token;
504
+ const value = eq < 0 ? field.optionalValue ? "=" : "" : token.slice(eq);
505
+ return `--${optionName(field)}${value}`;
506
+ });
507
+ const strict = command.strict !== false;
387
508
  let parsed;
388
509
  try {
389
510
  parsed = parseArgs({
390
- args: tokens,
511
+ args: rewritten,
391
512
  options,
392
513
  allowPositionals: true,
393
514
  allowNegative: true,
394
- strict: true
515
+ strict
395
516
  });
396
517
  } catch (error) {
397
518
  const message = error instanceof Error ? error.message : String(error);
398
519
  const unknown = /^Unknown option '(-[^']*)'/.exec(message)?.[1];
399
520
  if (!unknown)
400
521
  throw new UsageError(message.split(". ")[0] ?? message);
401
- const guess = closest(unknown.replace(/^--?/, ""), Object.keys(options));
522
+ const guess = closest(unknown.replace(/^--?/, ""), all.flatMap((field) => [field.flag, ...field.aliases]));
402
523
  throw new UsageError(`Unknown option ${unknown}.${guess ? ` Did you mean --${guess}?` : ""}`);
403
524
  }
404
525
  const issues = [];
405
526
  const validate = async (schema, fields, positionals) => {
406
- const raw = readFields(fields, parsed.values, positionals, env);
527
+ const raw = readFields(fields, parsed.values, positionals, env, strict);
407
528
  if (!schema)
408
529
  return raw;
409
530
  const result = await schema.safeParseAsync(raw);
@@ -416,7 +537,7 @@ ${groupHelp(group, path, globalFields)}`);
416
537
  }
417
538
  return raw;
418
539
  };
419
- const globals = await validate(config.globals, globalFields, []);
540
+ const globals = await validate(config.globals, globalFields, positionals);
420
541
  const input = await validate(command.raw ? undefined : command.input, fields, parsed.positionals);
421
542
  if (issues.length)
422
543
  throw new UsageError(issues.join(`
@@ -447,20 +568,39 @@ ${groupHelp(group, path, globalFields)}`);
447
568
  await output(result, ctx);
448
569
  return ctx.exitCode;
449
570
  } catch (error) {
450
- if (error instanceof UsageError) {
451
- stderr.write(`${error.message}
571
+ const failure = error instanceof UsageError ? new CliUsageError2(error.message, {
572
+ kind: error.kind,
573
+ path: current.path,
574
+ help: config.onError ? `${await helpText(current.node, current.path, globalFields)}
575
+ ` : ""
576
+ }) : error;
577
+ let printed;
578
+ const print = () => {
579
+ if (printed !== undefined)
580
+ return printed;
581
+ const message = failure instanceof Error ? failure.message : String(failure);
582
+ if (failure instanceof CliUsageError2)
583
+ stderr.write(`${message}
452
584
  Run "${usage} --help" for usage.
453
585
  `);
454
- return 2;
455
- }
456
- const message = error instanceof Error ? error.message : String(error);
457
- if (json)
458
- stderr.write(`${JSON.stringify({ error: message })}
586
+ else if (json)
587
+ stderr.write(`${JSON.stringify({ error: message })}
459
588
  `);
460
- else
461
- stderr.write(`${error instanceof Error && !(error instanceof CliError2) ? error.stack ?? message : message}
589
+ else
590
+ stderr.write(`${failure instanceof Error && !(failure instanceof CliError2) ? failure.stack ?? message : message}
462
591
  `);
463
- return error instanceof CliError2 ? error.exitCode : 1;
592
+ printed = failure instanceof CliError2 ? failure.exitCode : 1;
593
+ return printed;
594
+ };
595
+ if (!config.onError)
596
+ return print();
597
+ return await config.onError(failure, {
598
+ stdout,
599
+ stderr,
600
+ env,
601
+ json,
602
+ print
603
+ }) ?? print();
464
604
  }
465
605
  }
466
606
  };
@@ -472,4 +612,4 @@ function printResult(result, ctx) {
472
612
  `);
473
613
  }
474
614
 
475
- export { CliError2, defineCommand2, defineCli2 };
615
+ export { CliError2, CliUsageError2, defineCommand2, defineCli2 };
@@ -0,0 +1,135 @@
1
+ import {
2
+ utf8Encode2,
3
+ utf8Decode2,
4
+ encodeBase64Url2,
5
+ decodeBase64Url2,
6
+ toBytes,
7
+ randomBytes2,
8
+ getSubtleCrypto
9
+ } from "./index-rp5pqghz.js";
10
+
11
+ // src/crypto/sealer.ts
12
+ var ERROR_MESSAGES = {
13
+ malformed: "Malformed sealed value",
14
+ unsupported_version: "Unsupported sealed value version",
15
+ decrypt_failed: "Could not open sealed value: wrong key or tampered data"
16
+ };
17
+
18
+ class SealerError2 extends Error {
19
+ code;
20
+ constructor(code) {
21
+ super(ERROR_MESSAGES[code]);
22
+ this.name = "SealerError";
23
+ this.code = code;
24
+ }
25
+ }
26
+ var PREFIX = "enc:v1:";
27
+ var IV_BYTES = 12;
28
+ var TAG_BYTES = 16;
29
+ var DEFAULT_INFO = "hanzio.sealer.aes-256-gcm.v1";
30
+ function sealerKeyInputs2(options) {
31
+ const secret = toBytes(options.secret);
32
+ if (secret.length === 0) {
33
+ throw new TypeError("Sealer secret must not be empty");
34
+ }
35
+ const contexts = [options, ...options.fallbacks ?? []].map(({ salt = "", info = DEFAULT_INFO }) => ({ salt, info }));
36
+ return { secret, contexts };
37
+ }
38
+ function formatEnvelope2(iv, tag, ciphertext) {
39
+ return `${PREFIX}${encodeBase64Url2(iv)}.${encodeBase64Url2(tag)}.${encodeBase64Url2(ciphertext)}`;
40
+ }
41
+ function parseEnvelope2(sealed, passthroughUnsealed = false) {
42
+ if (typeof sealed !== "string" || !sealed.startsWith(PREFIX)) {
43
+ const isEnvelope = typeof sealed === "string" && /^enc:v\d+:/.test(sealed);
44
+ if (isEnvelope)
45
+ throw new SealerError2("unsupported_version");
46
+ if (passthroughUnsealed)
47
+ return null;
48
+ throw new SealerError2("malformed");
49
+ }
50
+ const parts = sealed.slice(PREFIX.length).split(".");
51
+ let envelope;
52
+ try {
53
+ if (parts.length !== 3)
54
+ throw new Error;
55
+ envelope = {
56
+ iv: decodeBase64Url2(parts[0]),
57
+ tag: decodeBase64Url2(parts[1]),
58
+ ciphertext: decodeBase64Url2(parts[2])
59
+ };
60
+ } catch (_error) {
61
+ throw new SealerError2("malformed");
62
+ }
63
+ if (envelope.iv.length !== IV_BYTES || envelope.tag.length !== TAG_BYTES) {
64
+ throw new SealerError2("malformed");
65
+ }
66
+ return envelope;
67
+ }
68
+ function jsonToSeal2(value) {
69
+ const json = JSON.stringify(value);
70
+ if (json === undefined) {
71
+ throw new TypeError("sealJson value is not JSON-serializable");
72
+ }
73
+ return json;
74
+ }
75
+ function createSealer2(options) {
76
+ const { secret, contexts } = sealerKeyInputs2(options);
77
+ const keys = [];
78
+ const getKey = (index) => {
79
+ let key = keys[index];
80
+ if (!key) {
81
+ const context = contexts[index];
82
+ key = deriveKey(secret, context.salt, context.info);
83
+ keys[index] = key;
84
+ key.catch(() => {
85
+ keys[index] = undefined;
86
+ });
87
+ }
88
+ return key;
89
+ };
90
+ const seal = async (plaintext) => {
91
+ const iv = randomBytes2(IV_BYTES);
92
+ const encrypted = new Uint8Array(await getSubtleCrypto().encrypt({ name: "AES-GCM", iv }, await getKey(0), utf8Encode2(plaintext)));
93
+ const tagStart = encrypted.length - TAG_BYTES;
94
+ return formatEnvelope2(iv, encrypted.subarray(tagStart), encrypted.subarray(0, tagStart));
95
+ };
96
+ const openEnvelope = async ({ iv, tag, ciphertext }) => {
97
+ const encrypted = new Uint8Array(ciphertext.length + TAG_BYTES);
98
+ encrypted.set(ciphertext);
99
+ encrypted.set(tag, ciphertext.length);
100
+ for (let index = 0;index < contexts.length; index++) {
101
+ const key = await getKey(index);
102
+ try {
103
+ return utf8Decode2(await getSubtleCrypto().decrypt({ name: "AES-GCM", iv }, key, encrypted));
104
+ } catch (_error) {}
105
+ }
106
+ throw new SealerError2("decrypt_failed");
107
+ };
108
+ const open = async (sealed) => {
109
+ const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
110
+ return envelope ? openEnvelope(envelope) : sealed;
111
+ };
112
+ return {
113
+ seal,
114
+ open,
115
+ sealJson: async (value) => seal(jsonToSeal2(value)),
116
+ async openJson(sealed) {
117
+ const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
118
+ return envelope ? JSON.parse(await openEnvelope(envelope)) : sealed;
119
+ }
120
+ };
121
+ }
122
+ async function deriveKey(secret, salt, info) {
123
+ const subtle = getSubtleCrypto();
124
+ const baseKey = await subtle.importKey("raw", secret, "HKDF", false, [
125
+ "deriveKey"
126
+ ]);
127
+ return subtle.deriveKey({
128
+ name: "HKDF",
129
+ hash: "SHA-256",
130
+ salt: utf8Encode2(salt),
131
+ info: utf8Encode2(info)
132
+ }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
133
+ }
134
+
135
+ export { SealerError2, sealerKeyInputs2, formatEnvelope2, parseEnvelope2, jsonToSeal2, createSealer2 };