@stll/anonymize-cli 2.6.3 → 2.7.1

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/dist/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as anonymize from "@stll/anonymize";
3
- import { ALL_DICTIONARY_IDS, DICTIONARY_META, loadCityDictionary, loadDictionary, loadNameDictionaries } from "@stll/anonymize-data";
3
+ import { ALL_DICTIONARY_IDS, DICTIONARY_META, loadDictionary, loadNameDictionaries } from "@stll/anonymize-data";
4
+ import { loadCityDictionary } from "@stll/anonymize-data/cities";
4
5
  import { availableParallelism } from "node:os";
5
6
  import { parseArgs } from "node:util";
6
7
  import { constants, realpathSync } from "node:fs";
@@ -334,7 +335,7 @@ const loadCliDictionaries = async ({ languages, countries }) => {
334
335
  };
335
336
  //#endregion
336
337
  //#region package.json
337
- var version = "2.6.3";
338
+ var version = "2.7.1";
338
339
  //#endregion
339
340
  //#region src/feedback.ts
340
341
  const FEEDBACK_HELP = `Usage: anonymize feedback [options] [body ...]
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
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/feedback.ts","../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\nFeedback:\n Run \"anonymize feedback --help\" to report a bug or gap. It sanitizes your\n text locally and prints a prefilled GitHub issue URL you submit yourself;\n it makes no network calls.\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 4 not found (missing path)\n 1 runtime error 5 unsupported format\n 2 usage error 6 output already exists\n 3 path not allowed 7 session unavailable\n 8 dependency missing\n Messages and, for coded failures, a \"hint:\" line print 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 { parseArgs } from \"node:util\";\n\nimport {\n FEEDBACK_KINDS,\n MAX_FEEDBACK_BODY_CHARS,\n MAX_FEEDBACK_TITLE_CHARS,\n buildFeedbackSubmission,\n type FeedbackKind,\n} from \"@stll/anonymize/feedback\";\n\nimport { UsageError } from \"./args\";\n\nexport const FEEDBACK_HELP = `Usage: anonymize feedback [options] [body ...]\n\nReport a bug, feature request, or docs issue to the stella-anonymize\nmaintainers. Your title and body are sanitized locally (emails, ids,\nsecrets, URLs, and IPs are redacted); the command prints a prefilled\nGitHub issue URL and a gh command that you open and submit under your\nown account. It makes no network calls and submits nothing on its own.\n\nNever include document text, client names, ids, or secrets; describe\nthe problem, the steps to reproduce, and expected vs actual result.\n\nOptions:\n --kind <kind> bug | feature_request | docs | other (default: bug)\n --title <text> Short one-line summary (required)\n --body <text> Details; if omitted, the positional args or stdin are used\n --json Emit the sanitized submission as JSON\n -h, --help Print this help\n`;\n\nconst isFeedbackKind = (value: string): value is FeedbackKind =>\n (FEEDBACK_KINDS as readonly string[]).includes(value);\n\ntype FeedbackCommandOptions = {\n argv: readonly string[];\n readStdin: () => Promise<string>;\n write: (text: string) => void;\n};\n\nconst renderHuman = (\n submission: ReturnType<typeof buildFeedbackSubmission>,\n): string =>\n [\n `Sanitized ${submission.redactions} item(s) from your feedback. Nothing was sent.`,\n \"\",\n \"Title:\",\n submission.title,\n \"\",\n \"Body:\",\n submission.sanitizedBody,\n \"\",\n \"Open this URL to submit the issue under your own GitHub account:\",\n submission.issueUrl,\n \"\",\n \"Or run:\",\n submission.ghCommand,\n \"\",\n ].join(\"\\n\");\n\n/** Parse the feedback flags, rethrowing parse errors as usage errors (exit 2). */\nconst parseFeedbackArgs = (argv: readonly string[]) => {\n try {\n return parseArgs({\n args: [...argv],\n allowPositionals: true,\n options: {\n kind: { type: \"string\" },\n title: { type: \"string\" },\n body: { type: \"string\" },\n json: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n};\n\nexport const runFeedbackCommand = async ({\n argv,\n readStdin,\n write,\n}: FeedbackCommandOptions): Promise<void> => {\n const { values, positionals } = parseFeedbackArgs(argv);\n if (values.help === true) {\n write(FEEDBACK_HELP);\n return;\n }\n const kind = values.kind ?? \"bug\";\n if (!isFeedbackKind(kind)) {\n throw new UsageError(\n `unknown feedback kind \"${kind}\" (expected ${FEEDBACK_KINDS.join(\", \")})`,\n );\n }\n const title = values.title?.trim() ?? \"\";\n if (title.length === 0) {\n throw new UsageError(\"feedback requires --title <text>\");\n }\n if (title.length > MAX_FEEDBACK_TITLE_CHARS) {\n throw new UsageError(\n `--title must be at most ${MAX_FEEDBACK_TITLE_CHARS} characters`,\n );\n }\n const inlineBody = (\n values.body ?? (positionals.length > 0 ? positionals.join(\" \") : undefined)\n )?.trim();\n const body =\n inlineBody !== undefined && inlineBody.length > 0\n ? inlineBody\n : (await readStdin()).trim();\n if (body.length === 0) {\n throw new UsageError(\n \"feedback requires a body (--body <text>, positional text, or stdin)\",\n );\n }\n if (body.length > MAX_FEEDBACK_BODY_CHARS) {\n throw new UsageError(\n `feedback body must be at most ${MAX_FEEDBACK_BODY_CHARS} characters`,\n );\n }\n const submission = buildFeedbackSubmission({ kind, title, body });\n if (values.json === true) {\n write(`${JSON.stringify({ channel: \"github\", ...submission }, null, 2)}\\n`);\n return;\n }\n write(renderHuman(submission));\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 {\n AnonymizeSurfaceError,\n exitCodeForError,\n isAnonymizeSurfaceError,\n} from \"@stll/anonymize/agent-surface\";\nimport { preloadNativeBinding } from \"@stll/anonymize/native-runtime\";\n\nimport type { CliOptions } from \"./args\";\nimport {\n DEFAULT_THRESHOLD,\n HELP,\n parseCliArgs,\n parseCountries,\n UsageError,\n} from \"./args\";\nimport { runFeedbackCommand } from \"./feedback\";\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) === \"feedback\") {\n await runFeedbackCommand({\n argv: argv.slice(1),\n readStdin,\n write: (text) => {\n process.stdout.write(text);\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 // Install the runtime binding (wasm under Bun) before any native call.\n // A no-op on Node, where the CLI ships to run.\n await preloadNativeBinding();\n await dispatch(engine);\n } catch (err) {\n const surface = isAnonymizeSurfaceError(err) ? err : mapFsError(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 if (surface !== undefined) {\n process.stderr.write(`anonymize: ${surface.message}\\n`);\n process.stderr.write(`hint: ${surface.hint}\\n`);\n process.exitCode = exitCodeForError(surface);\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\n/**\n * Map a raw filesystem error to the agent-surface taxonomy so a missing or\n * inaccessible input/output path exits with a stable code instead of the\n * generic runtime-error class. Returns undefined for anything else.\n */\nconst mapFsError = (error: unknown): AnonymizeSurfaceError | undefined => {\n if (typeof error !== \"object\" || error === null || !(\"code\" in error)) {\n return undefined;\n }\n const code = (error as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n return new AnonymizeSurfaceError(\"not_found\", \"input path does not exist\", {\n hint: \"Check the path; pass an existing file or directory.\",\n cause: error,\n });\n }\n if (code === \"EACCES\" || code === \"EPERM\") {\n return new AnonymizeSurfaceError(\n \"path_not_allowed\",\n \"permission denied for the given path\",\n {\n hint: \"Adjust file permissions or choose an accessible path.\",\n cause: error,\n },\n );\n }\n return undefined;\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/feedback.ts","../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\nFeedback:\n Run \"anonymize feedback --help\" to report a bug or gap. It sanitizes your\n text locally and prints a prefilled GitHub issue URL you submit yourself;\n it makes no network calls.\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 4 not found (missing path)\n 1 runtime error 5 unsupported format\n 2 usage error 6 output already exists\n 3 path not allowed 7 session unavailable\n 8 dependency missing\n Messages and, for coded failures, a \"hint:\" line print 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 loadDictionary,\n loadNameDictionaries,\n type NameLanguage,\n} from \"@stll/anonymize-data\";\nimport { loadCityDictionary } from \"@stll/anonymize-data/cities\";\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 { parseArgs } from \"node:util\";\n\nimport {\n FEEDBACK_KINDS,\n MAX_FEEDBACK_BODY_CHARS,\n MAX_FEEDBACK_TITLE_CHARS,\n buildFeedbackSubmission,\n type FeedbackKind,\n} from \"@stll/anonymize/feedback\";\n\nimport { UsageError } from \"./args\";\n\nexport const FEEDBACK_HELP = `Usage: anonymize feedback [options] [body ...]\n\nReport a bug, feature request, or docs issue to the stella-anonymize\nmaintainers. Your title and body are sanitized locally (emails, ids,\nsecrets, URLs, and IPs are redacted); the command prints a prefilled\nGitHub issue URL and a gh command that you open and submit under your\nown account. It makes no network calls and submits nothing on its own.\n\nNever include document text, client names, ids, or secrets; describe\nthe problem, the steps to reproduce, and expected vs actual result.\n\nOptions:\n --kind <kind> bug | feature_request | docs | other (default: bug)\n --title <text> Short one-line summary (required)\n --body <text> Details; if omitted, the positional args or stdin are used\n --json Emit the sanitized submission as JSON\n -h, --help Print this help\n`;\n\nconst isFeedbackKind = (value: string): value is FeedbackKind =>\n (FEEDBACK_KINDS as readonly string[]).includes(value);\n\ntype FeedbackCommandOptions = {\n argv: readonly string[];\n readStdin: () => Promise<string>;\n write: (text: string) => void;\n};\n\nconst renderHuman = (\n submission: ReturnType<typeof buildFeedbackSubmission>,\n): string =>\n [\n `Sanitized ${submission.redactions} item(s) from your feedback. Nothing was sent.`,\n \"\",\n \"Title:\",\n submission.title,\n \"\",\n \"Body:\",\n submission.sanitizedBody,\n \"\",\n \"Open this URL to submit the issue under your own GitHub account:\",\n submission.issueUrl,\n \"\",\n \"Or run:\",\n submission.ghCommand,\n \"\",\n ].join(\"\\n\");\n\n/** Parse the feedback flags, rethrowing parse errors as usage errors (exit 2). */\nconst parseFeedbackArgs = (argv: readonly string[]) => {\n try {\n return parseArgs({\n args: [...argv],\n allowPositionals: true,\n options: {\n kind: { type: \"string\" },\n title: { type: \"string\" },\n body: { type: \"string\" },\n json: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n } catch (error) {\n throw new UsageError(\n error instanceof Error ? error.message : String(error),\n );\n }\n};\n\nexport const runFeedbackCommand = async ({\n argv,\n readStdin,\n write,\n}: FeedbackCommandOptions): Promise<void> => {\n const { values, positionals } = parseFeedbackArgs(argv);\n if (values.help === true) {\n write(FEEDBACK_HELP);\n return;\n }\n const kind = values.kind ?? \"bug\";\n if (!isFeedbackKind(kind)) {\n throw new UsageError(\n `unknown feedback kind \"${kind}\" (expected ${FEEDBACK_KINDS.join(\", \")})`,\n );\n }\n const title = values.title?.trim() ?? \"\";\n if (title.length === 0) {\n throw new UsageError(\"feedback requires --title <text>\");\n }\n if (title.length > MAX_FEEDBACK_TITLE_CHARS) {\n throw new UsageError(\n `--title must be at most ${MAX_FEEDBACK_TITLE_CHARS} characters`,\n );\n }\n const inlineBody = (\n values.body ?? (positionals.length > 0 ? positionals.join(\" \") : undefined)\n )?.trim();\n const body =\n inlineBody !== undefined && inlineBody.length > 0\n ? inlineBody\n : (await readStdin()).trim();\n if (body.length === 0) {\n throw new UsageError(\n \"feedback requires a body (--body <text>, positional text, or stdin)\",\n );\n }\n if (body.length > MAX_FEEDBACK_BODY_CHARS) {\n throw new UsageError(\n `feedback body must be at most ${MAX_FEEDBACK_BODY_CHARS} characters`,\n );\n }\n const submission = buildFeedbackSubmission({ kind, title, body });\n if (values.json === true) {\n write(`${JSON.stringify({ channel: \"github\", ...submission }, null, 2)}\\n`);\n return;\n }\n write(renderHuman(submission));\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 {\n AnonymizeSurfaceError,\n exitCodeForError,\n isAnonymizeSurfaceError,\n} from \"@stll/anonymize/agent-surface\";\nimport { preloadNativeBinding } from \"@stll/anonymize/native-runtime\";\n\nimport type { CliOptions } from \"./args\";\nimport {\n DEFAULT_THRESHOLD,\n HELP,\n parseCliArgs,\n parseCountries,\n UsageError,\n} from \"./args\";\nimport { runFeedbackCommand } from \"./feedback\";\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) === \"feedback\") {\n await runFeedbackCommand({\n argv: argv.slice(1),\n readStdin,\n write: (text) => {\n process.stdout.write(text);\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 // Install the runtime binding (wasm under Bun) before any native call.\n // A no-op on Node, where the CLI ships to run.\n await preloadNativeBinding();\n await dispatch(engine);\n } catch (err) {\n const surface = isAnonymizeSurfaceError(err) ? err : mapFsError(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 if (surface !== undefined) {\n process.stderr.write(`anonymize: ${surface.message}\\n`);\n process.stderr.write(`hint: ${surface.hint}\\n`);\n process.exitCode = exitCodeForError(surface);\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\n/**\n * Map a raw filesystem error to the agent-surface taxonomy so a missing or\n * inaccessible input/output path exits with a stable code instead of the\n * generic runtime-error class. Returns undefined for anything else.\n */\nconst mapFsError = (error: unknown): AnonymizeSurfaceError | undefined => {\n if (typeof error !== \"object\" || error === null || !(\"code\" in error)) {\n return undefined;\n }\n const code = (error as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n return new AnonymizeSurfaceError(\"not_found\", \"input path does not exist\", {\n hint: \"Check the path; pass an existing file or directory.\",\n cause: error,\n });\n }\n if (code === \"EACCES\" || code === \"EPERM\") {\n return new AnonymizeSurfaceError(\n \"path_not_allowed\",\n \"permission denied for the given path\",\n {\n hint: \"Adjust file permissions or choose an accessible path.\",\n cause: error,\n },\n );\n }\n return undefined;\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.6.3",
3
+ "version": "2.7.1",
4
4
  "description": "Command-line PII detection and anonymization powered by @stll/anonymize",
5
5
  "keywords": [
6
6
  "anonymization",
@@ -36,10 +36,10 @@
36
36
  "format": "oxfmt ."
37
37
  },
38
38
  "dependencies": {
39
- "@stll/anonymize": "^2.6.3",
40
- "@stll/anonymize-data": "^0.0.7",
41
- "@stll/anonymize-docx": "^2.6.3",
42
- "@stll/anonymize-pdf": "^2.6.3"
39
+ "@stll/anonymize": "^2.7.1",
40
+ "@stll/anonymize-data": "^0.0.9",
41
+ "@stll/anonymize-docx": "^2.7.1",
42
+ "@stll/anonymize-pdf": "^2.7.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^26.1.1",