@stll/anonymize-cli 2.2.0 → 2.4.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.
package/README.md CHANGED
@@ -47,6 +47,27 @@ anonymize -d key.json reply.txt
47
47
  Run `anonymize --help` for the full reference, including the
48
48
  `--json` schema and exit codes.
49
49
 
50
+ ## PDF workflows
51
+
52
+ PDF anonymization uses locally installed Poppler and Tesseract, runs one
53
+ explicit OCR language pack, and writes a verified fresh image-only document.
54
+ It never overlays black boxes on retained source content and refuses to
55
+ overwrite the input, a symlink input, or an existing output.
56
+
57
+ ```bash
58
+ anonymize pdf anonymize contract.pdf \
59
+ --output contract.anonymized.pdf \
60
+ --ocr-language eng \
61
+ --languages en \
62
+ --countries GB \
63
+ --json
64
+ ```
65
+
66
+ The raster output intentionally loses searchability, accessibility, links,
67
+ forms, signatures, metadata, attachments, and other interactive PDF features.
68
+ Its certificate verifies structure and rewritten pixels but cannot prove
69
+ perfect OCR or detector recall; `piiCleanGuaranteed` is always false.
70
+
50
71
  ## DOCX workflows
51
72
 
52
73
  DOCX anonymization preserves supported document structure and stores reversible
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import * as anonymize from "@stll/anonymize";
3
3
  import { ALL_DICTIONARY_IDS, DICTIONARY_META, loadCityDictionary, loadDictionary, loadNameDictionaries } from "@stll/anonymize-data";
4
4
  import { availableParallelism } from "node:os";
5
5
  import { parseArgs } from "node:util";
6
- import { realpathSync } from "node:fs";
6
+ import { constants, realpathSync } from "node:fs";
7
7
  import { link, lstat, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
8
8
  import { basename, dirname, join, relative, resolve } from "node:path";
9
9
  import { createInterface } from "node:readline/promises";
@@ -11,6 +11,7 @@ import { CAPABILITY_MANIFEST } from "@stll/anonymize/capabilities";
11
11
  import { DEFAULT_ENTITY_LABELS, ENTITY_LABELS } from "@stll/anonymize/constants";
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { DOCX_COVERAGE_MODES, anonymizeDocx, restoreDocxText } from "@stll/anonymize-docx";
14
+ import { PDF_DOCUMENT_MAX_BYTES, anonymizePdfRaster, renderPdfWithPopplerTesseract } from "@stll/anonymize-pdf";
14
15
  //#region src/args.ts
15
16
  const CLI_MODES = ["replace", "redact"];
16
17
  const DEFAULT_THRESHOLD = .3;
@@ -33,6 +34,10 @@ DOCX workflows:
33
34
  Run "anonymize docx --help" for structure-preserving DOCX anonymization
34
35
  and restoration with encrypted session archives.
35
36
 
37
+ PDF workflows:
38
+ Run "anonymize pdf --help" for destructive local Poppler/Tesseract PDF
39
+ anonymization into a verified fresh image-only output.
40
+
36
41
  Options:
37
42
  -o, --output <path> Output file, or directory for batch
38
43
  input (multiple files or a directory)
@@ -318,7 +323,7 @@ const loadCliDictionaries = async ({ languages, countries }) => {
318
323
  };
319
324
  //#endregion
320
325
  //#region package.json
321
- var version = "2.2.0";
326
+ var version = "2.4.0";
322
327
  //#endregion
323
328
  //#region src/docx.ts
324
329
  const DOCX_SESSION_KEY_BYTES = 32;
@@ -388,7 +393,7 @@ const parseSessionMode = (raw) => {
388
393
  if (raw === DOCX_SESSION_MODES.create || raw === DOCX_SESSION_MODES.continue) return raw;
389
394
  throw new UsageError(`--session-mode must be one of: ${Object.values(DOCX_SESSION_MODES).join(", ")}; got "${raw}"`);
390
395
  };
391
- const required = (value, flag) => {
396
+ const required$1 = (value, flag) => {
392
397
  if (value === void 0 || value.length === 0) throw new UsageError(`${flag} is required for DOCX workflows`);
393
398
  return value;
394
399
  };
@@ -398,10 +403,10 @@ const commonOptions = (values, positionals) => {
398
403
  if (inputPath === void 0) throw new UsageError("DOCX workflows require exactly one input file");
399
404
  return {
400
405
  inputPath,
401
- outputPath: required(values.output, "--output"),
402
- sessionArchivePath: required(values["session-archive"], "--session-archive"),
403
- sessionKeyPath: required(values["session-key-file"], "--session-key-file"),
404
- sessionId: required(values["session-id"], "--session-id"),
406
+ outputPath: required$1(values.output, "--output"),
407
+ sessionArchivePath: required$1(values["session-archive"], "--session-archive"),
408
+ sessionKeyPath: required$1(values["session-key-file"], "--session-key-file"),
409
+ sessionId: required$1(values["session-id"], "--session-id"),
405
410
  coverage: parseCoverage(values.coverage),
406
411
  observedAtEpochSeconds: values["observed-at"] === void 0 ? void 0 : parseEpochSeconds(values["observed-at"]),
407
412
  json: values.json === true,
@@ -487,29 +492,29 @@ const DOCX_RESTORE_CONFIG = {
487
492
  strict: true,
488
493
  options: DOCX_COMMON_PARSE_OPTIONS
489
494
  };
490
- const canonicalPath$1 = (path) => {
495
+ const canonicalPath$2 = (path) => {
491
496
  try {
492
497
  return realpathSync(path);
493
498
  } catch {
494
499
  return resolve(path);
495
500
  }
496
501
  };
497
- const sessionArchiveLockPath = (archivePath) => `${canonicalPath$1(archivePath)}${DOCX_SESSION_LOCK_SUFFIX}`;
502
+ const sessionArchiveLockPath = (archivePath) => `${canonicalPath$2(archivePath)}${DOCX_SESSION_LOCK_SUFFIX}`;
498
503
  const assertDistinctPaths = (paths) => {
499
504
  const seen = /* @__PURE__ */ new Map();
500
505
  for (const entry of paths) {
501
- const canonical = canonicalPath$1(entry.path);
506
+ const canonical = canonicalPath$2(entry.path);
502
507
  const existing = seen.get(canonical);
503
508
  if (existing !== void 0) throw new UsageError(`${entry.flag} collides with ${existing}`);
504
509
  seen.set(canonical, `${entry.flag} "${entry.path}"`);
505
510
  }
506
511
  };
507
- const isNodeError = (error, code) => error instanceof Error && "code" in error && error.code === code;
512
+ const isNodeError$1 = (error, code) => error instanceof Error && "code" in error && error.code === code;
508
513
  const assertPathDoesNotExist = async (path, flag) => {
509
514
  try {
510
515
  await lstat(path);
511
516
  } catch (error) {
512
- if (isNodeError(error, "ENOENT")) return;
517
+ if (isNodeError$1(error, "ENOENT")) return;
513
518
  throw error;
514
519
  }
515
520
  throw new UsageError(`${flag} refuses to overwrite existing path "${path}"`);
@@ -558,14 +563,14 @@ const acquireSessionArchiveLock = async (archivePath) => {
558
563
  try {
559
564
  handle = await open(lockPath, "wx", 384);
560
565
  } catch (error) {
561
- if (isNodeError(error, "EEXIST")) throw new Error(`encrypted session archive is locked by another continuation; if no process is running, remove the stale lock "${lockPath}"`);
566
+ if (isNodeError$1(error, "EEXIST")) throw new Error(`encrypted session archive is locked by another continuation; if no process is running, remove the stale lock "${lockPath}"`);
562
567
  throw error;
563
568
  }
564
569
  return { release: async () => {
565
570
  const closeResult = await captureOperationResult(handle.close());
566
571
  const unlinkResult = await captureOperationResult(unlink(lockPath));
567
572
  if (closeResult.type === "failed") throw closeResult.error;
568
- if (unlinkResult.type === "failed" && !isNodeError(unlinkResult.error, "ENOENT")) throw unlinkResult.error;
573
+ if (unlinkResult.type === "failed" && !isNodeError$1(unlinkResult.error, "ENOENT")) throw unlinkResult.error;
569
574
  } };
570
575
  };
571
576
  const readSessionKey = async (path) => {
@@ -609,11 +614,11 @@ const stageFile = async (target, content) => {
609
614
  }
610
615
  return temporary;
611
616
  };
612
- const publishNewFile = async (temporary, target, flag) => {
617
+ const publishNewFile$1 = async (temporary, target, flag) => {
613
618
  try {
614
619
  await link(temporary, target);
615
620
  } catch (error) {
616
- if (isNodeError(error, "EEXIST")) throw new UsageError(`${flag} refuses to overwrite existing path "${target}"`);
621
+ if (isNodeError$1(error, "EEXIST")) throw new UsageError(`${flag} refuses to overwrite existing path "${target}"`);
617
622
  throw error;
618
623
  }
619
624
  await removeStagedFile(temporary);
@@ -639,7 +644,7 @@ const openSession = (pipeline, command, archive, key) => pipeline.restoreEncrypt
639
644
  ...command.observedAtEpochSeconds === void 0 ? {} : { observedAtEpochSeconds: command.observedAtEpochSeconds }
640
645
  });
641
646
  const runDocxAnonymize = async (command, pipeline) => {
642
- const archivePath = command.sessionMode === DOCX_SESSION_MODES.continue ? canonicalPath$1(command.sessionArchivePath) : command.sessionArchivePath;
647
+ const archivePath = command.sessionMode === DOCX_SESSION_MODES.continue ? canonicalPath$2(command.sessionArchivePath) : command.sessionArchivePath;
643
648
  const archiveLock = command.sessionMode === DOCX_SESSION_MODES.continue ? await acquireSessionArchiveLock(archivePath) : void 0;
644
649
  let workflowResult = { type: "succeeded" };
645
650
  let lockReleaseResult = { type: "succeeded" };
@@ -660,11 +665,11 @@ const runDocxAnonymize = async (command, pipeline) => {
660
665
  const encryptedArchive = sessionArchive(session, key, command.observedAtEpochSeconds);
661
666
  documentTemporary = await stageFile(command.outputPath, result.document);
662
667
  archiveTemporary = await stageFile(archivePath, encryptedArchive);
663
- if (command.sessionMode === DOCX_SESSION_MODES.create) await publishNewFile(archiveTemporary, command.sessionArchivePath, "--session-archive");
668
+ if (command.sessionMode === DOCX_SESSION_MODES.create) await publishNewFile$1(archiveTemporary, command.sessionArchivePath, "--session-archive");
664
669
  else await publishReplacement(archiveTemporary, archivePath);
665
670
  archiveTemporary = void 0;
666
671
  try {
667
- await publishNewFile(documentTemporary, command.outputPath, "--output");
672
+ await publishNewFile$1(documentTemporary, command.outputPath, "--output");
668
673
  documentTemporary = void 0;
669
674
  } catch (error) {
670
675
  const message = error instanceof Error ? error.message : String(error);
@@ -700,7 +705,7 @@ const runDocxRestore = async (command, pipeline) => {
700
705
  if (command.coverage === DOCX_COVERAGE_MODES.requireFull && result.coverage.status === "partial") throw new Error("DOCX contains content outside the fully supported restoration coverage");
701
706
  const temporary = await stageFile(command.outputPath, result.document);
702
707
  try {
703
- await publishNewFile(temporary, command.outputPath, "--output");
708
+ await publishNewFile$1(temporary, command.outputPath, "--output");
704
709
  } catch (error) {
705
710
  await removeStagedFile(temporary);
706
711
  throw error;
@@ -734,6 +739,245 @@ const runDocxCommand = async ({ argv, preparePipeline }) => {
734
739
  await runDocxRestore(command, pipeline);
735
740
  };
736
741
  //#endregion
742
+ //#region src/pdf.ts
743
+ const PDF_HELP = `Usage:
744
+ anonymize pdf anonymize [options] <input.pdf>
745
+
746
+ Render and OCR every page locally, run stella detection, and write a verified,
747
+ fresh image-only PDF. The command never overwrites the input or an existing
748
+ output and rejects symlink inputs. Searchability, accessibility, signatures,
749
+ forms, links, metadata, attachments, and other interactive structure are
750
+ deliberately removed.
751
+
752
+ Required options:
753
+ -o, --output <path> New PDF output path
754
+ --ocr-language <pack> One installed Tesseract pack, e.g. "eng"
755
+
756
+ Provider options:
757
+ --dpi <n> Integer render DPI from 72 to 600 (default: 300)
758
+ --pdftoppm <path> Poppler executable (default: pdftoppm on PATH)
759
+ --tesseract <path> Tesseract executable (default: tesseract on PATH)
760
+ --timeout-ms <n> Per-process timeout, 100-300000 (default: 120000)
761
+ --fill-rgb <r,g,b> Destructive fill color (default: 0,0,0)
762
+
763
+ Detection options:
764
+ --labels <list> Comma-separated entity labels
765
+ --languages <list> Name-corpus languages, e.g. "cs,de,en"
766
+ --countries <list> ISO 3166-1 alpha-2 country codes
767
+ --threshold <n> Minimum confidence score 0-1 (default: 0.3)
768
+
769
+ Output options:
770
+ --json Print the aggregate verification certificate
771
+ --quiet Suppress the human-readable stderr summary
772
+ -h, --help Show this help
773
+
774
+ The OCR language is explicit and singular. The certificate proves a fresh
775
+ image-only structure and requested pixel rewrite; it does not prove perfect OCR
776
+ or detector recall and always reports piiCleanGuaranteed=false.
777
+ `;
778
+ const PDF_PARSE_CONFIG = {
779
+ allowPositionals: true,
780
+ strict: true,
781
+ options: {
782
+ output: {
783
+ type: "string",
784
+ short: "o"
785
+ },
786
+ "ocr-language": { type: "string" },
787
+ dpi: { type: "string" },
788
+ pdftoppm: { type: "string" },
789
+ tesseract: { type: "string" },
790
+ "timeout-ms": { type: "string" },
791
+ "fill-rgb": { type: "string" },
792
+ labels: { type: "string" },
793
+ languages: { type: "string" },
794
+ countries: { type: "string" },
795
+ threshold: { type: "string" },
796
+ json: { type: "boolean" },
797
+ quiet: { type: "boolean" },
798
+ help: {
799
+ type: "boolean",
800
+ short: "h"
801
+ }
802
+ }
803
+ };
804
+ const required = (value, flag) => {
805
+ if (!value) throw new UsageError(`${flag} is required for PDF anonymization`);
806
+ return value;
807
+ };
808
+ const integerOption = (value, flag, fallback, minimum, maximum) => {
809
+ if (value === void 0) return fallback;
810
+ const parsed = Number(value);
811
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) throw new UsageError(`${flag} must be an integer from ${minimum} to ${maximum}`);
812
+ return parsed;
813
+ };
814
+ const thresholdOption = (value) => {
815
+ if (value === void 0) return .3;
816
+ const parsed = Number(value);
817
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) throw new UsageError("--threshold must be a number from 0 to 1");
818
+ return parsed;
819
+ };
820
+ const listOption = (value) => value === void 0 ? void 0 : [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
821
+ const fillOption = (value) => {
822
+ if (value === void 0) return [
823
+ 0,
824
+ 0,
825
+ 0
826
+ ];
827
+ const channels = value.split(",").map(Number);
828
+ if (channels.length !== 3 || channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) throw new UsageError("--fill-rgb must contain three integers from 0 to 255");
829
+ return [
830
+ channels[0] ?? 0,
831
+ channels[1] ?? 0,
832
+ channels[2] ?? 0
833
+ ];
834
+ };
835
+ const parsePdfCommand = (argv) => {
836
+ const action = argv.at(0);
837
+ if (action === void 0 || action === "--help" || action === "-h") return { type: "help" };
838
+ if (action !== "anonymize") throw new UsageError(`unknown PDF action "${action}"; expected "anonymize"`);
839
+ let parsed;
840
+ try {
841
+ parsed = parseArgs({
842
+ ...PDF_PARSE_CONFIG,
843
+ args: argv.slice(1)
844
+ });
845
+ } catch (error) {
846
+ throw new UsageError(error instanceof Error ? error.message : String(error));
847
+ }
848
+ if (parsed.values.help === true) return { type: "help" };
849
+ if (parsed.positionals.length !== 1 || parsed.positionals[0] === void 0) throw new UsageError("PDF anonymization requires exactly one input file");
850
+ return {
851
+ type: "anonymize",
852
+ inputPath: parsed.positionals[0],
853
+ outputPath: required(parsed.values.output, "--output"),
854
+ ocrLanguage: required(parsed.values["ocr-language"], "--ocr-language"),
855
+ dpi: integerOption(parsed.values.dpi, "--dpi", 300, 72, 600),
856
+ timeoutMs: integerOption(parsed.values["timeout-ms"], "--timeout-ms", 12e4, 100, 3e5),
857
+ pdftoppmPath: parsed.values.pdftoppm,
858
+ tesseractPath: parsed.values.tesseract,
859
+ fillRgb: fillOption(parsed.values["fill-rgb"]),
860
+ detection: {
861
+ labels: listOption(parsed.values.labels),
862
+ languages: listOption(parsed.values.languages),
863
+ countries: parsed.values.countries === void 0 ? void 0 : parseCountries(parsed.values.countries),
864
+ threshold: thresholdOption(parsed.values.threshold)
865
+ },
866
+ json: parsed.values.json === true,
867
+ quiet: parsed.values.quiet === true
868
+ };
869
+ };
870
+ const canonicalPath$1 = (path) => {
871
+ try {
872
+ return realpathSync(path);
873
+ } catch {
874
+ return resolve(path);
875
+ }
876
+ };
877
+ const isNodeError = (error, code) => error instanceof Error && "code" in error && error.code === code;
878
+ const preflight = async (command) => {
879
+ if (canonicalPath$1(command.inputPath) === canonicalPath$1(command.outputPath)) throw new UsageError("--output must not overwrite the PDF input");
880
+ let input;
881
+ try {
882
+ input = await lstat(command.inputPath);
883
+ } catch (error) {
884
+ if (isNodeError(error, "ENOENT")) throw new UsageError("PDF input must be a regular non-symlink file");
885
+ throw error;
886
+ }
887
+ if (!input.isFile() || input.isSymbolicLink()) throw new UsageError("PDF input must be a regular non-symlink file");
888
+ try {
889
+ await lstat(command.outputPath);
890
+ } catch (error) {
891
+ if (isNodeError(error, "ENOENT")) return;
892
+ throw error;
893
+ }
894
+ throw new UsageError("--output refuses to overwrite an existing path");
895
+ };
896
+ const readRegularInput = async (path) => {
897
+ let handle;
898
+ try {
899
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
900
+ } catch (error) {
901
+ if (isNodeError(error, "ELOOP")) throw new UsageError("PDF input must be a regular non-symlink file");
902
+ throw error;
903
+ }
904
+ try {
905
+ const opened = await handle.stat();
906
+ if (!opened.isFile() || !Number.isSafeInteger(opened.size) || opened.size > PDF_DOCUMENT_MAX_BYTES) throw new UsageError(`PDF input must be a regular file no larger than ${PDF_DOCUMENT_MAX_BYTES} bytes`);
907
+ const document = Buffer.allocUnsafe(opened.size);
908
+ let offset = 0;
909
+ while (offset < document.length) {
910
+ const { bytesRead } = await handle.read(document, offset, document.length - offset, offset);
911
+ if (bytesRead === 0) throw new UsageError("PDF input changed while it was being read");
912
+ offset += bytesRead;
913
+ }
914
+ const sentinel = Buffer.allocUnsafe(1);
915
+ const { bytesRead: trailingBytes } = await handle.read(sentinel, 0, 1, offset);
916
+ const current = await lstat(path);
917
+ if (trailingBytes !== 0 || !current.isFile() || current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) throw new UsageError("PDF input changed during validation");
918
+ return document;
919
+ } finally {
920
+ await handle.close();
921
+ }
922
+ };
923
+ const removeStaged = async (path) => {
924
+ try {
925
+ await unlink(path);
926
+ } catch (error) {
927
+ if (!isNodeError(error, "ENOENT")) throw error;
928
+ }
929
+ };
930
+ const publishNewFile = async (target, content) => {
931
+ const temporary = join(dirname(target), `.${basename(target)}.${randomUUID()}.tmp`);
932
+ const handle = await open(temporary, "wx", 384);
933
+ try {
934
+ await handle.writeFile(content);
935
+ await handle.sync();
936
+ await handle.close();
937
+ try {
938
+ await link(temporary, target);
939
+ } catch (error) {
940
+ if (isNodeError(error, "EEXIST")) throw new UsageError("--output refuses to overwrite an existing path");
941
+ throw error;
942
+ }
943
+ } catch (error) {
944
+ try {
945
+ await handle.close();
946
+ } catch {}
947
+ throw error;
948
+ } finally {
949
+ await removeStaged(temporary);
950
+ }
951
+ };
952
+ const runPdfCommand = async ({ argv, preparePipeline }) => {
953
+ const command = parsePdfCommand(argv);
954
+ if (command.type === "help") {
955
+ process.stdout.write(PDF_HELP);
956
+ return;
957
+ }
958
+ await preflight(command);
959
+ const document = await readRegularInput(command.inputPath);
960
+ const pipeline = await preparePipeline({ detection: command.detection });
961
+ const observed = await renderPdfWithPopplerTesseract({
962
+ document,
963
+ ocrLanguage: command.ocrLanguage,
964
+ dpi: command.dpi,
965
+ timeoutMs: command.timeoutMs,
966
+ pdftoppmPath: command.pdftoppmPath,
967
+ tesseractPath: command.tesseractPath
968
+ });
969
+ const result = anonymizePdfRaster({
970
+ document,
971
+ pipeline,
972
+ provider: observed.provider,
973
+ pages: observed.pages,
974
+ fillRgb: command.fillRgb
975
+ });
976
+ await publishNewFile(command.outputPath, result.document);
977
+ if (command.json) process.stdout.write(`${JSON.stringify(result.certificate, null, 2)}\n`);
978
+ if (!command.quiet) process.stderr.write(`anonymize: PDF anonymized: ${result.certificate.pageCount} pages, ${result.certificate.detectionCount} detections, PII-clean guarantee=false\n`);
979
+ };
980
+ //#endregion
737
981
  //#region src/main.ts
738
982
  const cliVersion = () => version;
739
983
  /**
@@ -1211,6 +1455,13 @@ const dispatch = async (engine) => {
1211
1455
  });
1212
1456
  return;
1213
1457
  }
1458
+ if (argv.at(0) === "pdf") {
1459
+ await runPdfCommand({
1460
+ argv: argv.slice(1),
1461
+ preparePipeline: async (request) => prepareNativeCliPipeline(engine.api, await buildPipelineConfig(request.detection, engine.loadDictionaries))
1462
+ });
1463
+ return;
1464
+ }
1214
1465
  const opts = parseCliArgs(argv);
1215
1466
  if (opts.help) {
1216
1467
  process.stdout.write(HELP);
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":["splitList","parseThreshold","canonicalPath","pkg.version","known"],"sources":["../src/args.ts","../src/dictionary-scope.ts","../src/dictionaries.ts","../package.json","../src/docx.ts","../src/main.ts","../src/cli.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { parseArgs } from \"node:util\";\n\nexport const CLI_MODES = [\"replace\", \"redact\"] as const;\nexport type CliMode = (typeof CLI_MODES)[number];\n\nexport const DEFAULT_THRESHOLD = 0.3;\nexport const DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/** Upper bound on the default worker count; batch I/O overlap\n * saturates well before this, and redaction itself is a\n * synchronous native call serialized on the JS thread. */\nexport const MAX_DEFAULT_WORKERS = 4;\n\n/** Default batch concurrency: min(4, cores). Workers overlap\n * file reads/writes; the shared native pipeline runs each\n * redaction to completion on the single JS thread. */\nexport const defaultWorkerCount = (): number =>\n Math.max(1, Math.min(MAX_DEFAULT_WORKERS, availableParallelism()));\n\n/** Invalid invocation; printed with usage hint, exit code 2. */\nexport class UsageError extends Error {}\n\nexport type CliOptions = {\n files: string[];\n output?: string | undefined;\n mode: CliMode;\n keyPath?: string | undefined;\n deanonymiseKeyPath?: string | undefined;\n revert?: string[] | undefined;\n recursive: boolean;\n workers: number;\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n redactString: string;\n json: boolean;\n quiet: boolean;\n help: boolean;\n version: boolean;\n listLabels: boolean;\n capabilities: boolean;\n};\n\nexport const HELP = `Usage: anonymize [options] [file|dir ...]\n\nDetect and anonymize PII in text. Reads the given files, or stdin\nwhen no files are given. A directory argument processes the text\nfiles inside it (add --recursive to descend into subdirectories).\nWrites to stdout, or to --output.\nAll processing is local; the CLI makes no network calls.\n\nDOCX workflows:\n Run \"anonymize docx --help\" for structure-preserving DOCX anonymization\n and restoration with encrypted session archives.\n\nOptions:\n -o, --output <path> Output file, or directory for batch\n input (multiple files or a directory)\n -m, --mode <mode> \"replace\" (reversible [PERSON_1]\n placeholders) or \"redact\"\n (default: replace)\n -k, --key <path> Write the redaction key as JSON\n (single input, replace mode)\n -d, --deanonymise <path> Restore redacted text using the\n redaction key at <path>\n --revert <term> With --deanonymise, restore only the\n given entity. Match a placeholder token\n (\"[PERSON_1]\") or an original value\n (\"Jan Novák\"), case-sensitive exact.\n Repeatable; others stay redacted\n -r, --recursive Descend into subdirectories when a\n directory is given as input\n --workers <n> Batch files to process concurrently\n (default: min(${MAX_DEFAULT_WORKERS}, CPU cores)). Overlaps\n file I/O; redaction is serialized on\n the JS thread\n --labels <list> Comma-separated entity labels to detect\n (default: all). Accepts canonical labels\n (\"email address\"), short aliases (email,\n phone, org, dob, ssn), and hyphen/underscore\n forms (\"credit-card-number\")\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n (default: all bundled)\n --countries <list> ISO 3166-1 alpha-2 codes scoping deny\n lists and city data, e.g. \"CZ,DE,GB\"\n (default: all deny lists; city data\n for a 30-country default set)\n --threshold <n> Minimum confidence score, 0-1\n (default: ${DEFAULT_THRESHOLD})\n --redact-string <s> Replacement text in redact mode\n (default: \"${DEFAULT_REDACT_STRING}\")\n --json Emit JSON (entities + redacted text) to\n stdout (single input only)\n --quiet Suppress the summary on stderr\n -h, --help Show this help\n -v, --version Show the version\n --list-labels List detectable entity labels and the\n short aliases accepted by --labels\n --capabilities Emit the versioned capability manifest as JSON\n\nBatch input (directory or multiple files):\n Requires --output <directory>. The input tree is mirrored\n into the output directory. Directory walks process regular\n files only and skip likely-binary files (a NUL byte in the\n first 8 KiB); explicitly named files are always processed.\n The stderr summary reports how many files were processed,\n failed, and skipped; any failure sets exit code 1.\n --key and --json apply to single inputs only.\n\nInteractive prompt:\n When run on files from a terminal without --countries or\n --languages, the CLI asks once which country scope to load.\n Piped stdin/stderr or --quiet skips the prompt, so scripts\n and agents never block on input.\n\nExit codes:\n 0 success\n 1 runtime error (message on stderr)\n 2 usage error (message on stderr)\n\nJSON output (--json):\n { \"entityCount\": number,\n \"entities\": [{ \"start\": number, \"end\": number,\n \"label\": string, \"text\": string,\n \"score\": number, \"source\": string }],\n \"redactedText\": string }\n Offsets are UTF-16 code-unit indexes into the input.\n The stderr summary contains entity counts only, never\n the detected text.\n\nExamples:\n anonymize contract.txt > contract.anon.txt\n anonymize -k contract.key.json -o contract.anon.txt contract.txt\n anonymize -d contract.key.json contract.anon.txt\n anonymize -r --workers 8 -o out/ docs/\n anonymize -d key.json --revert \"[PERSON_1]\" contract.anon.txt\n cat notes.md | anonymize --countries CZ,SK --languages cs,sk\n anonymize --json --quiet input.txt | jq '.entities[].label'\n anonymize docx --help\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseWorkers = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 1) {\n throw new UsageError(`--workers must be a positive integer, got \"${raw}\"`);\n }\n return value;\n};\n\nconst parseMode = (raw: string): CliMode => {\n const mode = CLI_MODES.find((candidate) => candidate === raw);\n if (!mode) {\n throw new UsageError(\n `--mode must be one of: ${CLI_MODES.join(\", \")}; got \"${raw}\"`,\n );\n }\n return mode;\n};\n\nconst COUNTRY_CODE_RE = /^[A-Za-z]{2}$/;\n\nexport const parseCountries = (raw: string): string[] => {\n const countries = [\n ...new Set(splitList(raw).map((code) => code.toUpperCase())),\n ];\n const invalid = countries.find((code) => !COUNTRY_CODE_RE.test(code));\n if (invalid) {\n throw new UsageError(\n `--countries expects ISO 3166-1 alpha-2 codes (e.g. \"CZ,DE\"), got \"${invalid}\"`,\n );\n }\n return countries;\n};\n\nexport const parseCliArgs = (argv: string[]): CliOptions => {\n let parsed: ReturnType<typeof parseArgs<typeof PARSE_CONFIG>>;\n try {\n parsed = parseArgs({ ...PARSE_CONFIG, args: argv });\n } catch (err) {\n throw new UsageError(err instanceof Error ? err.message : String(err));\n }\n const { values, positionals } = parsed;\n\n return {\n files: positionals,\n output: values.output,\n mode: values.mode === undefined ? \"replace\" : parseMode(values.mode),\n keyPath: values.key,\n deanonymiseKeyPath: values.deanonymise,\n revert:\n values.revert === undefined || values.revert.length === 0\n ? undefined\n : values.revert,\n recursive: values.recursive === true,\n workers:\n values.workers === undefined\n ? defaultWorkerCount()\n : parseWorkers(values.workers),\n labels: values.labels === undefined ? undefined : splitList(values.labels),\n languages:\n values.languages === undefined ? undefined : splitList(values.languages),\n countries:\n values.countries === undefined\n ? undefined\n : parseCountries(values.countries),\n threshold:\n values.threshold === undefined\n ? DEFAULT_THRESHOLD\n : parseThreshold(values.threshold),\n redactString: values[\"redact-string\"] ?? DEFAULT_REDACT_STRING,\n json: values.json === true,\n quiet: values.quiet === true,\n help: values.help === true,\n version: values.version === true,\n listLabels: values[\"list-labels\"] === true,\n capabilities: values.capabilities === true,\n };\n};\n\nconst PARSE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n output: { type: \"string\", short: \"o\" },\n mode: { type: \"string\", short: \"m\" },\n key: { type: \"string\", short: \"k\" },\n deanonymise: { type: \"string\", short: \"d\" },\n revert: { type: \"string\", multiple: true },\n recursive: { type: \"boolean\", short: \"r\" },\n workers: { type: \"string\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n \"redact-string\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n \"list-labels\": { type: \"boolean\" },\n capabilities: { type: \"boolean\" },\n },\n} as const;\n","/* Pure helpers shared by the npm and embedded dictionary\n * loaders. Must stay free of @stll/anonymize-data imports\n * so the compiled binary's bundle excludes the raw JSON\n * dictionary modules. */\nimport type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\n\nimport { UsageError } from \"./args\";\n\nexport const NAME_DICTIONARY_PREFIXES = [\n \"names/first/\",\n \"names/surnames/\",\n] as const;\n\n/** Language code of a name dictionary id, or null. */\nexport const nameLanguageOfDictionary = (id: string): string | null => {\n const prefix = NAME_DICTIONARY_PREFIXES.find((p) => id.startsWith(p));\n return prefix ? id.slice(prefix.length) : null;\n};\n\nexport type DictionaryScope = {\n languages?: readonly string[] | undefined;\n countries?: readonly string[] | undefined;\n};\n\nconst pickKeys = <T>(\n record: Record<string, T>,\n keep: (key: string) => boolean,\n): Record<string, T> => {\n const result: Record<string, T> = {};\n for (const [key, value] of Object.entries(record)) {\n if (keep(key)) result[key] = value;\n }\n return result;\n};\n\n/** Dictionaries with every section present (possibly empty). */\nexport type ScopedDictionaries = {\n firstNames: Record<string, readonly string[]>;\n surnames: Record<string, readonly string[]>;\n denyList: Record<string, readonly string[]>;\n denyListMeta: Record<string, DictionaryMeta>;\n citiesByCountry: Record<string, readonly string[]>;\n};\n\n/**\n * Scope a fully loaded dictionary set to the requested\n * languages and countries. Mirrors the pre-load scoping\n * the npm loader does in dictionaries.ts; used by the\n * embedded loader, which always starts from the full set.\n */\nexport const filterDictionaries = (\n all: Dictionaries,\n { languages, countries }: DictionaryScope,\n): ScopedDictionaries => {\n const firstNames = all.firstNames ?? {};\n const surnames = all.surnames ?? {};\n const allDenyList = all.denyList ?? {};\n const allDenyListMeta = all.denyListMeta ?? {};\n\n if (languages !== undefined) {\n const available = Object.keys(firstNames);\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n }\n const keepLanguage = (lang: string): boolean =>\n languages === undefined || languages.includes(lang);\n const keepCountry = (country: string | null): boolean =>\n countries === undefined || country === null || countries.includes(country);\n\n const denyListMeta: Record<string, DictionaryMeta> = {};\n const denyList: Record<string, readonly string[]> = {};\n for (const [id, meta] of Object.entries(allDenyListMeta)) {\n if (!keepCountry(meta.country)) continue;\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && !keepLanguage(nameLang)) continue;\n const entries = allDenyList[id];\n if (entries === undefined) continue;\n denyListMeta[id] = meta;\n denyList[id] = entries;\n }\n\n return {\n firstNames: pickKeys(firstNames, keepLanguage),\n surnames: pickKeys(surnames, keepLanguage),\n denyList,\n denyListMeta,\n citiesByCountry: pickKeys(all.citiesByCountry ?? {}, (country) =>\n keepCountry(country),\n ),\n };\n};\n","import type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\nimport {\n ALL_DICTIONARY_IDS,\n DICTIONARY_META,\n loadCityDictionary,\n loadDictionary,\n loadNameDictionaries,\n type NameLanguage,\n} from \"@stll/anonymize-data\";\n\nimport { UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n NAME_DICTIONARY_PREFIXES,\n nameLanguageOfDictionary,\n} from \"./dictionary-scope\";\n\n/**\n * Countries with bundled city dictionaries that are\n * loaded when no --countries scope is given.\n */\nconst DEFAULT_CITY_COUNTRIES = [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n] as const;\n\nconst availableNameLanguages = (): readonly string[] =>\n ALL_DICTIONARY_IDS.filter((id) =>\n id.startsWith(NAME_DICTIONARY_PREFIXES[0]),\n ).map((id) => id.slice(NAME_DICTIONARY_PREFIXES[0].length));\n\nconst validateLanguages = (\n languages: readonly string[],\n): readonly NameLanguage[] => {\n const available = availableNameLanguages();\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n // SAFETY: every entry was checked against the bundled\n // name dictionary ids, which define NameLanguage.\n return languages as readonly NameLanguage[];\n};\n\nexport type LoadCliDictionariesOptions = DictionaryScope;\n\n/**\n * Load the bundled @stll/anonymize-data dictionaries,\n * scoped to the requested languages and countries.\n */\nexport const loadCliDictionaries = async ({\n languages,\n countries,\n}: LoadCliDictionariesOptions): Promise<Dictionaries> => {\n const nameLanguages =\n languages === undefined ? undefined : validateLanguages(languages);\n\n const denyIds = ALL_DICTIONARY_IDS.filter((id) => {\n const meta = DICTIONARY_META[id];\n if (\n countries &&\n meta.country !== null &&\n !countries.includes(meta.country)\n ) {\n return false;\n }\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && nameLanguages !== undefined) {\n return nameLanguages.includes(\n // SAFETY: nameLang comes from a bundled dictionary\n // id, which defines NameLanguage.\n nameLang as NameLanguage,\n );\n }\n return true;\n });\n\n const cityCountries = countries ?? DEFAULT_CITY_COUNTRIES;\n\n const [names, denyEntries, cityEntries] = await Promise.all([\n loadNameDictionaries(nameLanguages),\n Promise.all(\n denyIds.map(async (id) => ({ id, entries: await loadDictionary(id) })),\n ),\n Promise.all(\n cityCountries.map(async (country) => ({\n country,\n entries: await loadCityDictionary(country),\n })),\n ),\n ]);\n\n const denyList: Record<string, readonly string[]> = {};\n const denyListMeta: Record<string, DictionaryMeta> = {};\n for (const { id, entries } of denyEntries) {\n denyList[id] = entries;\n // SAFETY: anonymize-data categories match\n // DenyListCategory at runtime.\n denyListMeta[id] = DICTIONARY_META[id] as DictionaryMeta;\n }\n\n const citiesByCountry: Record<string, readonly string[]> = {};\n for (const { country, entries } of cityEntries) {\n if (entries.length > 0) citiesByCountry[country] = entries;\n }\n\n return {\n firstNames: names.firstNames,\n surnames: names.surnames,\n denyList,\n denyListMeta,\n citiesByCountry,\n };\n};\n","","import { randomUUID } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport {\n type FileHandle,\n link,\n lstat,\n open,\n readFile,\n rename,\n unlink,\n} from \"node:fs/promises\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport type { NativeOpenSessionArchiveOptions } from \"@stll/anonymize\";\nimport {\n DOCX_COVERAGE_MODES,\n anonymizeDocx,\n restoreDocxText,\n type DocxAnonymizationSession,\n type DocxAnonymizationSummary,\n type DocxRestorationResult,\n type DocxRestorationSession,\n} from \"@stll/anonymize-docx\";\n\nimport { parseCountries, UsageError } from \"./args\";\n\nconst DOCX_SESSION_KEY_BYTES = 32;\nconst DOCX_SESSION_LOCK_SUFFIX = \".lock\";\nconst MAX_EPOCH_SECONDS = 4_294_967_295;\n\nconst DOCX_SESSION_MODES = {\n continue: \"continue\",\n create: \"create\",\n} as const;\n\ntype DocxSessionMode =\n (typeof DOCX_SESSION_MODES)[keyof typeof DOCX_SESSION_MODES];\n\ntype DocxDetectionOptions = {\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n};\n\ntype DocxCommonOptions = {\n inputPath: string;\n outputPath: string;\n sessionArchivePath: string;\n sessionKeyPath: string;\n sessionId: string;\n coverage: (typeof DOCX_COVERAGE_MODES)[keyof typeof DOCX_COVERAGE_MODES];\n observedAtEpochSeconds?: number | undefined;\n json: boolean;\n quiet: boolean;\n};\n\ntype DocxCommand =\n | { type: \"help\" }\n | ({\n type: \"anonymize\";\n sessionMode: DocxSessionMode;\n detection: DocxDetectionOptions;\n } & DocxCommonOptions)\n | ({ type: \"restore\" } & DocxCommonOptions);\n\nexport type DocxPipelineRequest =\n | { type: \"anonymize\"; detection: DocxDetectionOptions }\n | { type: \"restore\" };\n\ntype DocxCliSession = DocxAnonymizationSession &\n DocxRestorationSession & {\n toEncryptedArchive: (key: Uint8Array) => Uint8Array;\n toEncryptedArchiveAt: (\n key: Uint8Array,\n observedAtEpochSeconds: number,\n ) => Uint8Array;\n };\n\nexport type DocxCliPipeline = {\n createRedactionSession: (sessionId: string) => DocxCliSession;\n restoreEncryptedRedactionSession: (\n options: NativeOpenSessionArchiveOptions,\n ) => DocxCliSession;\n};\n\ntype RunDocxCommandOptions = {\n argv: readonly string[];\n preparePipeline: (request: DocxPipelineRequest) => Promise<DocxCliPipeline>;\n};\n\nconst DOCX_HELP = `Usage:\n anonymize docx anonymize [options] <input.docx>\n anonymize docx restore [options] <input.docx>\n\nAnonymize or restore one DOCX file with an encrypted redaction session.\nDocument and session outputs are written atomically and never overwrite the\ninput, key file, or an existing document output.\n\nRequired options:\n -o, --output <path> New DOCX output path\n --session-archive <path> Encrypted session archive path\n --session-key-file <path>\n File containing exactly 32 raw key bytes\n --session-id <id> Expected opaque session identity\n\nAnonymize options:\n --session-mode <mode> \"create\" or \"continue\" (required)\n --coverage <mode> \"require-full\" (default) or \"allow-partial\"\n --labels <list> Comma-separated entity labels\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n --countries <list> ISO 3166-1 alpha-2 country codes\n --threshold <n> Minimum confidence score 0-1 (default: 0.3)\n\nRestore options:\n --coverage <mode> \"require-full\" (default) or \"allow-partial\"\n\nCommon options:\n --observed-at <seconds> Deterministic Unix timestamp for lifecycle checks\n --json Print the aggregate audit-safe summary as JSON\n --quiet Suppress the human-readable stderr summary\n -h, --help Show this help\n\nThe session key is read from a file, never from a command argument. In create\nmode the archive path must not exist. Continue mode atomically replaces the\nexisting archive only after the DOCX rewrite succeeds. It holds an exclusive\n\"<archive>.lock\" sidecar throughout the continuation to prevent lost updates.\nCaller-supplied detection plans and interactive review are available through the\npackage API, not this CLI.\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseEpochSeconds = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 0 || value > MAX_EPOCH_SECONDS) {\n throw new UsageError(\n `--observed-at must be an integer from 0 to ${MAX_EPOCH_SECONDS}, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseCoverage = (\n raw: string | undefined,\n): DocxCommonOptions[\"coverage\"] => {\n const value = raw ?? DOCX_COVERAGE_MODES.requireFull;\n if (\n value === DOCX_COVERAGE_MODES.requireFull ||\n value === DOCX_COVERAGE_MODES.allowPartial\n ) {\n return value;\n }\n throw new UsageError(\n `--coverage must be one of: ${Object.values(DOCX_COVERAGE_MODES).join(\", \")}; got \"${value}\"`,\n );\n};\n\nconst parseSessionMode = (raw: string | undefined): DocxSessionMode => {\n if (raw === undefined) {\n throw new UsageError(\"--session-mode is required for DOCX anonymization\");\n }\n if (\n raw === DOCX_SESSION_MODES.create ||\n raw === DOCX_SESSION_MODES.continue\n ) {\n return raw;\n }\n throw new UsageError(\n `--session-mode must be one of: ${Object.values(DOCX_SESSION_MODES).join(\", \")}; got \"${raw}\"`,\n );\n};\n\nconst required = (value: string | undefined, flag: string): string => {\n if (value === undefined || value.length === 0) {\n throw new UsageError(`${flag} is required for DOCX workflows`);\n }\n return value;\n};\n\ntype ParsedCommonValues = {\n output?: string | undefined;\n \"session-archive\"?: string | undefined;\n \"session-key-file\"?: string | undefined;\n \"session-id\"?: string | undefined;\n coverage?: string | undefined;\n \"observed-at\"?: string | undefined;\n json?: boolean | undefined;\n quiet?: boolean | undefined;\n};\n\nconst commonOptions = (\n values: ParsedCommonValues,\n positionals: readonly string[],\n): DocxCommonOptions => {\n if (positionals.length !== 1) {\n throw new UsageError(\"DOCX workflows require exactly one input file\");\n }\n const inputPath = positionals.at(0);\n if (inputPath === undefined) {\n throw new UsageError(\"DOCX workflows require exactly one input file\");\n }\n return {\n inputPath,\n outputPath: required(values.output, \"--output\"),\n sessionArchivePath: required(\n values[\"session-archive\"],\n \"--session-archive\",\n ),\n sessionKeyPath: required(values[\"session-key-file\"], \"--session-key-file\"),\n sessionId: required(values[\"session-id\"], \"--session-id\"),\n coverage: parseCoverage(values.coverage),\n observedAtEpochSeconds:\n values[\"observed-at\"] === undefined\n ? undefined\n : parseEpochSeconds(values[\"observed-at\"]),\n json: values.json === true,\n quiet: values.quiet === true,\n };\n};\n\nconst parseDocxCommand = (argv: readonly string[]): DocxCommand => {\n const action = argv.at(0);\n if (action === undefined || action === \"--help\" || action === \"-h\") {\n return { type: \"help\" };\n }\n const args = argv.slice(1);\n if (action === \"anonymize\") {\n let parsed: ReturnType<typeof parseArgs<typeof DOCX_ANONYMIZE_CONFIG>>;\n try {\n parsed = parseArgs({ ...DOCX_ANONYMIZE_CONFIG, args: [...args] });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n if (parsed.values.help === true) {\n return { type: \"help\" };\n }\n return {\n type: \"anonymize\",\n ...commonOptions(parsed.values, parsed.positionals),\n sessionMode: parseSessionMode(parsed.values[\"session-mode\"]),\n detection: {\n labels:\n parsed.values.labels === undefined\n ? undefined\n : splitList(parsed.values.labels),\n languages:\n parsed.values.languages === undefined\n ? undefined\n : splitList(parsed.values.languages),\n countries:\n parsed.values.countries === undefined\n ? undefined\n : parseCountries(parsed.values.countries),\n threshold:\n parsed.values.threshold === undefined\n ? 0.3\n : parseThreshold(parsed.values.threshold),\n },\n };\n }\n if (action === \"restore\") {\n let parsed: ReturnType<typeof parseArgs<typeof DOCX_RESTORE_CONFIG>>;\n try {\n parsed = parseArgs({ ...DOCX_RESTORE_CONFIG, args: [...args] });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n if (parsed.values.help === true) {\n return { type: \"help\" };\n }\n return {\n type: \"restore\",\n ...commonOptions(parsed.values, parsed.positionals),\n };\n }\n throw new UsageError(\n `unknown DOCX action \"${action}\"; expected \"anonymize\" or \"restore\"`,\n );\n};\n\nconst DOCX_COMMON_PARSE_OPTIONS = {\n output: { type: \"string\", short: \"o\" },\n \"session-archive\": { type: \"string\" },\n \"session-key-file\": { type: \"string\" },\n \"session-id\": { type: \"string\" },\n coverage: { type: \"string\" },\n \"observed-at\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n} as const;\n\nconst DOCX_ANONYMIZE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n ...DOCX_COMMON_PARSE_OPTIONS,\n \"session-mode\": { type: \"string\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n },\n} as const;\n\nconst DOCX_RESTORE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: DOCX_COMMON_PARSE_OPTIONS,\n} as const;\n\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst sessionArchiveLockPath = (archivePath: string): string =>\n `${canonicalPath(archivePath)}${DOCX_SESSION_LOCK_SUFFIX}`;\n\nconst assertDistinctPaths = (\n paths: readonly { path: string; flag: string }[],\n): void => {\n const seen = new Map<string, string>();\n for (const entry of paths) {\n const canonical = canonicalPath(entry.path);\n const existing = seen.get(canonical);\n if (existing !== undefined) {\n throw new UsageError(`${entry.flag} collides with ${existing}`);\n }\n seen.set(canonical, `${entry.flag} \"${entry.path}\"`);\n }\n};\n\nconst isNodeError = (\n error: unknown,\n code: string,\n): error is NodeJS.ErrnoException =>\n error instanceof Error && \"code\" in error && error.code === code;\n\nconst assertPathDoesNotExist = async (\n path: string,\n flag: string,\n): Promise<void> => {\n try {\n await lstat(path);\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return;\n }\n throw error;\n }\n throw new UsageError(`${flag} refuses to overwrite existing path \"${path}\"`);\n};\n\nconst preflightDocxCommand = async (\n command: Exclude<DocxCommand, { type: \"help\" }>,\n): Promise<void> => {\n const paths = [\n { path: command.inputPath, flag: \"input\" },\n { path: command.outputPath, flag: \"--output\" },\n { path: command.sessionArchivePath, flag: \"--session-archive\" },\n { path: command.sessionKeyPath, flag: \"--session-key-file\" },\n ];\n if (\n command.type === \"anonymize\" &&\n command.sessionMode === DOCX_SESSION_MODES.continue\n ) {\n paths.push({\n path: sessionArchiveLockPath(command.sessionArchivePath),\n flag: \"session archive lock\",\n });\n }\n assertDistinctPaths(paths);\n await assertPathDoesNotExist(command.outputPath, \"--output\");\n if (\n command.type === \"anonymize\" &&\n command.sessionMode === DOCX_SESSION_MODES.create\n ) {\n await assertPathDoesNotExist(\n command.sessionArchivePath,\n \"--session-archive\",\n );\n }\n};\n\ntype SessionArchiveLock = {\n release: () => Promise<void>;\n};\n\ntype OperationResult =\n | { type: \"succeeded\" }\n | { type: \"failed\"; error: unknown };\n\nconst captureOperationResult = async (\n operation: Promise<void>,\n): Promise<OperationResult> => {\n try {\n await operation;\n return { type: \"succeeded\" };\n } catch (error) {\n return { type: \"failed\", error };\n }\n};\n\nconst acquireSessionArchiveLock = async (\n archivePath: string,\n): Promise<SessionArchiveLock> => {\n const lockPath = sessionArchiveLockPath(archivePath);\n let handle: FileHandle;\n try {\n handle = await open(lockPath, \"wx\", 0o600);\n } catch (error) {\n if (isNodeError(error, \"EEXIST\")) {\n throw new Error(\n `encrypted session archive is locked by another continuation; if no process is running, remove the stale lock \"${lockPath}\"`,\n );\n }\n throw error;\n }\n return {\n release: async () => {\n const closeResult = await captureOperationResult(handle.close());\n const unlinkResult = await captureOperationResult(unlink(lockPath));\n if (closeResult.type === \"failed\") {\n throw closeResult.error;\n }\n if (\n unlinkResult.type === \"failed\" &&\n !isNodeError(unlinkResult.error, \"ENOENT\")\n ) {\n throw unlinkResult.error;\n }\n },\n };\n};\n\nconst readSessionKey = async (path: string): Promise<Uint8Array> => {\n const handle = await open(path, \"r\");\n let key: Uint8Array | undefined;\n try {\n const stats = await handle.stat();\n if (!stats.isFile()) {\n throw new UsageError(\"--session-key-file must be a regular file\");\n }\n if (process.platform !== \"win32\" && (stats.mode & 0o077) !== 0) {\n throw new UsageError(\n \"--session-key-file must not grant permissions to group or other users (use chmod 600)\",\n );\n }\n key = await handle.readFile();\n if (key.byteLength !== DOCX_SESSION_KEY_BYTES) {\n throw new UsageError(\n `--session-key-file must contain exactly ${DOCX_SESSION_KEY_BYTES} raw bytes`,\n );\n }\n await handle.close();\n return key;\n } catch (error) {\n key?.fill(0);\n try {\n await handle.close();\n } catch {\n // Preserve the validation or read error.\n }\n throw error;\n }\n};\n\nconst removeStagedFile = async (path: string | undefined): Promise<void> => {\n if (path === undefined) {\n return;\n }\n try {\n await unlink(path);\n } catch {\n // Best-effort cleanup must not hide the original operation error.\n }\n};\n\nconst stageFile = async (\n target: string,\n content: Uint8Array,\n): Promise<string> => {\n const temporary = join(\n dirname(target),\n `.${basename(target)}.${randomUUID()}.tmp`,\n );\n const handle = await open(temporary, \"wx\", 0o600);\n try {\n await handle.writeFile(content);\n await handle.sync();\n await handle.close();\n } catch (error) {\n try {\n await handle.close();\n } catch {\n // Preserve the write error while cleanup remains best effort.\n }\n await removeStagedFile(temporary);\n throw error;\n }\n return temporary;\n};\n\nconst publishNewFile = async (\n temporary: string,\n target: string,\n flag: string,\n): Promise<void> => {\n try {\n await link(temporary, target);\n } catch (error) {\n if (isNodeError(error, \"EEXIST\")) {\n throw new UsageError(\n `${flag} refuses to overwrite existing path \"${target}\"`,\n );\n }\n throw error;\n }\n await removeStagedFile(temporary);\n};\n\nconst publishReplacement = async (\n temporary: string,\n target: string,\n): Promise<void> => {\n await rename(temporary, target);\n};\n\nconst sessionArchive = (\n session: DocxCliSession,\n key: Uint8Array,\n observedAtEpochSeconds: number | undefined,\n): Uint8Array =>\n observedAtEpochSeconds === undefined\n ? session.toEncryptedArchive(key)\n : session.toEncryptedArchiveAt(key, observedAtEpochSeconds);\n\nconst outputSummary = (\n command: Pick<DocxCommonOptions, \"json\" | \"quiet\">,\n action: \"anonymized\" | \"restored\",\n summary: DocxAnonymizationSummary | Omit<DocxRestorationResult, \"document\">,\n): void => {\n if (command.json) {\n process.stdout.write(`${JSON.stringify(summary, null, 2)}\\n`);\n }\n if (command.quiet) {\n return;\n }\n const coverage = summary.coverage.status;\n if (\"entityCount\" in summary) {\n process.stderr.write(\n `anonymize: DOCX ${action}: ${summary.entityCount} entities, ${summary.appliedReplacementCount} replacements, ${coverage} coverage\\n`,\n );\n return;\n }\n process.stderr.write(\n `anonymize: DOCX ${action}: ${summary.restoredPlaceholderCount} placeholders, ${coverage} coverage\\n`,\n );\n};\n\nconst openSession = (\n pipeline: DocxCliPipeline,\n command: Pick<DocxCommonOptions, \"sessionId\" | \"observedAtEpochSeconds\">,\n archive: Uint8Array,\n key: Uint8Array,\n): DocxCliSession =>\n pipeline.restoreEncryptedRedactionSession({\n archive,\n key,\n expectedSessionId: command.sessionId,\n ...(command.observedAtEpochSeconds === undefined\n ? {}\n : { observedAtEpochSeconds: command.observedAtEpochSeconds }),\n });\n\nconst runDocxAnonymize = async (\n command: Extract<DocxCommand, { type: \"anonymize\" }>,\n pipeline: DocxCliPipeline,\n): Promise<void> => {\n const archivePath =\n command.sessionMode === DOCX_SESSION_MODES.continue\n ? canonicalPath(command.sessionArchivePath)\n : command.sessionArchivePath;\n const archiveLock =\n command.sessionMode === DOCX_SESSION_MODES.continue\n ? await acquireSessionArchiveLock(archivePath)\n : undefined;\n let workflowResult: OperationResult = { type: \"succeeded\" };\n let lockReleaseResult: OperationResult = { type: \"succeeded\" };\n let key: Uint8Array | undefined;\n let documentTemporary: string | undefined;\n let archiveTemporary: string | undefined;\n try {\n key = await readSessionKey(command.sessionKeyPath);\n const [document, existingArchive] = await Promise.all([\n readFile(command.inputPath),\n command.sessionMode === DOCX_SESSION_MODES.continue\n ? readFile(archivePath)\n : Promise.resolve(undefined),\n ]);\n const session =\n existingArchive === undefined\n ? pipeline.createRedactionSession(command.sessionId)\n : openSession(pipeline, command, existingArchive, key);\n const result = anonymizeDocx({\n document,\n session,\n expectedSessionId: command.sessionId,\n policy: { coverage: { mode: command.coverage } },\n ...(command.observedAtEpochSeconds === undefined\n ? {}\n : { observedAtEpochSeconds: command.observedAtEpochSeconds }),\n });\n const encryptedArchive = sessionArchive(\n session,\n key,\n command.observedAtEpochSeconds,\n );\n documentTemporary = await stageFile(command.outputPath, result.document);\n archiveTemporary = await stageFile(archivePath, encryptedArchive);\n if (command.sessionMode === DOCX_SESSION_MODES.create) {\n await publishNewFile(\n archiveTemporary,\n command.sessionArchivePath,\n \"--session-archive\",\n );\n } else {\n await publishReplacement(archiveTemporary, archivePath);\n }\n archiveTemporary = undefined;\n try {\n await publishNewFile(documentTemporary, command.outputPath, \"--output\");\n documentTemporary = undefined;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `encrypted session archive was updated, but DOCX output could not be published: ${message}`,\n );\n }\n outputSummary(command, \"anonymized\", result.summary);\n } catch (error) {\n workflowResult = { type: \"failed\", error };\n } finally {\n key?.fill(0);\n await Promise.all([\n removeStagedFile(documentTemporary),\n removeStagedFile(archiveTemporary),\n ]);\n if (archiveLock !== undefined) {\n lockReleaseResult = await captureOperationResult(archiveLock.release());\n }\n }\n if (workflowResult.type === \"failed\") {\n throw workflowResult.error;\n }\n if (lockReleaseResult.type === \"failed\") {\n const message =\n lockReleaseResult.error instanceof Error\n ? lockReleaseResult.error.message\n : String(lockReleaseResult.error);\n throw new Error(\n `DOCX and session outputs were published, but the session archive lock could not be released: ${message}`,\n );\n }\n};\n\nconst runDocxRestore = async (\n command: Extract<DocxCommand, { type: \"restore\" }>,\n pipeline: DocxCliPipeline,\n): Promise<void> => {\n const key = await readSessionKey(command.sessionKeyPath);\n try {\n const [document, archive] = await Promise.all([\n readFile(command.inputPath),\n readFile(command.sessionArchivePath),\n ]);\n const session = openSession(pipeline, command, archive, key);\n const result = restoreDocxText({\n document,\n session,\n expectedSessionId: command.sessionId,\n ...(command.observedAtEpochSeconds === undefined\n ? {}\n : { observedAtEpochSeconds: command.observedAtEpochSeconds }),\n });\n if (\n command.coverage === DOCX_COVERAGE_MODES.requireFull &&\n result.coverage.status === \"partial\"\n ) {\n throw new Error(\n \"DOCX contains content outside the fully supported restoration coverage\",\n );\n }\n const temporary = await stageFile(command.outputPath, result.document);\n try {\n await publishNewFile(temporary, command.outputPath, \"--output\");\n } catch (error) {\n await removeStagedFile(temporary);\n throw error;\n }\n const summary: Omit<DocxRestorationResult, \"document\"> = {\n sessionId: result.sessionId,\n restoredBlockCount: result.restoredBlockCount,\n restoredPlaceholderCount: result.restoredPlaceholderCount,\n coverage: result.coverage,\n };\n outputSummary(command, \"restored\", summary);\n } finally {\n key.fill(0);\n }\n};\n\nexport const runDocxCommand = async ({\n argv,\n preparePipeline,\n}: RunDocxCommandOptions): Promise<void> => {\n const command = parseDocxCommand(argv);\n if (command.type === \"help\") {\n process.stdout.write(DOCX_HELP);\n return;\n }\n await preflightDocxCommand(command);\n const pipeline = await preparePipeline(\n command.type === \"anonymize\"\n ? { type: \"anonymize\", detection: command.detection }\n : { type: \"restore\" },\n );\n if (command.type === \"anonymize\") {\n await runDocxAnonymize(command, pipeline);\n return;\n }\n await runDocxRestore(command, pipeline);\n};\n","import { realpathSync } from \"node:fs\";\nimport {\n mkdir,\n open,\n readdir,\n readFile,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { basename, dirname, join, relative, resolve } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\n\nimport type {\n deanonymise,\n Dictionaries,\n exportRedactionKey,\n NativeAnonymizeBinding,\n NativeOperatorConfig,\n NativePipelineBuildOptions,\n OperatorType,\n PipelineConfig,\n} from \"@stll/anonymize\";\nimport { CAPABILITY_MANIFEST } from \"@stll/anonymize/capabilities\";\nimport {\n DEFAULT_ENTITY_LABELS,\n ENTITY_LABELS,\n type EntityLabel,\n} from \"@stll/anonymize/constants\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nimport type { CliOptions } from \"./args\";\nimport {\n DEFAULT_THRESHOLD,\n HELP,\n parseCliArgs,\n parseCountries,\n UsageError,\n} from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n type DocxCliPipeline,\n type DocxPipelineRequest,\n runDocxCommand,\n} from \"./docx\";\n\n/**\n * The pipeline functions the CLI needs, backed by the\n * @stll/anonymize native SDK: a binding loader and the\n * config-to-pipeline builder, plus the redaction-key\n * helpers used by the deanonymise path.\n */\nexport type AnonymizeApi = {\n deanonymise: typeof deanonymise;\n exportRedactionKey: typeof exportRedactionKey;\n createNativePipelineFromConfig: (\n options: NativePipelineBuildOptions,\n ) => Promise<NativeCliPipeline>;\n loadNativeAnonymizeBinding: () => NativeAnonymizeBinding;\n};\n\n/**\n * Everything an entry point injects: the pipeline engine\n * and the dictionary source (the @stll/anonymize-data\n * package for the npm bin).\n */\nexport type CliEngine = {\n api: AnonymizeApi;\n loadDictionaries: (scope: DictionaryScope) => Promise<Dictionaries>;\n};\n\n// Statically imported so the version is baked into both\n// the npm bundle and the compiled binary; a runtime\n// package.json lookup would fail inside the binary's\n// virtual filesystem.\nconst cliVersion = (): string => pkg.version;\n\n/**\n * Filesystem identity of a path: realpath when it exists\n * (so symlinks to the same file compare equal), lexical\n * resolution otherwise (the file may not exist yet).\n */\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst readStdin = async (): Promise<string> => {\n process.stdin.setEncoding(\"utf8\");\n let text = \"\";\n for await (const chunk of process.stdin) text += chunk;\n return text;\n};\n\ntype NamedInput = {\n /** Source path, or null when reading stdin. */\n path: string | null;\n text: string;\n};\n\nconst readInputs = async (files: string[]): Promise<NamedInput[]> => {\n if (files.length === 0) {\n if (process.stdin.isTTY) {\n throw new UsageError(\n \"no input files and stdin is a terminal (see --help)\",\n );\n }\n return [{ path: null, text: await readStdin() }];\n }\n return Promise.all(\n files.map(async (path) => ({ path, text: await readFile(path, \"utf8\") })),\n );\n};\n\n/**\n * One file to anonymize in a batch run: the source path to\n * read and the path, relative to the output directory, to\n * write. For a plain file argument the relative path is the\n * basename; for a directory argument the input tree is\n * mirrored, so it is the path relative to that directory.\n */\ntype FileJob = {\n path: string;\n outputRelative: string;\n};\n\n/** Result of expanding the positional arguments into concrete\n * files. `batch` is true when the output must be a directory:\n * more than one file, or any directory argument. */\ntype ExpandedInputs = {\n jobs: FileJob[];\n batch: boolean;\n /** Likely-binary files skipped during directory walks. */\n skipped: number;\n};\n\n// Sniff window for the binary check. A regular text file never\n// contains a NUL byte; binaries (images, archives) reliably do.\nconst TEXT_SNIFF_BYTES = 8192;\n\n/**\n * True when the file's first {@link TEXT_SNIFF_BYTES} bytes\n * contain no NUL byte. Used to skip binaries discovered by a\n * directory walk without reading the whole file.\n */\nconst looksTextual = async (path: string): Promise<boolean> => {\n const handle = await open(path, \"r\");\n try {\n const buffer = Buffer.alloc(TEXT_SNIFF_BYTES);\n const { bytesRead } = await handle.read(buffer, 0, TEXT_SNIFF_BYTES, 0);\n return buffer.subarray(0, bytesRead).indexOf(0) === -1;\n } finally {\n await handle.close();\n }\n};\n\n/**\n * Collect regular files under `root`, sorted for deterministic\n * order. Symlinks are skipped (avoids cycles and escaping the\n * tree); subdirectories are descended only when `recursive`.\n */\nconst walkDirectory = async (\n root: string,\n recursive: boolean,\n excludeDir?: string,\n): Promise<string[]> => {\n const found: string[] = [];\n const visit = async (dir: string): Promise<void> => {\n const entries = (await readdir(dir, { withFileTypes: true })).toSorted(\n (a, b) => a.name.localeCompare(b.name),\n );\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n // Never descend into the output tree: rerunning with --output inside\n // the input directory must not ingest previously generated files.\n if (excludeDir !== undefined && resolve(full) === excludeDir) continue;\n if (recursive) await visit(full);\n } else if (entry.isFile()) {\n found.push(full);\n }\n }\n };\n await visit(root);\n return found;\n};\n\n/**\n * Expand positional arguments into concrete file jobs. A file\n * argument becomes one job (always processed); a directory is\n * walked, mirroring its tree into the output and skipping\n * likely-binary files.\n */\nconst expandInputs = async (\n files: readonly string[],\n recursive: boolean,\n outputDir?: string,\n): Promise<ExpandedInputs> => {\n const excludeDir = outputDir === undefined ? undefined : resolve(outputDir);\n const jobs: FileJob[] = [];\n let hasDirectory = false;\n let skipped = 0;\n for (const path of files) {\n // A stat failure (missing path, permission error) is not\n // fatal here: treat it as a file job so the read failure is\n // reported per file. A single such job stays single-input\n // and surfaces the error as a runtime exit; in a batch it is\n // counted as a failed file.\n let stats: Awaited<ReturnType<typeof stat>> | undefined;\n try {\n stats = await stat(path);\n } catch {\n jobs.push({ path, outputRelative: basename(path) });\n continue;\n }\n if (!stats.isDirectory()) {\n jobs.push({ path, outputRelative: basename(path) });\n continue;\n }\n hasDirectory = true;\n for (const file of await walkDirectory(path, recursive, excludeDir)) {\n // A file that disappears or turns unreadable mid-walk is queued anyway:\n // the per-file worker try/catch counts it as failed without aborting\n // the batch. Only a successful sniff that says \"binary\" skips it.\n const textual = await looksTextual(file).catch(() => true);\n if (!textual) {\n skipped += 1;\n continue;\n }\n jobs.push({ path: file, outputRelative: relative(path, file) });\n }\n }\n return { jobs, batch: hasDirectory || jobs.length > 1, skipped };\n};\n\n/**\n * Run `task` over `items` with at most `workers` in flight.\n * The shared native pipeline makes each redaction a synchronous\n * native call, so concurrency here only overlaps async file\n * I/O; the increments below are safe without locking because\n * no `await` sits between the read and the write of `next`.\n */\nconst runPool = async <T>(\n items: readonly T[],\n workers: number,\n task: (item: T) => Promise<void>,\n): Promise<void> => {\n let next = 0;\n const worker = async (): Promise<void> => {\n while (next < items.length) {\n const index = next;\n next += 1;\n // SAFETY: index < items.length checked above.\n await task(items[index] as T);\n }\n };\n const count = Math.max(1, Math.min(workers, items.length));\n await Promise.all(Array.from({ length: count }, worker));\n};\n\ntype CliEntity = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n source: string;\n};\n\ntype CliRedactionResult = {\n redactedText: string;\n redactionMap: Map<string, string>;\n operatorMap: Map<string, OperatorType>;\n entityCount: number;\n};\n\ntype NativeCliPipeline = DocxCliPipeline & {\n warmLazyRegex?: () => void;\n redactText: (\n fullText: string,\n operators?: NativeOperatorConfig,\n ) => {\n resolvedEntities: CliEntity[];\n redaction: CliRedactionResult;\n };\n};\n\n// Short aliases for the canonical multi-word labels so that\n// `--labels person,email,iban` works without quoting the space\n// in \"email address\". Separator-insensitive resolution (below)\n// additionally accepts hyphen/underscore forms such as\n// \"credit-card-number\".\nconst LABEL_ALIASES: Record<string, EntityLabel> = {\n email: \"email address\",\n phone: \"phone number\",\n org: \"organization\",\n organisation: \"organization\",\n dob: \"date of birth\",\n ssn: \"social security number\",\n \"tax id\": \"tax identification number\",\n passport: \"passport number\",\n \"credit card\": \"credit card number\",\n \"national id\": \"national identification number\",\n};\n\nconst LABEL_SEPARATOR_RE = /[\\s_-]+/g;\nconst ENTITY_LABEL_SET: ReadonlySet<string> = new Set(ENTITY_LABELS);\n\nconst isEntityLabel = (label: string): label is EntityLabel =>\n ENTITY_LABEL_SET.has(label);\n\n/**\n * Resolve a user-supplied label token to a canonical label.\n * Lowercases and collapses separators, then maps known short\n * aliases. Unknown tokens are returned normalized so the\n * caller can report them verbatim.\n */\nconst canonicalizeLabel = (raw: string): string => {\n const normalized = raw.toLowerCase().replace(LABEL_SEPARATOR_RE, \" \").trim();\n const known: readonly string[] = ENTITY_LABELS;\n if (known.includes(normalized)) {\n return normalized;\n }\n return LABEL_ALIASES[normalized] ?? normalized;\n};\n\nconst validateLabels = (labels: readonly string[]): EntityLabel[] => {\n const resolved = [...new Set(labels.map(canonicalizeLabel))];\n const valid: EntityLabel[] = [];\n const availableLabels = ENTITY_LABELS.join(\", \");\n const availableAliases = Object.keys(LABEL_ALIASES).join(\", \");\n for (const label of resolved) {\n if (!isEntityLabel(label)) {\n throw new UsageError(\n [\n \"--labels: unknown label\",\n JSON.stringify(label) + \";\",\n \"available:\",\n availableLabels,\n \"(aliases:\",\n availableAliases + \")\",\n ].join(\" \"),\n );\n }\n valid.push(label);\n }\n return valid;\n};\n\ntype PipelineConfigOptions = Pick<\n CliOptions,\n \"countries\" | \"labels\" | \"languages\" | \"threshold\"\n>;\n\nconst buildPipelineConfig = async (\n opts: PipelineConfigOptions,\n loadDictionaries: CliEngine[\"loadDictionaries\"],\n): Promise<PipelineConfig> => {\n const dictionaries = await loadDictionaries({\n languages: opts.languages,\n countries: opts.countries,\n });\n return {\n threshold: opts.threshold,\n enableTriggerPhrases: true,\n enableRegex: true,\n enableLegalForms: true,\n enableNameCorpus: true,\n ...(opts.languages === undefined\n ? {}\n : { nameCorpusLanguages: [...opts.languages] }),\n enableDenyList: true,\n ...(opts.countries === undefined\n ? {}\n : { denyListCountries: [...opts.countries] }),\n enableGazetteer: false,\n enableCountries: true,\n enableConfidenceBoost: true,\n enableCoreference: true,\n enableZoneClassification: true,\n enableHotwordRules: true,\n labels:\n opts.labels === undefined\n ? [...DEFAULT_ENTITY_LABELS]\n : validateLabels(opts.labels),\n workspaceId: \"cli\",\n dictionaries,\n };\n};\n\nconst buildOperatorConfig = (opts: CliOptions): NativeOperatorConfig => {\n const operators: NonNullable<NativeOperatorConfig[\"operators\"]> = {};\n if (opts.mode === \"redact\") {\n const labels =\n opts.labels === undefined\n ? DEFAULT_ENTITY_LABELS\n : validateLabels(opts.labels);\n for (const label of labels) operators[label] = \"redact\";\n }\n return { operators, redactString: opts.redactString };\n};\n\nconst writeOutput = async (\n path: string | undefined,\n content: string,\n): Promise<void> => {\n if (path === undefined) {\n process.stdout.write(content);\n return;\n }\n await writeFile(path, content, \"utf8\");\n};\n\ntype RedactionKeyFile = {\n entries: Record<string, { original: string; operator: string }>;\n};\n\nconst parseRedactionKey = (raw: string): Map<string, string> => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new UsageError(\"redaction key is not valid JSON\");\n }\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new UsageError(\n 'redaction key must be an object with an \"entries\" field',\n );\n }\n const { entries } = parsed as RedactionKeyFile;\n if (\n typeof entries !== \"object\" ||\n entries === null ||\n Array.isArray(entries)\n ) {\n throw new UsageError('redaction key \"entries\" must be an object');\n }\n const map = new Map<string, string>();\n for (const [placeholder, entry] of Object.entries(entries)) {\n if (typeof entry?.original !== \"string\") {\n throw new UsageError(\n `redaction key entry \"${placeholder}\" has no original text`,\n );\n }\n map.set(placeholder, entry.original);\n }\n return map;\n};\n\n/**\n * Restrict a redaction key to the entities named by --revert.\n * Each token matches a placeholder (\"[PERSON_1]\") or an original\n * value (\"Jan Novák\"), case-sensitive and exact. A token that\n * matches nothing is a usage error listing the placeholders the\n * key does define, so the caller can correct the spelling.\n */\nconst selectRevertEntries = (\n redactionMap: ReadonlyMap<string, string>,\n tokens: readonly string[],\n): Map<string, string> => {\n const selected = new Map<string, string>();\n for (const token of tokens) {\n let matched = false;\n for (const [placeholder, original] of redactionMap) {\n if (placeholder === token || original === token) {\n selected.set(placeholder, original);\n matched = true;\n }\n }\n if (!matched) {\n const MAX_LISTED_PLACEHOLDERS = 20;\n const placeholders = [...redactionMap.keys()];\n const listed = placeholders.slice(0, MAX_LISTED_PLACEHOLDERS).join(\", \");\n const rest = placeholders.length - MAX_LISTED_PLACEHOLDERS;\n const suffix = rest > 0 ? ` and ${rest} more` : \"\";\n throw new UsageError(\n `--revert ${JSON.stringify(token)} matched no placeholder or ` +\n `original; available placeholders: ${listed}${suffix}`,\n );\n }\n }\n return selected;\n};\n\n/**\n * Ask for a country scope when running interactively on\n * files with no scope flags. Skipped for piped stdin so\n * the CLI stays scriptable.\n */\nexport const shouldPromptForScope = (\n opts: CliOptions,\n tty: { stdinIsTTY: boolean; stderrIsTTY: boolean },\n): boolean =>\n opts.countries === undefined &&\n opts.languages === undefined &&\n !opts.quiet &&\n opts.files.length > 0 &&\n tty.stdinIsTTY &&\n tty.stderrIsTTY;\n\nconst promptForCountries = async (): Promise<string[] | undefined> => {\n const rl = createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n try {\n const answer = await rl.question(\n \"Country scope (ISO codes like CZ,DE,GB; Enter loads all): \",\n );\n const trimmed = answer.trim();\n return trimmed === \"\" ? undefined : parseCountries(trimmed);\n } finally {\n rl.close();\n }\n};\n\nconst runDeanonymise = async (\n opts: CliOptions,\n api: AnonymizeApi,\n): Promise<void> => {\n if (opts.keyPath !== undefined) {\n throw new UsageError(\"--key cannot be combined with --deanonymise\");\n }\n const keyPath = opts.deanonymiseKeyPath;\n if (keyPath === undefined) throw new UsageError(\"missing redaction key path\");\n const fullMap = parseRedactionKey(await readFile(keyPath, \"utf8\"));\n\n // --revert restores a chosen subset; leaving the rest of the\n // key out means deanonymise skips those placeholders, so the\n // other entities stay redacted.\n const redactionMap =\n opts.revert === undefined\n ? fullMap\n : selectRevertEntries(fullMap, opts.revert);\n\n const inputs = await readInputs(opts.files);\n if (inputs.length > 1) {\n throw new UsageError(\"--deanonymise accepts a single input\");\n }\n const input = inputs[0];\n if (!input) throw new UsageError(\"no input to deanonymise\");\n if (opts.output !== undefined) {\n guardWriteTargets(input.path === null ? [] : [input.path], [\n { path: opts.output, flag: \"--output\" },\n ]);\n }\n await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));\n};\n\n/**\n * Reject any write target (output or key file) whose\n * filesystem identity collides with an input file or with\n * another write target. Symlinks count as collisions.\n */\nconst guardWriteTargets = (\n inputPaths: readonly string[],\n writeTargets: readonly { path: string; flag: string }[],\n): void => {\n const inputs = new Set(inputPaths.map(canonicalPath));\n const seen = new Map<string, string>();\n for (const target of writeTargets) {\n const canonical = canonicalPath(target.path);\n if (inputs.has(canonical)) {\n throw new UsageError(\n `refusing to overwrite input file \"${target.path}\" (${target.flag})`,\n );\n }\n const clash = seen.get(canonical);\n if (clash !== undefined) {\n throw new UsageError(\n `${target.flag} \"${target.path}\" collides with ${clash}`,\n );\n }\n seen.set(canonical, `${target.flag} \"${target.path}\"`);\n }\n};\n\nconst summarize = (entities: readonly CliEntity[]): string => {\n const counts = new Map<string, number>();\n for (const entity of entities) {\n counts.set(entity.label, (counts.get(entity.label) ?? 0) + 1);\n }\n const parts = [...counts.entries()]\n .toSorted((a, b) => b[1] - a[1])\n .map(([label, count]) => `${label}: ${count}`);\n return parts.length > 0 ? parts.join(\", \") : \"none\";\n};\n\n/**\n * A single unit of anonymize work: the text to process, where\n * to write it (undefined means stdout), and a label for the\n * stderr summary. Used by the stdin and single-file flows,\n * which additionally support --json and --key.\n */\ntype SingleInput = {\n text: string;\n outputPath: string | undefined;\n source: string;\n};\n\nconst runAnonymiseSingle = async (\n opts: CliOptions,\n runtime: CliRuntime,\n api: AnonymizeApi,\n input: SingleInput,\n): Promise<void> => {\n const { entities, redaction } = await runtime.redact(\n input.text,\n buildOperatorConfig(opts),\n );\n\n if (opts.json) {\n // In redact mode the user chose irreversibility, so the\n // JSON must not carry any detected text. Whitelist the\n // non-sensitive metadata fields; this drops `text` and a\n // coref alias's `corefSourceText`. Offsets index the\n // caller's own input and are kept.\n const jsonEntities =\n opts.mode === \"redact\"\n ? entities.map(({ start, end, label, score, source }) => ({\n start,\n end,\n label,\n score,\n source,\n }))\n : entities;\n const payload = {\n entityCount: redaction.entityCount,\n entities: jsonEntities,\n redactedText: redaction.redactedText,\n };\n await writeOutput(\n input.outputPath,\n `${JSON.stringify(payload, null, 2)}\\n`,\n );\n } else {\n await writeOutput(input.outputPath, redaction.redactedText);\n }\n\n if (opts.keyPath !== undefined) {\n await writeFile(\n opts.keyPath,\n api.exportRedactionKey(redaction.redactionMap, redaction.operatorMap),\n \"utf8\",\n );\n }\n\n if (!opts.quiet) {\n process.stderr.write(\n `anonymize: ${input.source}: ${summarize(entities)}\\n`,\n );\n }\n};\n\n/** Tally of a batch run for the closing summary line. */\ntype BatchOutcome = { processed: number; failed: number };\n\nconst runAnonymiseBatch = async (\n opts: CliOptions,\n runtime: CliRuntime,\n output: string,\n jobs: readonly FileJob[],\n skipped: number,\n): Promise<void> => {\n await mkdir(output, { recursive: true });\n const operatorConfig = buildOperatorConfig(opts);\n const outcome: BatchOutcome = { processed: 0, failed: 0 };\n\n await runPool(jobs, opts.workers, async (job) => {\n const outputPath = join(output, job.outputRelative);\n try {\n const text = await readFile(job.path, \"utf8\");\n const { entities, redaction } = await runtime.redact(\n text,\n operatorConfig,\n );\n await mkdir(dirname(outputPath), { recursive: true });\n await writeFile(outputPath, redaction.redactedText, \"utf8\");\n // No await between here and the increment: safe on the\n // single JS thread despite concurrent workers.\n outcome.processed += 1;\n if (!opts.quiet) {\n process.stderr.write(\n `anonymize: ${job.path}: ${summarize(entities)}\\n`,\n );\n }\n } catch (err) {\n outcome.failed += 1;\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${job.path}: error: ${message}\\n`);\n }\n });\n\n if (!opts.quiet) {\n const parts = [\n `${outcome.processed} processed`,\n `${outcome.failed} failed`,\n ];\n if (skipped > 0) parts.push(`${skipped} skipped`);\n process.stderr.write(`anonymize: ${parts.join(\", \")}\\n`);\n }\n // Any per-file failure is a nonzero exit, but the whole batch\n // still runs so one bad file does not hide the rest.\n if (outcome.failed > 0) process.exitCode = 1;\n};\n\nconst runAnonymise = async (\n opts: CliOptions,\n { api, loadDictionaries }: CliEngine,\n): Promise<void> => {\n if (opts.keyPath !== undefined && opts.mode !== \"replace\") {\n throw new UsageError('--key requires --mode \"replace\"');\n }\n\n const scoped = shouldPromptForScope(opts, {\n stdinIsTTY: process.stdin.isTTY === true,\n stderrIsTTY: process.stderr.isTTY === true,\n })\n ? { ...opts, countries: await promptForCountries() }\n : opts;\n\n // No positional arguments: read stdin as a single input.\n if (scoped.files.length === 0) {\n const [input] = await readInputs(scoped.files);\n if (!input) throw new UsageError(\"no input to anonymize\");\n guardWriteTargets([], collectSingleTargets(scoped));\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseSingle(scoped, runtime, api, {\n text: input.text,\n outputPath: scoped.output,\n source: \"stdin\",\n });\n return;\n }\n\n const { jobs, batch, skipped } = await expandInputs(\n scoped.files,\n scoped.recursive,\n scoped.output,\n );\n\n if (!batch) {\n // Exactly one plain file: single-input flow with --json/--key.\n const [job] = jobs;\n if (!job) throw new UsageError(\"no input to anonymize\");\n guardWriteTargets([job.path], collectSingleTargets(scoped));\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseSingle(scoped, runtime, api, {\n text: await readFile(job.path, \"utf8\"),\n outputPath: scoped.output,\n source: job.path,\n });\n return;\n }\n\n // Batch: a directory, or more than one file.\n const output = scoped.output;\n if (output === undefined) {\n throw new UsageError(\n \"batch input (a directory or multiple files) requires --output <directory>\",\n );\n }\n if (scoped.keyPath !== undefined) {\n throw new UsageError(\"--key works with a single input only\");\n }\n if (scoped.json) {\n throw new UsageError(\"--json works with a single input only\");\n }\n\n // Validate every write target before any work: colliding\n // output paths (same basename from different input dirs,\n // symlinks to an input) fail fast instead of silently\n // clobbering files mid-batch.\n guardWriteTargets(\n jobs.map((job) => job.path),\n jobs.map((job) => ({\n path: join(output, job.outputRelative),\n flag: \"--output\",\n })),\n );\n\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseBatch(scoped, runtime, output, jobs, skipped);\n};\n\n/** Write targets for a single-input run: --output and --key. */\nconst collectSingleTargets = (\n opts: CliOptions,\n): { path: string; flag: string }[] => {\n const targets: { path: string; flag: string }[] = [];\n if (opts.output !== undefined) {\n targets.push({ path: opts.output, flag: \"--output\" });\n }\n if (opts.keyPath !== undefined) {\n targets.push({ path: opts.keyPath, flag: \"--key\" });\n }\n return targets;\n};\n\ntype CliRuntime = {\n redact: (\n fullText: string,\n operators: NativeOperatorConfig,\n ) => Promise<{ entities: CliEntity[]; redaction: CliRedactionResult }>;\n};\n\nconst prepareNativeCliPipeline = async (\n api: AnonymizeApi,\n config: PipelineConfig,\n): Promise<NativeCliPipeline> => {\n const pipeline = await api.createNativePipelineFromConfig({\n binding: api.loadNativeAnonymizeBinding(),\n config,\n gazetteerEntries: [],\n });\n pipeline.warmLazyRegex?.();\n return pipeline;\n};\n\nconst prepareCliRuntime = async (\n api: AnonymizeApi,\n config: PipelineConfig,\n): Promise<CliRuntime> => {\n const pipeline = await prepareNativeCliPipeline(api, config);\n return {\n redact: async (fullText, operators) => {\n const result = pipeline.redactText(fullText, operators);\n return {\n entities: result.resolvedEntities,\n redaction: result.redaction,\n };\n },\n };\n};\n\n/**\n * Render the canonical entity labels and the short aliases\n * accepted by --labels, for the --list-labels discovery flag.\n */\nconst formatLabelList = (): string => {\n const lines: string[] = [\"Detectable entity labels (pass to --labels):\"];\n for (const label of ENTITY_LABELS) {\n lines.push(` ${label}`);\n }\n lines.push(\"\", \"Short aliases:\");\n const aliases = Object.entries(LABEL_ALIASES);\n let width = 0;\n for (const [alias] of aliases) {\n width = Math.max(width, alias.length);\n }\n for (const [alias, canonical] of aliases) {\n lines.push(` ${alias.padEnd(width)} -> ${canonical}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n};\n\nconst dispatch = async (engine: CliEngine): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv.at(0) === \"docx\") {\n await runDocxCommand({\n argv: argv.slice(1),\n preparePipeline: async (\n request: DocxPipelineRequest,\n ): Promise<DocxCliPipeline> => {\n const options: PipelineConfigOptions =\n request.type === \"anonymize\"\n ? request.detection\n : {\n countries: [],\n languages: [],\n threshold: DEFAULT_THRESHOLD,\n };\n return prepareNativeCliPipeline(\n engine.api,\n await buildPipelineConfig(options, engine.loadDictionaries),\n );\n },\n });\n return;\n }\n const opts = parseCliArgs(argv);\n if (opts.help) {\n process.stdout.write(HELP);\n return;\n }\n if (opts.version) {\n process.stdout.write(`${cliVersion()}\\n`);\n return;\n }\n if (opts.listLabels) {\n process.stdout.write(formatLabelList());\n return;\n }\n if (opts.capabilities) {\n process.stdout.write(`${JSON.stringify(CAPABILITY_MANIFEST, null, 2)}\\n`);\n return;\n }\n if (opts.deanonymiseKeyPath !== undefined) {\n await runDeanonymise(opts, engine.api);\n return;\n }\n if (opts.revert !== undefined) {\n throw new UsageError(\"--revert requires --deanonymise <key>\");\n }\n await runAnonymise(opts, engine);\n};\n\n/**\n * Run the CLI against the given engine and set the\n * process exit code (0 ok, 1 runtime error, 2 usage).\n */\nexport const runCli = async (engine: CliEngine): Promise<void> => {\n try {\n await dispatch(engine);\n } catch (err) {\n if (err instanceof UsageError) {\n process.stderr.write(`anonymize: ${err.message}\\n`);\n process.stderr.write(`Try \"anonymize --help\" for usage.\\n`);\n process.exitCode = 2;\n } else {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${message}\\n`);\n process.exitCode = 1;\n }\n }\n};\n","#!/usr/bin/env node\n/* npm-distributed entry point — backs the CLI with the\n * native engine (@stll/anonymize napi bindings) and\n * the @stll/anonymize-data dictionary package. */\nimport * as anonymize from \"@stll/anonymize\";\n\nimport { loadCliDictionaries } from \"./dictionaries\";\nimport { runCli } from \"./main\";\n\nawait runCli({ api: anonymize, loadDictionaries: loadCliDictionaries });\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":["splitList","parseThreshold","required","canonicalPath","isNodeError","publishNewFile","canonicalPath","pkg.version","known"],"sources":["../src/args.ts","../src/dictionary-scope.ts","../src/dictionaries.ts","../package.json","../src/docx.ts","../src/pdf.ts","../src/main.ts","../src/cli.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { parseArgs } from \"node:util\";\n\nexport const CLI_MODES = [\"replace\", \"redact\"] as const;\nexport type CliMode = (typeof CLI_MODES)[number];\n\nexport const DEFAULT_THRESHOLD = 0.3;\nexport const DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/** Upper bound on the default worker count; batch I/O overlap\n * saturates well before this, and redaction itself is a\n * synchronous native call serialized on the JS thread. */\nexport const MAX_DEFAULT_WORKERS = 4;\n\n/** Default batch concurrency: min(4, cores). Workers overlap\n * file reads/writes; the shared native pipeline runs each\n * redaction to completion on the single JS thread. */\nexport const defaultWorkerCount = (): number =>\n Math.max(1, Math.min(MAX_DEFAULT_WORKERS, availableParallelism()));\n\n/** Invalid invocation; printed with usage hint, exit code 2. */\nexport class UsageError extends Error {}\n\nexport type CliOptions = {\n files: string[];\n output?: string | undefined;\n mode: CliMode;\n keyPath?: string | undefined;\n deanonymiseKeyPath?: string | undefined;\n revert?: string[] | undefined;\n recursive: boolean;\n workers: number;\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n redactString: string;\n json: boolean;\n quiet: boolean;\n help: boolean;\n version: boolean;\n listLabels: boolean;\n capabilities: boolean;\n};\n\nexport const HELP = `Usage: anonymize [options] [file|dir ...]\n\nDetect and anonymize PII in text. Reads the given files, or stdin\nwhen no files are given. A directory argument processes the text\nfiles inside it (add --recursive to descend into subdirectories).\nWrites to stdout, or to --output.\nAll processing is local; the CLI makes no network calls.\n\nDOCX workflows:\n Run \"anonymize docx --help\" for structure-preserving DOCX anonymization\n and restoration with encrypted session archives.\n\nPDF workflows:\n Run \"anonymize pdf --help\" for destructive local Poppler/Tesseract PDF\n anonymization into a verified fresh image-only output.\n\nOptions:\n -o, --output <path> Output file, or directory for batch\n input (multiple files or a directory)\n -m, --mode <mode> \"replace\" (reversible [PERSON_1]\n placeholders) or \"redact\"\n (default: replace)\n -k, --key <path> Write the redaction key as JSON\n (single input, replace mode)\n -d, --deanonymise <path> Restore redacted text using the\n redaction key at <path>\n --revert <term> With --deanonymise, restore only the\n given entity. Match a placeholder token\n (\"[PERSON_1]\") or an original value\n (\"Jan Novák\"), case-sensitive exact.\n Repeatable; others stay redacted\n -r, --recursive Descend into subdirectories when a\n directory is given as input\n --workers <n> Batch files to process concurrently\n (default: min(${MAX_DEFAULT_WORKERS}, CPU cores)). Overlaps\n file I/O; redaction is serialized on\n the JS thread\n --labels <list> Comma-separated entity labels to detect\n (default: all). Accepts canonical labels\n (\"email address\"), short aliases (email,\n phone, org, dob, ssn), and hyphen/underscore\n forms (\"credit-card-number\")\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n (default: all bundled)\n --countries <list> ISO 3166-1 alpha-2 codes scoping deny\n lists and city data, e.g. \"CZ,DE,GB\"\n (default: all deny lists; city data\n for a 30-country default set)\n --threshold <n> Minimum confidence score, 0-1\n (default: ${DEFAULT_THRESHOLD})\n --redact-string <s> Replacement text in redact mode\n (default: \"${DEFAULT_REDACT_STRING}\")\n --json Emit JSON (entities + redacted text) to\n stdout (single input only)\n --quiet Suppress the summary on stderr\n -h, --help Show this help\n -v, --version Show the version\n --list-labels List detectable entity labels and the\n short aliases accepted by --labels\n --capabilities Emit the versioned capability manifest as JSON\n\nBatch input (directory or multiple files):\n Requires --output <directory>. The input tree is mirrored\n into the output directory. Directory walks process regular\n files only and skip likely-binary files (a NUL byte in the\n first 8 KiB); explicitly named files are always processed.\n The stderr summary reports how many files were processed,\n failed, and skipped; any failure sets exit code 1.\n --key and --json apply to single inputs only.\n\nInteractive prompt:\n When run on files from a terminal without --countries or\n --languages, the CLI asks once which country scope to load.\n Piped stdin/stderr or --quiet skips the prompt, so scripts\n and agents never block on input.\n\nExit codes:\n 0 success\n 1 runtime error (message on stderr)\n 2 usage error (message on stderr)\n\nJSON output (--json):\n { \"entityCount\": number,\n \"entities\": [{ \"start\": number, \"end\": number,\n \"label\": string, \"text\": string,\n \"score\": number, \"source\": string }],\n \"redactedText\": string }\n Offsets are UTF-16 code-unit indexes into the input.\n The stderr summary contains entity counts only, never\n the detected text.\n\nExamples:\n anonymize contract.txt > contract.anon.txt\n anonymize -k contract.key.json -o contract.anon.txt contract.txt\n anonymize -d contract.key.json contract.anon.txt\n anonymize -r --workers 8 -o out/ docs/\n anonymize -d key.json --revert \"[PERSON_1]\" contract.anon.txt\n cat notes.md | anonymize --countries CZ,SK --languages cs,sk\n anonymize --json --quiet input.txt | jq '.entities[].label'\n anonymize docx --help\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseWorkers = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 1) {\n throw new UsageError(`--workers must be a positive integer, got \"${raw}\"`);\n }\n return value;\n};\n\nconst parseMode = (raw: string): CliMode => {\n const mode = CLI_MODES.find((candidate) => candidate === raw);\n if (!mode) {\n throw new UsageError(\n `--mode must be one of: ${CLI_MODES.join(\", \")}; got \"${raw}\"`,\n );\n }\n return mode;\n};\n\nconst COUNTRY_CODE_RE = /^[A-Za-z]{2}$/;\n\nexport const parseCountries = (raw: string): string[] => {\n const countries = [\n ...new Set(splitList(raw).map((code) => code.toUpperCase())),\n ];\n const invalid = countries.find((code) => !COUNTRY_CODE_RE.test(code));\n if (invalid) {\n throw new UsageError(\n `--countries expects ISO 3166-1 alpha-2 codes (e.g. \"CZ,DE\"), got \"${invalid}\"`,\n );\n }\n return countries;\n};\n\nexport const parseCliArgs = (argv: string[]): CliOptions => {\n let parsed: ReturnType<typeof parseArgs<typeof PARSE_CONFIG>>;\n try {\n parsed = parseArgs({ ...PARSE_CONFIG, args: argv });\n } catch (err) {\n throw new UsageError(err instanceof Error ? err.message : String(err));\n }\n const { values, positionals } = parsed;\n\n return {\n files: positionals,\n output: values.output,\n mode: values.mode === undefined ? \"replace\" : parseMode(values.mode),\n keyPath: values.key,\n deanonymiseKeyPath: values.deanonymise,\n revert:\n values.revert === undefined || values.revert.length === 0\n ? undefined\n : values.revert,\n recursive: values.recursive === true,\n workers:\n values.workers === undefined\n ? defaultWorkerCount()\n : parseWorkers(values.workers),\n labels: values.labels === undefined ? undefined : splitList(values.labels),\n languages:\n values.languages === undefined ? undefined : splitList(values.languages),\n countries:\n values.countries === undefined\n ? undefined\n : parseCountries(values.countries),\n threshold:\n values.threshold === undefined\n ? DEFAULT_THRESHOLD\n : parseThreshold(values.threshold),\n redactString: values[\"redact-string\"] ?? DEFAULT_REDACT_STRING,\n json: values.json === true,\n quiet: values.quiet === true,\n help: values.help === true,\n version: values.version === true,\n listLabels: values[\"list-labels\"] === true,\n capabilities: values.capabilities === true,\n };\n};\n\nconst PARSE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n output: { type: \"string\", short: \"o\" },\n mode: { type: \"string\", short: \"m\" },\n key: { type: \"string\", short: \"k\" },\n deanonymise: { type: \"string\", short: \"d\" },\n revert: { type: \"string\", multiple: true },\n recursive: { type: \"boolean\", short: \"r\" },\n workers: { type: \"string\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n \"redact-string\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n \"list-labels\": { type: \"boolean\" },\n capabilities: { type: \"boolean\" },\n },\n} as const;\n","/* Pure helpers shared by the npm and embedded dictionary\n * loaders. Must stay free of @stll/anonymize-data imports\n * so the compiled binary's bundle excludes the raw JSON\n * dictionary modules. */\nimport type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\n\nimport { UsageError } from \"./args\";\n\nexport const NAME_DICTIONARY_PREFIXES = [\n \"names/first/\",\n \"names/surnames/\",\n] as const;\n\n/** Language code of a name dictionary id, or null. */\nexport const nameLanguageOfDictionary = (id: string): string | null => {\n const prefix = NAME_DICTIONARY_PREFIXES.find((p) => id.startsWith(p));\n return prefix ? id.slice(prefix.length) : null;\n};\n\nexport type DictionaryScope = {\n languages?: readonly string[] | undefined;\n countries?: readonly string[] | undefined;\n};\n\nconst pickKeys = <T>(\n record: Record<string, T>,\n keep: (key: string) => boolean,\n): Record<string, T> => {\n const result: Record<string, T> = {};\n for (const [key, value] of Object.entries(record)) {\n if (keep(key)) result[key] = value;\n }\n return result;\n};\n\n/** Dictionaries with every section present (possibly empty). */\nexport type ScopedDictionaries = {\n firstNames: Record<string, readonly string[]>;\n surnames: Record<string, readonly string[]>;\n denyList: Record<string, readonly string[]>;\n denyListMeta: Record<string, DictionaryMeta>;\n citiesByCountry: Record<string, readonly string[]>;\n};\n\n/**\n * Scope a fully loaded dictionary set to the requested\n * languages and countries. Mirrors the pre-load scoping\n * the npm loader does in dictionaries.ts; used by the\n * embedded loader, which always starts from the full set.\n */\nexport const filterDictionaries = (\n all: Dictionaries,\n { languages, countries }: DictionaryScope,\n): ScopedDictionaries => {\n const firstNames = all.firstNames ?? {};\n const surnames = all.surnames ?? {};\n const allDenyList = all.denyList ?? {};\n const allDenyListMeta = all.denyListMeta ?? {};\n\n if (languages !== undefined) {\n const available = Object.keys(firstNames);\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n }\n const keepLanguage = (lang: string): boolean =>\n languages === undefined || languages.includes(lang);\n const keepCountry = (country: string | null): boolean =>\n countries === undefined || country === null || countries.includes(country);\n\n const denyListMeta: Record<string, DictionaryMeta> = {};\n const denyList: Record<string, readonly string[]> = {};\n for (const [id, meta] of Object.entries(allDenyListMeta)) {\n if (!keepCountry(meta.country)) continue;\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && !keepLanguage(nameLang)) continue;\n const entries = allDenyList[id];\n if (entries === undefined) continue;\n denyListMeta[id] = meta;\n denyList[id] = entries;\n }\n\n return {\n firstNames: pickKeys(firstNames, keepLanguage),\n surnames: pickKeys(surnames, keepLanguage),\n denyList,\n denyListMeta,\n citiesByCountry: pickKeys(all.citiesByCountry ?? {}, (country) =>\n keepCountry(country),\n ),\n };\n};\n","import type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\nimport {\n ALL_DICTIONARY_IDS,\n DICTIONARY_META,\n loadCityDictionary,\n loadDictionary,\n loadNameDictionaries,\n type NameLanguage,\n} from \"@stll/anonymize-data\";\n\nimport { UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n NAME_DICTIONARY_PREFIXES,\n nameLanguageOfDictionary,\n} from \"./dictionary-scope\";\n\n/**\n * Countries with bundled city dictionaries that are\n * loaded when no --countries scope is given.\n */\nconst DEFAULT_CITY_COUNTRIES = [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n] as const;\n\nconst availableNameLanguages = (): readonly string[] =>\n ALL_DICTIONARY_IDS.filter((id) =>\n id.startsWith(NAME_DICTIONARY_PREFIXES[0]),\n ).map((id) => id.slice(NAME_DICTIONARY_PREFIXES[0].length));\n\nconst validateLanguages = (\n languages: readonly string[],\n): readonly NameLanguage[] => {\n const available = availableNameLanguages();\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n // SAFETY: every entry was checked against the bundled\n // name dictionary ids, which define NameLanguage.\n return languages as readonly NameLanguage[];\n};\n\nexport type LoadCliDictionariesOptions = DictionaryScope;\n\n/**\n * Load the bundled @stll/anonymize-data dictionaries,\n * scoped to the requested languages and countries.\n */\nexport const loadCliDictionaries = async ({\n languages,\n countries,\n}: LoadCliDictionariesOptions): Promise<Dictionaries> => {\n const nameLanguages =\n languages === undefined ? undefined : validateLanguages(languages);\n\n const denyIds = ALL_DICTIONARY_IDS.filter((id) => {\n const meta = DICTIONARY_META[id];\n if (\n countries &&\n meta.country !== null &&\n !countries.includes(meta.country)\n ) {\n return false;\n }\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && nameLanguages !== undefined) {\n return nameLanguages.includes(\n // SAFETY: nameLang comes from a bundled dictionary\n // id, which defines NameLanguage.\n nameLang as NameLanguage,\n );\n }\n return true;\n });\n\n const cityCountries = countries ?? DEFAULT_CITY_COUNTRIES;\n\n const [names, denyEntries, cityEntries] = await Promise.all([\n loadNameDictionaries(nameLanguages),\n Promise.all(\n denyIds.map(async (id) => ({ id, entries: await loadDictionary(id) })),\n ),\n Promise.all(\n cityCountries.map(async (country) => ({\n country,\n entries: await loadCityDictionary(country),\n })),\n ),\n ]);\n\n const denyList: Record<string, readonly string[]> = {};\n const denyListMeta: Record<string, DictionaryMeta> = {};\n for (const { id, entries } of denyEntries) {\n denyList[id] = entries;\n // SAFETY: anonymize-data categories match\n // DenyListCategory at runtime.\n denyListMeta[id] = DICTIONARY_META[id] as DictionaryMeta;\n }\n\n const citiesByCountry: Record<string, readonly string[]> = {};\n for (const { country, entries } of cityEntries) {\n if (entries.length > 0) citiesByCountry[country] = entries;\n }\n\n return {\n firstNames: names.firstNames,\n surnames: names.surnames,\n denyList,\n denyListMeta,\n citiesByCountry,\n };\n};\n","","import { randomUUID } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport {\n type FileHandle,\n link,\n lstat,\n open,\n readFile,\n rename,\n unlink,\n} from \"node:fs/promises\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport type { NativeOpenSessionArchiveOptions } from \"@stll/anonymize\";\nimport {\n DOCX_COVERAGE_MODES,\n anonymizeDocx,\n restoreDocxText,\n type DocxAnonymizationSession,\n type DocxAnonymizationSummary,\n type DocxRestorationResult,\n type DocxRestorationSession,\n} from \"@stll/anonymize-docx\";\n\nimport { parseCountries, UsageError } from \"./args\";\n\nconst DOCX_SESSION_KEY_BYTES = 32;\nconst DOCX_SESSION_LOCK_SUFFIX = \".lock\";\nconst MAX_EPOCH_SECONDS = 4_294_967_295;\n\nconst DOCX_SESSION_MODES = {\n continue: \"continue\",\n create: \"create\",\n} as const;\n\ntype DocxSessionMode =\n (typeof DOCX_SESSION_MODES)[keyof typeof DOCX_SESSION_MODES];\n\ntype DocxDetectionOptions = {\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n};\n\ntype DocxCommonOptions = {\n inputPath: string;\n outputPath: string;\n sessionArchivePath: string;\n sessionKeyPath: string;\n sessionId: string;\n coverage: (typeof DOCX_COVERAGE_MODES)[keyof typeof DOCX_COVERAGE_MODES];\n observedAtEpochSeconds?: number | undefined;\n json: boolean;\n quiet: boolean;\n};\n\ntype DocxCommand =\n | { type: \"help\" }\n | ({\n type: \"anonymize\";\n sessionMode: DocxSessionMode;\n detection: DocxDetectionOptions;\n } & DocxCommonOptions)\n | ({ type: \"restore\" } & DocxCommonOptions);\n\nexport type DocxPipelineRequest =\n | { type: \"anonymize\"; detection: DocxDetectionOptions }\n | { type: \"restore\" };\n\ntype DocxCliSession = DocxAnonymizationSession &\n DocxRestorationSession & {\n toEncryptedArchive: (key: Uint8Array) => Uint8Array;\n toEncryptedArchiveAt: (\n key: Uint8Array,\n observedAtEpochSeconds: number,\n ) => Uint8Array;\n };\n\nexport type DocxCliPipeline = {\n createRedactionSession: (sessionId: string) => DocxCliSession;\n restoreEncryptedRedactionSession: (\n options: NativeOpenSessionArchiveOptions,\n ) => DocxCliSession;\n};\n\ntype RunDocxCommandOptions = {\n argv: readonly string[];\n preparePipeline: (request: DocxPipelineRequest) => Promise<DocxCliPipeline>;\n};\n\nconst DOCX_HELP = `Usage:\n anonymize docx anonymize [options] <input.docx>\n anonymize docx restore [options] <input.docx>\n\nAnonymize or restore one DOCX file with an encrypted redaction session.\nDocument and session outputs are written atomically and never overwrite the\ninput, key file, or an existing document output.\n\nRequired options:\n -o, --output <path> New DOCX output path\n --session-archive <path> Encrypted session archive path\n --session-key-file <path>\n File containing exactly 32 raw key bytes\n --session-id <id> Expected opaque session identity\n\nAnonymize options:\n --session-mode <mode> \"create\" or \"continue\" (required)\n --coverage <mode> \"require-full\" (default) or \"allow-partial\"\n --labels <list> Comma-separated entity labels\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n --countries <list> ISO 3166-1 alpha-2 country codes\n --threshold <n> Minimum confidence score 0-1 (default: 0.3)\n\nRestore options:\n --coverage <mode> \"require-full\" (default) or \"allow-partial\"\n\nCommon options:\n --observed-at <seconds> Deterministic Unix timestamp for lifecycle checks\n --json Print the aggregate audit-safe summary as JSON\n --quiet Suppress the human-readable stderr summary\n -h, --help Show this help\n\nThe session key is read from a file, never from a command argument. In create\nmode the archive path must not exist. Continue mode atomically replaces the\nexisting archive only after the DOCX rewrite succeeds. It holds an exclusive\n\"<archive>.lock\" sidecar throughout the continuation to prevent lost updates.\nCaller-supplied detection plans and interactive review are available through the\npackage API, not this CLI.\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseEpochSeconds = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 0 || value > MAX_EPOCH_SECONDS) {\n throw new UsageError(\n `--observed-at must be an integer from 0 to ${MAX_EPOCH_SECONDS}, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseCoverage = (\n raw: string | undefined,\n): DocxCommonOptions[\"coverage\"] => {\n const value = raw ?? DOCX_COVERAGE_MODES.requireFull;\n if (\n value === DOCX_COVERAGE_MODES.requireFull ||\n value === DOCX_COVERAGE_MODES.allowPartial\n ) {\n return value;\n }\n throw new UsageError(\n `--coverage must be one of: ${Object.values(DOCX_COVERAGE_MODES).join(\", \")}; got \"${value}\"`,\n );\n};\n\nconst parseSessionMode = (raw: string | undefined): DocxSessionMode => {\n if (raw === undefined) {\n throw new UsageError(\"--session-mode is required for DOCX anonymization\");\n }\n if (\n raw === DOCX_SESSION_MODES.create ||\n raw === DOCX_SESSION_MODES.continue\n ) {\n return raw;\n }\n throw new UsageError(\n `--session-mode must be one of: ${Object.values(DOCX_SESSION_MODES).join(\", \")}; got \"${raw}\"`,\n );\n};\n\nconst required = (value: string | undefined, flag: string): string => {\n if (value === undefined || value.length === 0) {\n throw new UsageError(`${flag} is required for DOCX workflows`);\n }\n return value;\n};\n\ntype ParsedCommonValues = {\n output?: string | undefined;\n \"session-archive\"?: string | undefined;\n \"session-key-file\"?: string | undefined;\n \"session-id\"?: string | undefined;\n coverage?: string | undefined;\n \"observed-at\"?: string | undefined;\n json?: boolean | undefined;\n quiet?: boolean | undefined;\n};\n\nconst commonOptions = (\n values: ParsedCommonValues,\n positionals: readonly string[],\n): DocxCommonOptions => {\n if (positionals.length !== 1) {\n throw new UsageError(\"DOCX workflows require exactly one input file\");\n }\n const inputPath = positionals.at(0);\n if (inputPath === undefined) {\n throw new UsageError(\"DOCX workflows require exactly one input file\");\n }\n return {\n inputPath,\n outputPath: required(values.output, \"--output\"),\n sessionArchivePath: required(\n values[\"session-archive\"],\n \"--session-archive\",\n ),\n sessionKeyPath: required(values[\"session-key-file\"], \"--session-key-file\"),\n sessionId: required(values[\"session-id\"], \"--session-id\"),\n coverage: parseCoverage(values.coverage),\n observedAtEpochSeconds:\n values[\"observed-at\"] === undefined\n ? undefined\n : parseEpochSeconds(values[\"observed-at\"]),\n json: values.json === true,\n quiet: values.quiet === true,\n };\n};\n\nconst parseDocxCommand = (argv: readonly string[]): DocxCommand => {\n const action = argv.at(0);\n if (action === undefined || action === \"--help\" || action === \"-h\") {\n return { type: \"help\" };\n }\n const args = argv.slice(1);\n if (action === \"anonymize\") {\n let parsed: ReturnType<typeof parseArgs<typeof DOCX_ANONYMIZE_CONFIG>>;\n try {\n parsed = parseArgs({ ...DOCX_ANONYMIZE_CONFIG, args: [...args] });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n if (parsed.values.help === true) {\n return { type: \"help\" };\n }\n return {\n type: \"anonymize\",\n ...commonOptions(parsed.values, parsed.positionals),\n sessionMode: parseSessionMode(parsed.values[\"session-mode\"]),\n detection: {\n labels:\n parsed.values.labels === undefined\n ? undefined\n : splitList(parsed.values.labels),\n languages:\n parsed.values.languages === undefined\n ? undefined\n : splitList(parsed.values.languages),\n countries:\n parsed.values.countries === undefined\n ? undefined\n : parseCountries(parsed.values.countries),\n threshold:\n parsed.values.threshold === undefined\n ? 0.3\n : parseThreshold(parsed.values.threshold),\n },\n };\n }\n if (action === \"restore\") {\n let parsed: ReturnType<typeof parseArgs<typeof DOCX_RESTORE_CONFIG>>;\n try {\n parsed = parseArgs({ ...DOCX_RESTORE_CONFIG, args: [...args] });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n if (parsed.values.help === true) {\n return { type: \"help\" };\n }\n return {\n type: \"restore\",\n ...commonOptions(parsed.values, parsed.positionals),\n };\n }\n throw new UsageError(\n `unknown DOCX action \"${action}\"; expected \"anonymize\" or \"restore\"`,\n );\n};\n\nconst DOCX_COMMON_PARSE_OPTIONS = {\n output: { type: \"string\", short: \"o\" },\n \"session-archive\": { type: \"string\" },\n \"session-key-file\": { type: \"string\" },\n \"session-id\": { type: \"string\" },\n coverage: { type: \"string\" },\n \"observed-at\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n} as const;\n\nconst DOCX_ANONYMIZE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n ...DOCX_COMMON_PARSE_OPTIONS,\n \"session-mode\": { type: \"string\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n },\n} as const;\n\nconst DOCX_RESTORE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: DOCX_COMMON_PARSE_OPTIONS,\n} as const;\n\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst sessionArchiveLockPath = (archivePath: string): string =>\n `${canonicalPath(archivePath)}${DOCX_SESSION_LOCK_SUFFIX}`;\n\nconst assertDistinctPaths = (\n paths: readonly { path: string; flag: string }[],\n): void => {\n const seen = new Map<string, string>();\n for (const entry of paths) {\n const canonical = canonicalPath(entry.path);\n const existing = seen.get(canonical);\n if (existing !== undefined) {\n throw new UsageError(`${entry.flag} collides with ${existing}`);\n }\n seen.set(canonical, `${entry.flag} \"${entry.path}\"`);\n }\n};\n\nconst isNodeError = (\n error: unknown,\n code: string,\n): error is NodeJS.ErrnoException =>\n error instanceof Error && \"code\" in error && error.code === code;\n\nconst assertPathDoesNotExist = async (\n path: string,\n flag: string,\n): Promise<void> => {\n try {\n await lstat(path);\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return;\n }\n throw error;\n }\n throw new UsageError(`${flag} refuses to overwrite existing path \"${path}\"`);\n};\n\nconst preflightDocxCommand = async (\n command: Exclude<DocxCommand, { type: \"help\" }>,\n): Promise<void> => {\n const paths = [\n { path: command.inputPath, flag: \"input\" },\n { path: command.outputPath, flag: \"--output\" },\n { path: command.sessionArchivePath, flag: \"--session-archive\" },\n { path: command.sessionKeyPath, flag: \"--session-key-file\" },\n ];\n if (\n command.type === \"anonymize\" &&\n command.sessionMode === DOCX_SESSION_MODES.continue\n ) {\n paths.push({\n path: sessionArchiveLockPath(command.sessionArchivePath),\n flag: \"session archive lock\",\n });\n }\n assertDistinctPaths(paths);\n await assertPathDoesNotExist(command.outputPath, \"--output\");\n if (\n command.type === \"anonymize\" &&\n command.sessionMode === DOCX_SESSION_MODES.create\n ) {\n await assertPathDoesNotExist(\n command.sessionArchivePath,\n \"--session-archive\",\n );\n }\n};\n\ntype SessionArchiveLock = {\n release: () => Promise<void>;\n};\n\ntype OperationResult =\n | { type: \"succeeded\" }\n | { type: \"failed\"; error: unknown };\n\nconst captureOperationResult = async (\n operation: Promise<void>,\n): Promise<OperationResult> => {\n try {\n await operation;\n return { type: \"succeeded\" };\n } catch (error) {\n return { type: \"failed\", error };\n }\n};\n\nconst acquireSessionArchiveLock = async (\n archivePath: string,\n): Promise<SessionArchiveLock> => {\n const lockPath = sessionArchiveLockPath(archivePath);\n let handle: FileHandle;\n try {\n handle = await open(lockPath, \"wx\", 0o600);\n } catch (error) {\n if (isNodeError(error, \"EEXIST\")) {\n throw new Error(\n `encrypted session archive is locked by another continuation; if no process is running, remove the stale lock \"${lockPath}\"`,\n );\n }\n throw error;\n }\n return {\n release: async () => {\n const closeResult = await captureOperationResult(handle.close());\n const unlinkResult = await captureOperationResult(unlink(lockPath));\n if (closeResult.type === \"failed\") {\n throw closeResult.error;\n }\n if (\n unlinkResult.type === \"failed\" &&\n !isNodeError(unlinkResult.error, \"ENOENT\")\n ) {\n throw unlinkResult.error;\n }\n },\n };\n};\n\nconst readSessionKey = async (path: string): Promise<Uint8Array> => {\n const handle = await open(path, \"r\");\n let key: Uint8Array | undefined;\n try {\n const stats = await handle.stat();\n if (!stats.isFile()) {\n throw new UsageError(\"--session-key-file must be a regular file\");\n }\n if (process.platform !== \"win32\" && (stats.mode & 0o077) !== 0) {\n throw new UsageError(\n \"--session-key-file must not grant permissions to group or other users (use chmod 600)\",\n );\n }\n key = await handle.readFile();\n if (key.byteLength !== DOCX_SESSION_KEY_BYTES) {\n throw new UsageError(\n `--session-key-file must contain exactly ${DOCX_SESSION_KEY_BYTES} raw bytes`,\n );\n }\n await handle.close();\n return key;\n } catch (error) {\n key?.fill(0);\n try {\n await handle.close();\n } catch {\n // Preserve the validation or read error.\n }\n throw error;\n }\n};\n\nconst removeStagedFile = async (path: string | undefined): Promise<void> => {\n if (path === undefined) {\n return;\n }\n try {\n await unlink(path);\n } catch {\n // Best-effort cleanup must not hide the original operation error.\n }\n};\n\nconst stageFile = async (\n target: string,\n content: Uint8Array,\n): Promise<string> => {\n const temporary = join(\n dirname(target),\n `.${basename(target)}.${randomUUID()}.tmp`,\n );\n const handle = await open(temporary, \"wx\", 0o600);\n try {\n await handle.writeFile(content);\n await handle.sync();\n await handle.close();\n } catch (error) {\n try {\n await handle.close();\n } catch {\n // Preserve the write error while cleanup remains best effort.\n }\n await removeStagedFile(temporary);\n throw error;\n }\n return temporary;\n};\n\nconst publishNewFile = async (\n temporary: string,\n target: string,\n flag: string,\n): Promise<void> => {\n try {\n await link(temporary, target);\n } catch (error) {\n if (isNodeError(error, \"EEXIST\")) {\n throw new UsageError(\n `${flag} refuses to overwrite existing path \"${target}\"`,\n );\n }\n throw error;\n }\n await removeStagedFile(temporary);\n};\n\nconst publishReplacement = async (\n temporary: string,\n target: string,\n): Promise<void> => {\n await rename(temporary, target);\n};\n\nconst sessionArchive = (\n session: DocxCliSession,\n key: Uint8Array,\n observedAtEpochSeconds: number | undefined,\n): Uint8Array =>\n observedAtEpochSeconds === undefined\n ? session.toEncryptedArchive(key)\n : session.toEncryptedArchiveAt(key, observedAtEpochSeconds);\n\nconst outputSummary = (\n command: Pick<DocxCommonOptions, \"json\" | \"quiet\">,\n action: \"anonymized\" | \"restored\",\n summary: DocxAnonymizationSummary | Omit<DocxRestorationResult, \"document\">,\n): void => {\n if (command.json) {\n process.stdout.write(`${JSON.stringify(summary, null, 2)}\\n`);\n }\n if (command.quiet) {\n return;\n }\n const coverage = summary.coverage.status;\n if (\"entityCount\" in summary) {\n process.stderr.write(\n `anonymize: DOCX ${action}: ${summary.entityCount} entities, ${summary.appliedReplacementCount} replacements, ${coverage} coverage\\n`,\n );\n return;\n }\n process.stderr.write(\n `anonymize: DOCX ${action}: ${summary.restoredPlaceholderCount} placeholders, ${coverage} coverage\\n`,\n );\n};\n\nconst openSession = (\n pipeline: DocxCliPipeline,\n command: Pick<DocxCommonOptions, \"sessionId\" | \"observedAtEpochSeconds\">,\n archive: Uint8Array,\n key: Uint8Array,\n): DocxCliSession =>\n pipeline.restoreEncryptedRedactionSession({\n archive,\n key,\n expectedSessionId: command.sessionId,\n ...(command.observedAtEpochSeconds === undefined\n ? {}\n : { observedAtEpochSeconds: command.observedAtEpochSeconds }),\n });\n\nconst runDocxAnonymize = async (\n command: Extract<DocxCommand, { type: \"anonymize\" }>,\n pipeline: DocxCliPipeline,\n): Promise<void> => {\n const archivePath =\n command.sessionMode === DOCX_SESSION_MODES.continue\n ? canonicalPath(command.sessionArchivePath)\n : command.sessionArchivePath;\n const archiveLock =\n command.sessionMode === DOCX_SESSION_MODES.continue\n ? await acquireSessionArchiveLock(archivePath)\n : undefined;\n let workflowResult: OperationResult = { type: \"succeeded\" };\n let lockReleaseResult: OperationResult = { type: \"succeeded\" };\n let key: Uint8Array | undefined;\n let documentTemporary: string | undefined;\n let archiveTemporary: string | undefined;\n try {\n key = await readSessionKey(command.sessionKeyPath);\n const [document, existingArchive] = await Promise.all([\n readFile(command.inputPath),\n command.sessionMode === DOCX_SESSION_MODES.continue\n ? readFile(archivePath)\n : Promise.resolve(undefined),\n ]);\n const session =\n existingArchive === undefined\n ? pipeline.createRedactionSession(command.sessionId)\n : openSession(pipeline, command, existingArchive, key);\n const result = anonymizeDocx({\n document,\n session,\n expectedSessionId: command.sessionId,\n policy: { coverage: { mode: command.coverage } },\n ...(command.observedAtEpochSeconds === undefined\n ? {}\n : { observedAtEpochSeconds: command.observedAtEpochSeconds }),\n });\n const encryptedArchive = sessionArchive(\n session,\n key,\n command.observedAtEpochSeconds,\n );\n documentTemporary = await stageFile(command.outputPath, result.document);\n archiveTemporary = await stageFile(archivePath, encryptedArchive);\n if (command.sessionMode === DOCX_SESSION_MODES.create) {\n await publishNewFile(\n archiveTemporary,\n command.sessionArchivePath,\n \"--session-archive\",\n );\n } else {\n await publishReplacement(archiveTemporary, archivePath);\n }\n archiveTemporary = undefined;\n try {\n await publishNewFile(documentTemporary, command.outputPath, \"--output\");\n documentTemporary = undefined;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `encrypted session archive was updated, but DOCX output could not be published: ${message}`,\n );\n }\n outputSummary(command, \"anonymized\", result.summary);\n } catch (error) {\n workflowResult = { type: \"failed\", error };\n } finally {\n key?.fill(0);\n await Promise.all([\n removeStagedFile(documentTemporary),\n removeStagedFile(archiveTemporary),\n ]);\n if (archiveLock !== undefined) {\n lockReleaseResult = await captureOperationResult(archiveLock.release());\n }\n }\n if (workflowResult.type === \"failed\") {\n throw workflowResult.error;\n }\n if (lockReleaseResult.type === \"failed\") {\n const message =\n lockReleaseResult.error instanceof Error\n ? lockReleaseResult.error.message\n : String(lockReleaseResult.error);\n throw new Error(\n `DOCX and session outputs were published, but the session archive lock could not be released: ${message}`,\n );\n }\n};\n\nconst runDocxRestore = async (\n command: Extract<DocxCommand, { type: \"restore\" }>,\n pipeline: DocxCliPipeline,\n): Promise<void> => {\n const key = await readSessionKey(command.sessionKeyPath);\n try {\n const [document, archive] = await Promise.all([\n readFile(command.inputPath),\n readFile(command.sessionArchivePath),\n ]);\n const session = openSession(pipeline, command, archive, key);\n const result = restoreDocxText({\n document,\n session,\n expectedSessionId: command.sessionId,\n ...(command.observedAtEpochSeconds === undefined\n ? {}\n : { observedAtEpochSeconds: command.observedAtEpochSeconds }),\n });\n if (\n command.coverage === DOCX_COVERAGE_MODES.requireFull &&\n result.coverage.status === \"partial\"\n ) {\n throw new Error(\n \"DOCX contains content outside the fully supported restoration coverage\",\n );\n }\n const temporary = await stageFile(command.outputPath, result.document);\n try {\n await publishNewFile(temporary, command.outputPath, \"--output\");\n } catch (error) {\n await removeStagedFile(temporary);\n throw error;\n }\n const summary: Omit<DocxRestorationResult, \"document\"> = {\n sessionId: result.sessionId,\n restoredBlockCount: result.restoredBlockCount,\n restoredPlaceholderCount: result.restoredPlaceholderCount,\n coverage: result.coverage,\n };\n outputSummary(command, \"restored\", summary);\n } finally {\n key.fill(0);\n }\n};\n\nexport const runDocxCommand = async ({\n argv,\n preparePipeline,\n}: RunDocxCommandOptions): Promise<void> => {\n const command = parseDocxCommand(argv);\n if (command.type === \"help\") {\n process.stdout.write(DOCX_HELP);\n return;\n }\n await preflightDocxCommand(command);\n const pipeline = await preparePipeline(\n command.type === \"anonymize\"\n ? { type: \"anonymize\", detection: command.detection }\n : { type: \"restore\" },\n );\n if (command.type === \"anonymize\") {\n await runDocxAnonymize(command, pipeline);\n return;\n }\n await runDocxRestore(command, pipeline);\n};\n","import { randomUUID } from \"node:crypto\";\nimport { constants, realpathSync } from \"node:fs\";\nimport { link, lstat, open, unlink } from \"node:fs/promises\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport type { PreparedNativePipeline } from \"@stll/anonymize\";\nimport {\n anonymizePdfRaster,\n PDF_DOCUMENT_MAX_BYTES,\n renderPdfWithPopplerTesseract,\n} from \"@stll/anonymize-pdf\";\n\nimport { parseCountries, UsageError } from \"./args\";\n\nexport type PdfDetectionOptions = {\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n};\n\nexport type PdfCliPipeline = Pick<\n PreparedNativePipeline,\n \"redactText\" | \"redactTextWithCallerDetections\"\n>;\n\nexport type PdfPipelineRequest = { detection: PdfDetectionOptions };\n\ntype RunPdfCommandOptions = {\n argv: readonly string[];\n preparePipeline: (request: PdfPipelineRequest) => Promise<PdfCliPipeline>;\n};\n\ntype PdfCommand =\n | { type: \"help\" }\n | {\n type: \"anonymize\";\n inputPath: string;\n outputPath: string;\n ocrLanguage: string;\n dpi: number;\n timeoutMs: number;\n pdftoppmPath?: string | undefined;\n tesseractPath?: string | undefined;\n fillRgb: readonly [number, number, number];\n detection: PdfDetectionOptions;\n json: boolean;\n quiet: boolean;\n };\n\nconst PDF_HELP = `Usage:\n anonymize pdf anonymize [options] <input.pdf>\n\nRender and OCR every page locally, run stella detection, and write a verified,\nfresh image-only PDF. The command never overwrites the input or an existing\noutput and rejects symlink inputs. Searchability, accessibility, signatures,\nforms, links, metadata, attachments, and other interactive structure are\ndeliberately removed.\n\nRequired options:\n -o, --output <path> New PDF output path\n --ocr-language <pack> One installed Tesseract pack, e.g. \"eng\"\n\nProvider options:\n --dpi <n> Integer render DPI from 72 to 600 (default: 300)\n --pdftoppm <path> Poppler executable (default: pdftoppm on PATH)\n --tesseract <path> Tesseract executable (default: tesseract on PATH)\n --timeout-ms <n> Per-process timeout, 100-300000 (default: 120000)\n --fill-rgb <r,g,b> Destructive fill color (default: 0,0,0)\n\nDetection options:\n --labels <list> Comma-separated entity labels\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n --countries <list> ISO 3166-1 alpha-2 country codes\n --threshold <n> Minimum confidence score 0-1 (default: 0.3)\n\nOutput options:\n --json Print the aggregate verification certificate\n --quiet Suppress the human-readable stderr summary\n -h, --help Show this help\n\nThe OCR language is explicit and singular. The certificate proves a fresh\nimage-only structure and requested pixel rewrite; it does not prove perfect OCR\nor detector recall and always reports piiCleanGuaranteed=false.\n`;\n\nconst PDF_PARSE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n output: { type: \"string\", short: \"o\" },\n \"ocr-language\": { type: \"string\" },\n dpi: { type: \"string\" },\n pdftoppm: { type: \"string\" },\n tesseract: { type: \"string\" },\n \"timeout-ms\": { type: \"string\" },\n \"fill-rgb\": { type: \"string\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n} as const;\n\nconst required = (value: string | undefined, flag: string): string => {\n if (!value) throw new UsageError(`${flag} is required for PDF anonymization`);\n return value;\n};\n\nconst integerOption = (\n value: string | undefined,\n flag: string,\n fallback: number,\n minimum: number,\n maximum: number,\n): number => {\n if (value === undefined) return fallback;\n const parsed = Number(value);\n if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {\n throw new UsageError(\n `${flag} must be an integer from ${minimum} to ${maximum}`,\n );\n }\n return parsed;\n};\n\nconst thresholdOption = (value: string | undefined): number => {\n if (value === undefined) return 0.3;\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {\n throw new UsageError(\"--threshold must be a number from 0 to 1\");\n }\n return parsed;\n};\n\nconst listOption = (value: string | undefined): string[] | undefined =>\n value === undefined\n ? undefined\n : [\n ...new Set(\n value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean),\n ),\n ];\n\nconst fillOption = (\n value: string | undefined,\n): readonly [number, number, number] => {\n if (value === undefined) return [0, 0, 0];\n const channels = value.split(\",\").map(Number);\n if (\n channels.length !== 3 ||\n channels.some(\n (channel) => !Number.isInteger(channel) || channel < 0 || channel > 255,\n )\n ) {\n throw new UsageError(\n \"--fill-rgb must contain three integers from 0 to 255\",\n );\n }\n return [channels[0] ?? 0, channels[1] ?? 0, channels[2] ?? 0];\n};\n\nconst parsePdfCommand = (argv: readonly string[]): PdfCommand => {\n const action = argv.at(0);\n if (action === undefined || action === \"--help\" || action === \"-h\") {\n return { type: \"help\" };\n }\n if (action !== \"anonymize\") {\n throw new UsageError(\n `unknown PDF action \"${action}\"; expected \"anonymize\"`,\n );\n }\n let parsed: ReturnType<typeof parseArgs<typeof PDF_PARSE_CONFIG>>;\n try {\n parsed = parseArgs({ ...PDF_PARSE_CONFIG, args: argv.slice(1) });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n if (parsed.values.help === true) return { type: \"help\" };\n if (parsed.positionals.length !== 1 || parsed.positionals[0] === undefined) {\n throw new UsageError(\"PDF anonymization requires exactly one input file\");\n }\n return {\n type: \"anonymize\",\n inputPath: parsed.positionals[0],\n outputPath: required(parsed.values.output, \"--output\"),\n ocrLanguage: required(parsed.values[\"ocr-language\"], \"--ocr-language\"),\n dpi: integerOption(parsed.values.dpi, \"--dpi\", 300, 72, 600),\n timeoutMs: integerOption(\n parsed.values[\"timeout-ms\"],\n \"--timeout-ms\",\n 120_000,\n 100,\n 300_000,\n ),\n pdftoppmPath: parsed.values.pdftoppm,\n tesseractPath: parsed.values.tesseract,\n fillRgb: fillOption(parsed.values[\"fill-rgb\"]),\n detection: {\n labels: listOption(parsed.values.labels),\n languages: listOption(parsed.values.languages),\n countries:\n parsed.values.countries === undefined\n ? undefined\n : parseCountries(parsed.values.countries),\n threshold: thresholdOption(parsed.values.threshold),\n },\n json: parsed.values.json === true,\n quiet: parsed.values.quiet === true,\n };\n};\n\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst isNodeError = (\n error: unknown,\n code: string,\n): error is NodeJS.ErrnoException =>\n error instanceof Error && \"code\" in error && error.code === code;\n\nconst preflight = async (\n command: Extract<PdfCommand, { type: \"anonymize\" }>,\n): Promise<void> => {\n if (canonicalPath(command.inputPath) === canonicalPath(command.outputPath)) {\n throw new UsageError(\"--output must not overwrite the PDF input\");\n }\n let input;\n try {\n input = await lstat(command.inputPath);\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n throw new UsageError(\"PDF input must be a regular non-symlink file\");\n }\n throw error;\n }\n if (!input.isFile() || input.isSymbolicLink()) {\n throw new UsageError(\"PDF input must be a regular non-symlink file\");\n }\n try {\n await lstat(command.outputPath);\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) return;\n throw error;\n }\n throw new UsageError(\"--output refuses to overwrite an existing path\");\n};\n\nconst readRegularInput = async (path: string): Promise<Uint8Array> => {\n let handle;\n try {\n handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);\n } catch (error) {\n if (isNodeError(error, \"ELOOP\")) {\n throw new UsageError(\"PDF input must be a regular non-symlink file\");\n }\n throw error;\n }\n try {\n const opened = await handle.stat();\n if (\n !opened.isFile() ||\n !Number.isSafeInteger(opened.size) ||\n opened.size > PDF_DOCUMENT_MAX_BYTES\n ) {\n throw new UsageError(\n `PDF input must be a regular file no larger than ${PDF_DOCUMENT_MAX_BYTES} bytes`,\n );\n }\n const document = Buffer.allocUnsafe(opened.size);\n let offset = 0;\n while (offset < document.length) {\n const { bytesRead } = await handle.read(\n document,\n offset,\n document.length - offset,\n offset,\n );\n if (bytesRead === 0) {\n throw new UsageError(\"PDF input changed while it was being read\");\n }\n offset += bytesRead;\n }\n const sentinel = Buffer.allocUnsafe(1);\n const { bytesRead: trailingBytes } = await handle.read(\n sentinel,\n 0,\n 1,\n offset,\n );\n const current = await lstat(path);\n if (\n trailingBytes !== 0 ||\n !current.isFile() ||\n current.isSymbolicLink() ||\n opened.dev !== current.dev ||\n opened.ino !== current.ino\n ) {\n throw new UsageError(\"PDF input changed during validation\");\n }\n return document;\n } finally {\n await handle.close();\n }\n};\n\nconst removeStaged = async (path: string): Promise<void> => {\n try {\n await unlink(path);\n } catch (error) {\n if (!isNodeError(error, \"ENOENT\")) throw error;\n }\n};\n\nconst publishNewFile = async (\n target: string,\n content: Uint8Array,\n): Promise<void> => {\n const temporary = join(\n dirname(target),\n `.${basename(target)}.${randomUUID()}.tmp`,\n );\n const handle = await open(temporary, \"wx\", 0o600);\n try {\n await handle.writeFile(content);\n await handle.sync();\n await handle.close();\n try {\n await link(temporary, target);\n } catch (error) {\n if (isNodeError(error, \"EEXIST\")) {\n throw new UsageError(\"--output refuses to overwrite an existing path\");\n }\n throw error;\n }\n } catch (error) {\n try {\n await handle.close();\n } catch {\n // Preserve the primary publication error.\n }\n throw error;\n } finally {\n await removeStaged(temporary);\n }\n};\n\nexport const runPdfCommand = async ({\n argv,\n preparePipeline,\n}: RunPdfCommandOptions): Promise<void> => {\n const command = parsePdfCommand(argv);\n if (command.type === \"help\") {\n process.stdout.write(PDF_HELP);\n return;\n }\n await preflight(command);\n const document = await readRegularInput(command.inputPath);\n const pipeline = await preparePipeline({ detection: command.detection });\n const observed = await renderPdfWithPopplerTesseract({\n document,\n ocrLanguage: command.ocrLanguage,\n dpi: command.dpi,\n timeoutMs: command.timeoutMs,\n pdftoppmPath: command.pdftoppmPath,\n tesseractPath: command.tesseractPath,\n });\n const result = anonymizePdfRaster({\n document,\n pipeline,\n provider: observed.provider,\n pages: observed.pages,\n fillRgb: command.fillRgb,\n });\n await publishNewFile(command.outputPath, result.document);\n if (command.json) {\n process.stdout.write(`${JSON.stringify(result.certificate, null, 2)}\\n`);\n }\n if (!command.quiet) {\n process.stderr.write(\n `anonymize: PDF anonymized: ${result.certificate.pageCount} pages, ${result.certificate.detectionCount} detections, PII-clean guarantee=false\\n`,\n );\n }\n};\n","import { realpathSync } from \"node:fs\";\nimport {\n mkdir,\n open,\n readdir,\n readFile,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { basename, dirname, join, relative, resolve } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\n\nimport type {\n deanonymise,\n Dictionaries,\n exportRedactionKey,\n NativeAnonymizeBinding,\n NativeOperatorConfig,\n NativePipelineBuildOptions,\n OperatorType,\n PipelineConfig,\n PreparedNativePipeline,\n} from \"@stll/anonymize\";\nimport { CAPABILITY_MANIFEST } from \"@stll/anonymize/capabilities\";\nimport {\n DEFAULT_ENTITY_LABELS,\n ENTITY_LABELS,\n type EntityLabel,\n} from \"@stll/anonymize/constants\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nimport type { CliOptions } from \"./args\";\nimport {\n DEFAULT_THRESHOLD,\n HELP,\n parseCliArgs,\n parseCountries,\n UsageError,\n} from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n type DocxCliPipeline,\n type DocxPipelineRequest,\n runDocxCommand,\n} from \"./docx\";\nimport {\n type PdfCliPipeline,\n type PdfPipelineRequest,\n runPdfCommand,\n} from \"./pdf\";\n\n/**\n * The pipeline functions the CLI needs, backed by the\n * @stll/anonymize native SDK: a binding loader and the\n * config-to-pipeline builder, plus the redaction-key\n * helpers used by the deanonymise path.\n */\nexport type AnonymizeApi = {\n deanonymise: typeof deanonymise;\n exportRedactionKey: typeof exportRedactionKey;\n createNativePipelineFromConfig: (\n options: NativePipelineBuildOptions,\n ) => Promise<NativeCliPipeline>;\n loadNativeAnonymizeBinding: () => NativeAnonymizeBinding;\n};\n\n/**\n * Everything an entry point injects: the pipeline engine\n * and the dictionary source (the @stll/anonymize-data\n * package for the npm bin).\n */\nexport type CliEngine = {\n api: AnonymizeApi;\n loadDictionaries: (scope: DictionaryScope) => Promise<Dictionaries>;\n};\n\n// Statically imported so the version is baked into both\n// the npm bundle and the compiled binary; a runtime\n// package.json lookup would fail inside the binary's\n// virtual filesystem.\nconst cliVersion = (): string => pkg.version;\n\n/**\n * Filesystem identity of a path: realpath when it exists\n * (so symlinks to the same file compare equal), lexical\n * resolution otherwise (the file may not exist yet).\n */\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst readStdin = async (): Promise<string> => {\n process.stdin.setEncoding(\"utf8\");\n let text = \"\";\n for await (const chunk of process.stdin) text += chunk;\n return text;\n};\n\ntype NamedInput = {\n /** Source path, or null when reading stdin. */\n path: string | null;\n text: string;\n};\n\nconst readInputs = async (files: string[]): Promise<NamedInput[]> => {\n if (files.length === 0) {\n if (process.stdin.isTTY) {\n throw new UsageError(\n \"no input files and stdin is a terminal (see --help)\",\n );\n }\n return [{ path: null, text: await readStdin() }];\n }\n return Promise.all(\n files.map(async (path) => ({ path, text: await readFile(path, \"utf8\") })),\n );\n};\n\n/**\n * One file to anonymize in a batch run: the source path to\n * read and the path, relative to the output directory, to\n * write. For a plain file argument the relative path is the\n * basename; for a directory argument the input tree is\n * mirrored, so it is the path relative to that directory.\n */\ntype FileJob = {\n path: string;\n outputRelative: string;\n};\n\n/** Result of expanding the positional arguments into concrete\n * files. `batch` is true when the output must be a directory:\n * more than one file, or any directory argument. */\ntype ExpandedInputs = {\n jobs: FileJob[];\n batch: boolean;\n /** Likely-binary files skipped during directory walks. */\n skipped: number;\n};\n\n// Sniff window for the binary check. A regular text file never\n// contains a NUL byte; binaries (images, archives) reliably do.\nconst TEXT_SNIFF_BYTES = 8192;\n\n/**\n * True when the file's first {@link TEXT_SNIFF_BYTES} bytes\n * contain no NUL byte. Used to skip binaries discovered by a\n * directory walk without reading the whole file.\n */\nconst looksTextual = async (path: string): Promise<boolean> => {\n const handle = await open(path, \"r\");\n try {\n const buffer = Buffer.alloc(TEXT_SNIFF_BYTES);\n const { bytesRead } = await handle.read(buffer, 0, TEXT_SNIFF_BYTES, 0);\n return buffer.subarray(0, bytesRead).indexOf(0) === -1;\n } finally {\n await handle.close();\n }\n};\n\n/**\n * Collect regular files under `root`, sorted for deterministic\n * order. Symlinks are skipped (avoids cycles and escaping the\n * tree); subdirectories are descended only when `recursive`.\n */\nconst walkDirectory = async (\n root: string,\n recursive: boolean,\n excludeDir?: string,\n): Promise<string[]> => {\n const found: string[] = [];\n const visit = async (dir: string): Promise<void> => {\n const entries = (await readdir(dir, { withFileTypes: true })).toSorted(\n (a, b) => a.name.localeCompare(b.name),\n );\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n // Never descend into the output tree: rerunning with --output inside\n // the input directory must not ingest previously generated files.\n if (excludeDir !== undefined && resolve(full) === excludeDir) continue;\n if (recursive) await visit(full);\n } else if (entry.isFile()) {\n found.push(full);\n }\n }\n };\n await visit(root);\n return found;\n};\n\n/**\n * Expand positional arguments into concrete file jobs. A file\n * argument becomes one job (always processed); a directory is\n * walked, mirroring its tree into the output and skipping\n * likely-binary files.\n */\nconst expandInputs = async (\n files: readonly string[],\n recursive: boolean,\n outputDir?: string,\n): Promise<ExpandedInputs> => {\n const excludeDir = outputDir === undefined ? undefined : resolve(outputDir);\n const jobs: FileJob[] = [];\n let hasDirectory = false;\n let skipped = 0;\n for (const path of files) {\n // A stat failure (missing path, permission error) is not\n // fatal here: treat it as a file job so the read failure is\n // reported per file. A single such job stays single-input\n // and surfaces the error as a runtime exit; in a batch it is\n // counted as a failed file.\n let stats: Awaited<ReturnType<typeof stat>> | undefined;\n try {\n stats = await stat(path);\n } catch {\n jobs.push({ path, outputRelative: basename(path) });\n continue;\n }\n if (!stats.isDirectory()) {\n jobs.push({ path, outputRelative: basename(path) });\n continue;\n }\n hasDirectory = true;\n for (const file of await walkDirectory(path, recursive, excludeDir)) {\n // A file that disappears or turns unreadable mid-walk is queued anyway:\n // the per-file worker try/catch counts it as failed without aborting\n // the batch. Only a successful sniff that says \"binary\" skips it.\n const textual = await looksTextual(file).catch(() => true);\n if (!textual) {\n skipped += 1;\n continue;\n }\n jobs.push({ path: file, outputRelative: relative(path, file) });\n }\n }\n return { jobs, batch: hasDirectory || jobs.length > 1, skipped };\n};\n\n/**\n * Run `task` over `items` with at most `workers` in flight.\n * The shared native pipeline makes each redaction a synchronous\n * native call, so concurrency here only overlaps async file\n * I/O; the increments below are safe without locking because\n * no `await` sits between the read and the write of `next`.\n */\nconst runPool = async <T>(\n items: readonly T[],\n workers: number,\n task: (item: T) => Promise<void>,\n): Promise<void> => {\n let next = 0;\n const worker = async (): Promise<void> => {\n while (next < items.length) {\n const index = next;\n next += 1;\n // SAFETY: index < items.length checked above.\n await task(items[index] as T);\n }\n };\n const count = Math.max(1, Math.min(workers, items.length));\n await Promise.all(Array.from({ length: count }, worker));\n};\n\ntype CliEntity = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n source: string;\n};\n\ntype CliRedactionResult = {\n redactedText: string;\n redactionMap: Map<string, string>;\n operatorMap: Map<string, OperatorType>;\n entityCount: number;\n};\n\ntype NativeCliPipeline = DocxCliPipeline & {\n warmLazyRegex?: () => void;\n redactTextWithCallerDetections: PreparedNativePipeline[\"redactTextWithCallerDetections\"];\n redactText: (\n fullText: string,\n operators?: NativeOperatorConfig,\n ) => {\n resolvedEntities: CliEntity[];\n redaction: CliRedactionResult;\n };\n};\n\n// Short aliases for the canonical multi-word labels so that\n// `--labels person,email,iban` works without quoting the space\n// in \"email address\". Separator-insensitive resolution (below)\n// additionally accepts hyphen/underscore forms such as\n// \"credit-card-number\".\nconst LABEL_ALIASES: Record<string, EntityLabel> = {\n email: \"email address\",\n phone: \"phone number\",\n org: \"organization\",\n organisation: \"organization\",\n dob: \"date of birth\",\n ssn: \"social security number\",\n \"tax id\": \"tax identification number\",\n passport: \"passport number\",\n \"credit card\": \"credit card number\",\n \"national id\": \"national identification number\",\n};\n\nconst LABEL_SEPARATOR_RE = /[\\s_-]+/g;\nconst ENTITY_LABEL_SET: ReadonlySet<string> = new Set(ENTITY_LABELS);\n\nconst isEntityLabel = (label: string): label is EntityLabel =>\n ENTITY_LABEL_SET.has(label);\n\n/**\n * Resolve a user-supplied label token to a canonical label.\n * Lowercases and collapses separators, then maps known short\n * aliases. Unknown tokens are returned normalized so the\n * caller can report them verbatim.\n */\nconst canonicalizeLabel = (raw: string): string => {\n const normalized = raw.toLowerCase().replace(LABEL_SEPARATOR_RE, \" \").trim();\n const known: readonly string[] = ENTITY_LABELS;\n if (known.includes(normalized)) {\n return normalized;\n }\n return LABEL_ALIASES[normalized] ?? normalized;\n};\n\nconst validateLabels = (labels: readonly string[]): EntityLabel[] => {\n const resolved = [...new Set(labels.map(canonicalizeLabel))];\n const valid: EntityLabel[] = [];\n const availableLabels = ENTITY_LABELS.join(\", \");\n const availableAliases = Object.keys(LABEL_ALIASES).join(\", \");\n for (const label of resolved) {\n if (!isEntityLabel(label)) {\n throw new UsageError(\n [\n \"--labels: unknown label\",\n JSON.stringify(label) + \";\",\n \"available:\",\n availableLabels,\n \"(aliases:\",\n availableAliases + \")\",\n ].join(\" \"),\n );\n }\n valid.push(label);\n }\n return valid;\n};\n\ntype PipelineConfigOptions = Pick<\n CliOptions,\n \"countries\" | \"labels\" | \"languages\" | \"threshold\"\n>;\n\nconst buildPipelineConfig = async (\n opts: PipelineConfigOptions,\n loadDictionaries: CliEngine[\"loadDictionaries\"],\n): Promise<PipelineConfig> => {\n const dictionaries = await loadDictionaries({\n languages: opts.languages,\n countries: opts.countries,\n });\n return {\n threshold: opts.threshold,\n enableTriggerPhrases: true,\n enableRegex: true,\n enableLegalForms: true,\n enableNameCorpus: true,\n ...(opts.languages === undefined\n ? {}\n : { nameCorpusLanguages: [...opts.languages] }),\n enableDenyList: true,\n ...(opts.countries === undefined\n ? {}\n : { denyListCountries: [...opts.countries] }),\n enableGazetteer: false,\n enableCountries: true,\n enableConfidenceBoost: true,\n enableCoreference: true,\n enableZoneClassification: true,\n enableHotwordRules: true,\n labels:\n opts.labels === undefined\n ? [...DEFAULT_ENTITY_LABELS]\n : validateLabels(opts.labels),\n workspaceId: \"cli\",\n dictionaries,\n };\n};\n\nconst buildOperatorConfig = (opts: CliOptions): NativeOperatorConfig => {\n const operators: NonNullable<NativeOperatorConfig[\"operators\"]> = {};\n if (opts.mode === \"redact\") {\n const labels =\n opts.labels === undefined\n ? DEFAULT_ENTITY_LABELS\n : validateLabels(opts.labels);\n for (const label of labels) operators[label] = \"redact\";\n }\n return { operators, redactString: opts.redactString };\n};\n\nconst writeOutput = async (\n path: string | undefined,\n content: string,\n): Promise<void> => {\n if (path === undefined) {\n process.stdout.write(content);\n return;\n }\n await writeFile(path, content, \"utf8\");\n};\n\ntype RedactionKeyFile = {\n entries: Record<string, { original: string; operator: string }>;\n};\n\nconst parseRedactionKey = (raw: string): Map<string, string> => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new UsageError(\"redaction key is not valid JSON\");\n }\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new UsageError(\n 'redaction key must be an object with an \"entries\" field',\n );\n }\n const { entries } = parsed as RedactionKeyFile;\n if (\n typeof entries !== \"object\" ||\n entries === null ||\n Array.isArray(entries)\n ) {\n throw new UsageError('redaction key \"entries\" must be an object');\n }\n const map = new Map<string, string>();\n for (const [placeholder, entry] of Object.entries(entries)) {\n if (typeof entry?.original !== \"string\") {\n throw new UsageError(\n `redaction key entry \"${placeholder}\" has no original text`,\n );\n }\n map.set(placeholder, entry.original);\n }\n return map;\n};\n\n/**\n * Restrict a redaction key to the entities named by --revert.\n * Each token matches a placeholder (\"[PERSON_1]\") or an original\n * value (\"Jan Novák\"), case-sensitive and exact. A token that\n * matches nothing is a usage error listing the placeholders the\n * key does define, so the caller can correct the spelling.\n */\nconst selectRevertEntries = (\n redactionMap: ReadonlyMap<string, string>,\n tokens: readonly string[],\n): Map<string, string> => {\n const selected = new Map<string, string>();\n for (const token of tokens) {\n let matched = false;\n for (const [placeholder, original] of redactionMap) {\n if (placeholder === token || original === token) {\n selected.set(placeholder, original);\n matched = true;\n }\n }\n if (!matched) {\n const MAX_LISTED_PLACEHOLDERS = 20;\n const placeholders = [...redactionMap.keys()];\n const listed = placeholders.slice(0, MAX_LISTED_PLACEHOLDERS).join(\", \");\n const rest = placeholders.length - MAX_LISTED_PLACEHOLDERS;\n const suffix = rest > 0 ? ` and ${rest} more` : \"\";\n throw new UsageError(\n `--revert ${JSON.stringify(token)} matched no placeholder or ` +\n `original; available placeholders: ${listed}${suffix}`,\n );\n }\n }\n return selected;\n};\n\n/**\n * Ask for a country scope when running interactively on\n * files with no scope flags. Skipped for piped stdin so\n * the CLI stays scriptable.\n */\nexport const shouldPromptForScope = (\n opts: CliOptions,\n tty: { stdinIsTTY: boolean; stderrIsTTY: boolean },\n): boolean =>\n opts.countries === undefined &&\n opts.languages === undefined &&\n !opts.quiet &&\n opts.files.length > 0 &&\n tty.stdinIsTTY &&\n tty.stderrIsTTY;\n\nconst promptForCountries = async (): Promise<string[] | undefined> => {\n const rl = createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n try {\n const answer = await rl.question(\n \"Country scope (ISO codes like CZ,DE,GB; Enter loads all): \",\n );\n const trimmed = answer.trim();\n return trimmed === \"\" ? undefined : parseCountries(trimmed);\n } finally {\n rl.close();\n }\n};\n\nconst runDeanonymise = async (\n opts: CliOptions,\n api: AnonymizeApi,\n): Promise<void> => {\n if (opts.keyPath !== undefined) {\n throw new UsageError(\"--key cannot be combined with --deanonymise\");\n }\n const keyPath = opts.deanonymiseKeyPath;\n if (keyPath === undefined) throw new UsageError(\"missing redaction key path\");\n const fullMap = parseRedactionKey(await readFile(keyPath, \"utf8\"));\n\n // --revert restores a chosen subset; leaving the rest of the\n // key out means deanonymise skips those placeholders, so the\n // other entities stay redacted.\n const redactionMap =\n opts.revert === undefined\n ? fullMap\n : selectRevertEntries(fullMap, opts.revert);\n\n const inputs = await readInputs(opts.files);\n if (inputs.length > 1) {\n throw new UsageError(\"--deanonymise accepts a single input\");\n }\n const input = inputs[0];\n if (!input) throw new UsageError(\"no input to deanonymise\");\n if (opts.output !== undefined) {\n guardWriteTargets(input.path === null ? [] : [input.path], [\n { path: opts.output, flag: \"--output\" },\n ]);\n }\n await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));\n};\n\n/**\n * Reject any write target (output or key file) whose\n * filesystem identity collides with an input file or with\n * another write target. Symlinks count as collisions.\n */\nconst guardWriteTargets = (\n inputPaths: readonly string[],\n writeTargets: readonly { path: string; flag: string }[],\n): void => {\n const inputs = new Set(inputPaths.map(canonicalPath));\n const seen = new Map<string, string>();\n for (const target of writeTargets) {\n const canonical = canonicalPath(target.path);\n if (inputs.has(canonical)) {\n throw new UsageError(\n `refusing to overwrite input file \"${target.path}\" (${target.flag})`,\n );\n }\n const clash = seen.get(canonical);\n if (clash !== undefined) {\n throw new UsageError(\n `${target.flag} \"${target.path}\" collides with ${clash}`,\n );\n }\n seen.set(canonical, `${target.flag} \"${target.path}\"`);\n }\n};\n\nconst summarize = (entities: readonly CliEntity[]): string => {\n const counts = new Map<string, number>();\n for (const entity of entities) {\n counts.set(entity.label, (counts.get(entity.label) ?? 0) + 1);\n }\n const parts = [...counts.entries()]\n .toSorted((a, b) => b[1] - a[1])\n .map(([label, count]) => `${label}: ${count}`);\n return parts.length > 0 ? parts.join(\", \") : \"none\";\n};\n\n/**\n * A single unit of anonymize work: the text to process, where\n * to write it (undefined means stdout), and a label for the\n * stderr summary. Used by the stdin and single-file flows,\n * which additionally support --json and --key.\n */\ntype SingleInput = {\n text: string;\n outputPath: string | undefined;\n source: string;\n};\n\nconst runAnonymiseSingle = async (\n opts: CliOptions,\n runtime: CliRuntime,\n api: AnonymizeApi,\n input: SingleInput,\n): Promise<void> => {\n const { entities, redaction } = await runtime.redact(\n input.text,\n buildOperatorConfig(opts),\n );\n\n if (opts.json) {\n // In redact mode the user chose irreversibility, so the\n // JSON must not carry any detected text. Whitelist the\n // non-sensitive metadata fields; this drops `text` and a\n // coref alias's `corefSourceText`. Offsets index the\n // caller's own input and are kept.\n const jsonEntities =\n opts.mode === \"redact\"\n ? entities.map(({ start, end, label, score, source }) => ({\n start,\n end,\n label,\n score,\n source,\n }))\n : entities;\n const payload = {\n entityCount: redaction.entityCount,\n entities: jsonEntities,\n redactedText: redaction.redactedText,\n };\n await writeOutput(\n input.outputPath,\n `${JSON.stringify(payload, null, 2)}\\n`,\n );\n } else {\n await writeOutput(input.outputPath, redaction.redactedText);\n }\n\n if (opts.keyPath !== undefined) {\n await writeFile(\n opts.keyPath,\n api.exportRedactionKey(redaction.redactionMap, redaction.operatorMap),\n \"utf8\",\n );\n }\n\n if (!opts.quiet) {\n process.stderr.write(\n `anonymize: ${input.source}: ${summarize(entities)}\\n`,\n );\n }\n};\n\n/** Tally of a batch run for the closing summary line. */\ntype BatchOutcome = { processed: number; failed: number };\n\nconst runAnonymiseBatch = async (\n opts: CliOptions,\n runtime: CliRuntime,\n output: string,\n jobs: readonly FileJob[],\n skipped: number,\n): Promise<void> => {\n await mkdir(output, { recursive: true });\n const operatorConfig = buildOperatorConfig(opts);\n const outcome: BatchOutcome = { processed: 0, failed: 0 };\n\n await runPool(jobs, opts.workers, async (job) => {\n const outputPath = join(output, job.outputRelative);\n try {\n const text = await readFile(job.path, \"utf8\");\n const { entities, redaction } = await runtime.redact(\n text,\n operatorConfig,\n );\n await mkdir(dirname(outputPath), { recursive: true });\n await writeFile(outputPath, redaction.redactedText, \"utf8\");\n // No await between here and the increment: safe on the\n // single JS thread despite concurrent workers.\n outcome.processed += 1;\n if (!opts.quiet) {\n process.stderr.write(\n `anonymize: ${job.path}: ${summarize(entities)}\\n`,\n );\n }\n } catch (err) {\n outcome.failed += 1;\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${job.path}: error: ${message}\\n`);\n }\n });\n\n if (!opts.quiet) {\n const parts = [\n `${outcome.processed} processed`,\n `${outcome.failed} failed`,\n ];\n if (skipped > 0) parts.push(`${skipped} skipped`);\n process.stderr.write(`anonymize: ${parts.join(\", \")}\\n`);\n }\n // Any per-file failure is a nonzero exit, but the whole batch\n // still runs so one bad file does not hide the rest.\n if (outcome.failed > 0) process.exitCode = 1;\n};\n\nconst runAnonymise = async (\n opts: CliOptions,\n { api, loadDictionaries }: CliEngine,\n): Promise<void> => {\n if (opts.keyPath !== undefined && opts.mode !== \"replace\") {\n throw new UsageError('--key requires --mode \"replace\"');\n }\n\n const scoped = shouldPromptForScope(opts, {\n stdinIsTTY: process.stdin.isTTY === true,\n stderrIsTTY: process.stderr.isTTY === true,\n })\n ? { ...opts, countries: await promptForCountries() }\n : opts;\n\n // No positional arguments: read stdin as a single input.\n if (scoped.files.length === 0) {\n const [input] = await readInputs(scoped.files);\n if (!input) throw new UsageError(\"no input to anonymize\");\n guardWriteTargets([], collectSingleTargets(scoped));\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseSingle(scoped, runtime, api, {\n text: input.text,\n outputPath: scoped.output,\n source: \"stdin\",\n });\n return;\n }\n\n const { jobs, batch, skipped } = await expandInputs(\n scoped.files,\n scoped.recursive,\n scoped.output,\n );\n\n if (!batch) {\n // Exactly one plain file: single-input flow with --json/--key.\n const [job] = jobs;\n if (!job) throw new UsageError(\"no input to anonymize\");\n guardWriteTargets([job.path], collectSingleTargets(scoped));\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseSingle(scoped, runtime, api, {\n text: await readFile(job.path, \"utf8\"),\n outputPath: scoped.output,\n source: job.path,\n });\n return;\n }\n\n // Batch: a directory, or more than one file.\n const output = scoped.output;\n if (output === undefined) {\n throw new UsageError(\n \"batch input (a directory or multiple files) requires --output <directory>\",\n );\n }\n if (scoped.keyPath !== undefined) {\n throw new UsageError(\"--key works with a single input only\");\n }\n if (scoped.json) {\n throw new UsageError(\"--json works with a single input only\");\n }\n\n // Validate every write target before any work: colliding\n // output paths (same basename from different input dirs,\n // symlinks to an input) fail fast instead of silently\n // clobbering files mid-batch.\n guardWriteTargets(\n jobs.map((job) => job.path),\n jobs.map((job) => ({\n path: join(output, job.outputRelative),\n flag: \"--output\",\n })),\n );\n\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseBatch(scoped, runtime, output, jobs, skipped);\n};\n\n/** Write targets for a single-input run: --output and --key. */\nconst collectSingleTargets = (\n opts: CliOptions,\n): { path: string; flag: string }[] => {\n const targets: { path: string; flag: string }[] = [];\n if (opts.output !== undefined) {\n targets.push({ path: opts.output, flag: \"--output\" });\n }\n if (opts.keyPath !== undefined) {\n targets.push({ path: opts.keyPath, flag: \"--key\" });\n }\n return targets;\n};\n\ntype CliRuntime = {\n redact: (\n fullText: string,\n operators: NativeOperatorConfig,\n ) => Promise<{ entities: CliEntity[]; redaction: CliRedactionResult }>;\n};\n\nconst prepareNativeCliPipeline = async (\n api: AnonymizeApi,\n config: PipelineConfig,\n): Promise<NativeCliPipeline> => {\n const pipeline = await api.createNativePipelineFromConfig({\n binding: api.loadNativeAnonymizeBinding(),\n config,\n gazetteerEntries: [],\n });\n pipeline.warmLazyRegex?.();\n return pipeline;\n};\n\nconst prepareCliRuntime = async (\n api: AnonymizeApi,\n config: PipelineConfig,\n): Promise<CliRuntime> => {\n const pipeline = await prepareNativeCliPipeline(api, config);\n return {\n redact: async (fullText, operators) => {\n const result = pipeline.redactText(fullText, operators);\n return {\n entities: result.resolvedEntities,\n redaction: result.redaction,\n };\n },\n };\n};\n\n/**\n * Render the canonical entity labels and the short aliases\n * accepted by --labels, for the --list-labels discovery flag.\n */\nconst formatLabelList = (): string => {\n const lines: string[] = [\"Detectable entity labels (pass to --labels):\"];\n for (const label of ENTITY_LABELS) {\n lines.push(` ${label}`);\n }\n lines.push(\"\", \"Short aliases:\");\n const aliases = Object.entries(LABEL_ALIASES);\n let width = 0;\n for (const [alias] of aliases) {\n width = Math.max(width, alias.length);\n }\n for (const [alias, canonical] of aliases) {\n lines.push(` ${alias.padEnd(width)} -> ${canonical}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n};\n\nconst dispatch = async (engine: CliEngine): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv.at(0) === \"docx\") {\n await runDocxCommand({\n argv: argv.slice(1),\n preparePipeline: async (\n request: DocxPipelineRequest,\n ): Promise<DocxCliPipeline> => {\n const options: PipelineConfigOptions =\n request.type === \"anonymize\"\n ? request.detection\n : {\n countries: [],\n languages: [],\n threshold: DEFAULT_THRESHOLD,\n };\n return prepareNativeCliPipeline(\n engine.api,\n await buildPipelineConfig(options, engine.loadDictionaries),\n );\n },\n });\n return;\n }\n if (argv.at(0) === \"pdf\") {\n await runPdfCommand({\n argv: argv.slice(1),\n preparePipeline: async (\n request: PdfPipelineRequest,\n ): Promise<PdfCliPipeline> =>\n prepareNativeCliPipeline(\n engine.api,\n await buildPipelineConfig(request.detection, engine.loadDictionaries),\n ),\n });\n return;\n }\n const opts = parseCliArgs(argv);\n if (opts.help) {\n process.stdout.write(HELP);\n return;\n }\n if (opts.version) {\n process.stdout.write(`${cliVersion()}\\n`);\n return;\n }\n if (opts.listLabels) {\n process.stdout.write(formatLabelList());\n return;\n }\n if (opts.capabilities) {\n process.stdout.write(`${JSON.stringify(CAPABILITY_MANIFEST, null, 2)}\\n`);\n return;\n }\n if (opts.deanonymiseKeyPath !== undefined) {\n await runDeanonymise(opts, engine.api);\n return;\n }\n if (opts.revert !== undefined) {\n throw new UsageError(\"--revert requires --deanonymise <key>\");\n }\n await runAnonymise(opts, engine);\n};\n\n/**\n * Run the CLI against the given engine and set the\n * process exit code (0 ok, 1 runtime error, 2 usage).\n */\nexport const runCli = async (engine: CliEngine): Promise<void> => {\n try {\n await dispatch(engine);\n } catch (err) {\n if (err instanceof UsageError) {\n process.stderr.write(`anonymize: ${err.message}\\n`);\n process.stderr.write(`Try \"anonymize --help\" for usage.\\n`);\n process.exitCode = 2;\n } else {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${message}\\n`);\n process.exitCode = 1;\n }\n }\n};\n","#!/usr/bin/env node\n/* npm-distributed entry point — backs the CLI with the\n * native engine (@stll/anonymize napi bindings) and\n * the @stll/anonymize-data dictionary package. */\nimport * as anonymize from \"@stll/anonymize\";\n\nimport { loadCliDictionaries } from \"./dictionaries\";\nimport { runCli } from \"./main\";\n\nawait runCli({ api: anonymize, loadDictionaries: loadCliDictionaries });\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/anonymize-cli",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Command-line PII detection and anonymization powered by @stll/anonymize",
5
5
  "keywords": [
6
6
  "anonymization",
@@ -36,9 +36,10 @@
36
36
  "format": "oxfmt ."
37
37
  },
38
38
  "dependencies": {
39
- "@stll/anonymize": "^2.2.0",
39
+ "@stll/anonymize": "^2.4.0",
40
40
  "@stll/anonymize-data": "^0.0.6",
41
- "@stll/anonymize-docx": "^2.2.0"
41
+ "@stll/anonymize-docx": "^2.4.0",
42
+ "@stll/anonymize-pdf": "^2.4.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^26.1.1",