@stll/anonymize-cli 2.7.8 → 2.8.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/LICENSE CHANGED
@@ -1,27 +1,202 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 stella labs, s.r.o.
4
-
5
- Permission is hereby granted, free of charge, to any
6
- person obtaining a copy of this software and associated
7
- documentation files (the "Software"), to deal in the
8
- Software without restriction, including without
9
- limitation the rights to use, copy, modify, merge,
10
- publish, distribute, sublicense, and/or sell copies of
11
- the Software, and to permit persons to whom the
12
- Software is furnished to do so, subject to the
13
- following conditions:
14
-
15
- The above copyright notice and this permission notice
16
- shall be included in all copies or substantial portions
17
- of the Software.
18
-
19
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
20
- ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
21
- TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
22
- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
23
- SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
26
- IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
- DEALINGS IN THE SOFTWARE.
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,20 @@
1
+ anonymize
2
+ Copyright 2026 stella labs, s.r.o.
3
+
4
+ This product includes software developed at stella labs, s.r.o.
5
+ (https://stll.app/).
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
8
+ not use this product except in compliance with the License. You may
9
+ obtain a copy of the License in the LICENSE file distributed with this product
10
+ or at https://www.apache.org/licenses/LICENSE-2.0.
11
+
12
+ --------------------------------------------------------------------------
13
+
14
+ Third-party notices
15
+
16
+ This product bundles and builds on third-party components distributed
17
+ under their own licenses (including MIT). Those components and their
18
+ attributions are listed in the ATTRIBUTION.md files under packages/ and
19
+ crates/, and the browser build's runtime attributions are listed in
20
+ packages/anonymize/wasm/README.md.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Command-line PII detection and anonymization powered by
4
4
  [`@stll/anonymize`](https://github.com/stella/anonymize).
5
- Fully offline: no network calls, ever.
5
+ Processing is local and makes no network calls. `bunx` and `npx` may contact a
6
+ package registry to download the CLI; use an existing local or global install
7
+ when the invocation itself must remain offline.
6
8
 
7
9
  ## Usage
8
10
 
@@ -44,6 +46,9 @@ anonymize -d key.json reply.txt
44
46
  | `--capabilities` | Emit the versioned capability manifest as JSON |
45
47
  | `--quiet` | Suppress the stderr summary |
46
48
 
49
+ `--key` export is supported on Linux. It fails closed on other platforms
50
+ because the CLI cannot verify owner-only filesystem ACLs.
51
+
47
52
  Run `anonymize --help` for the full reference, including the
48
53
  `--json` schema and exit codes.
49
54
 
package/dist/cli.mjs CHANGED
@@ -11,7 +11,6 @@ import { createInterface } from "node:readline/promises";
11
11
  import { CAPABILITY_MANIFEST } from "@stll/anonymize/capabilities";
12
12
  import { DEFAULT_ENTITY_LABELS, ENTITY_LABELS } from "@stll/anonymize/constants";
13
13
  import { AnonymizeSurfaceError, exitCodeForError, isAnonymizeSurfaceError } from "@stll/anonymize/agent-surface";
14
- import { preloadNativeBinding } from "@stll/anonymize/native-runtime";
15
14
  import { FEEDBACK_KINDS, MAX_FEEDBACK_BODY_CHARS, MAX_FEEDBACK_TITLE_CHARS, buildFeedbackSubmission } from "@stll/anonymize/feedback";
16
15
  import { randomUUID } from "node:crypto";
17
16
  import { DOCX_COVERAGE_MODES, anonymizeDocx, restoreDocxText } from "@stll/anonymize-docx";
@@ -335,7 +334,7 @@ const loadCliDictionaries = async ({ languages, countries }) => {
335
334
  };
336
335
  //#endregion
337
336
  //#region package.json
338
- var version = "2.7.8";
337
+ var version = "2.8.1";
339
338
  //#endregion
340
339
  //#region src/feedback.ts
341
340
  const FEEDBACK_HELP = `Usage: anonymize feedback [options] [body ...]
@@ -608,12 +607,12 @@ const assertDistinctPaths = (paths) => {
608
607
  seen.set(canonical, `${entry.flag} "${entry.path}"`);
609
608
  }
610
609
  };
611
- const isNodeError$1 = (error, code) => error instanceof Error && "code" in error && error.code === code;
610
+ const isNodeError$2 = (error, code) => error instanceof Error && "code" in error && error.code === code;
612
611
  const assertPathDoesNotExist = async (path, flag) => {
613
612
  try {
614
613
  await lstat(path);
615
614
  } catch (error) {
616
- if (isNodeError$1(error, "ENOENT")) return;
615
+ if (isNodeError$2(error, "ENOENT")) return;
617
616
  throw error;
618
617
  }
619
618
  throw new UsageError(`${flag} refuses to overwrite existing path "${path}"`);
@@ -662,14 +661,14 @@ const acquireSessionArchiveLock = async (archivePath) => {
662
661
  try {
663
662
  handle = await open(lockPath, "wx", 384);
664
663
  } catch (error) {
665
- if (isNodeError$1(error, "EEXIST")) throw new Error(`encrypted session archive is locked by another continuation; if no process is running, remove the stale lock "${lockPath}"`);
664
+ if (isNodeError$2(error, "EEXIST")) throw new Error(`encrypted session archive is locked by another continuation; if no process is running, remove the stale lock "${lockPath}"`);
666
665
  throw error;
667
666
  }
668
667
  return { release: async () => {
669
668
  const closeResult = await captureOperationResult(handle.close());
670
669
  const unlinkResult = await captureOperationResult(unlink(lockPath));
671
670
  if (closeResult.type === "failed") throw closeResult.error;
672
- if (unlinkResult.type === "failed" && !isNodeError$1(unlinkResult.error, "ENOENT")) throw unlinkResult.error;
671
+ if (unlinkResult.type === "failed" && !isNodeError$2(unlinkResult.error, "ENOENT")) throw unlinkResult.error;
673
672
  } };
674
673
  };
675
674
  const readSessionKey = async (path) => {
@@ -717,7 +716,7 @@ const publishNewFile$1 = async (temporary, target, flag) => {
717
716
  try {
718
717
  await link(temporary, target);
719
718
  } catch (error) {
720
- if (isNodeError$1(error, "EEXIST")) throw new UsageError(`${flag} refuses to overwrite existing path "${target}"`);
719
+ if (isNodeError$2(error, "EEXIST")) throw new UsageError(`${flag} refuses to overwrite existing path "${target}"`);
721
720
  throw error;
722
721
  }
723
722
  await removeStagedFile(temporary);
@@ -973,21 +972,21 @@ const canonicalPath$1 = (path) => {
973
972
  return resolve(path);
974
973
  }
975
974
  };
976
- const isNodeError = (error, code) => error instanceof Error && "code" in error && error.code === code;
975
+ const isNodeError$1 = (error, code) => error instanceof Error && "code" in error && error.code === code;
977
976
  const preflight = async (command) => {
978
977
  if (canonicalPath$1(command.inputPath) === canonicalPath$1(command.outputPath)) throw new UsageError("--output must not overwrite the PDF input");
979
978
  let input;
980
979
  try {
981
980
  input = await lstat(command.inputPath);
982
981
  } catch (error) {
983
- if (isNodeError(error, "ENOENT")) throw new UsageError("PDF input must be a regular non-symlink file");
982
+ if (isNodeError$1(error, "ENOENT")) throw new UsageError("PDF input must be a regular non-symlink file");
984
983
  throw error;
985
984
  }
986
985
  if (!input.isFile() || input.isSymbolicLink()) throw new UsageError("PDF input must be a regular non-symlink file");
987
986
  try {
988
987
  await lstat(command.outputPath);
989
988
  } catch (error) {
990
- if (isNodeError(error, "ENOENT")) return;
989
+ if (isNodeError$1(error, "ENOENT")) return;
991
990
  throw error;
992
991
  }
993
992
  throw new UsageError("--output refuses to overwrite an existing path");
@@ -997,7 +996,7 @@ const readRegularInput = async (path) => {
997
996
  try {
998
997
  handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
999
998
  } catch (error) {
1000
- if (isNodeError(error, "ELOOP")) throw new UsageError("PDF input must be a regular non-symlink file");
999
+ if (isNodeError$1(error, "ELOOP")) throw new UsageError("PDF input must be a regular non-symlink file");
1001
1000
  throw error;
1002
1001
  }
1003
1002
  try {
@@ -1023,7 +1022,7 @@ const removeStaged = async (path) => {
1023
1022
  try {
1024
1023
  await unlink(path);
1025
1024
  } catch (error) {
1026
- if (!isNodeError(error, "ENOENT")) throw error;
1025
+ if (!isNodeError$1(error, "ENOENT")) throw error;
1027
1026
  }
1028
1027
  };
1029
1028
  const publishNewFile = async (target, content) => {
@@ -1036,7 +1035,7 @@ const publishNewFile = async (target, content) => {
1036
1035
  try {
1037
1036
  await link(temporary, target);
1038
1037
  } catch (error) {
1039
- if (isNodeError(error, "EEXIST")) throw new UsageError("--output refuses to overwrite an existing path");
1038
+ if (isNodeError$1(error, "EEXIST")) throw new UsageError("--output refuses to overwrite an existing path");
1040
1039
  throw error;
1041
1040
  }
1042
1041
  } catch (error) {
@@ -1077,6 +1076,70 @@ const runPdfCommand = async ({ argv, preparePipeline }) => {
1077
1076
  if (!command.quiet) process.stderr.write(`anonymize: PDF anonymized: ${result.certificate.pageCount} pages, ${result.certificate.detectionCount} detections, PII-clean guarantee=false\n`);
1078
1077
  };
1079
1078
  //#endregion
1079
+ //#region src/private-file.ts
1080
+ const isNodeError = (error, code) => error instanceof Error && "code" in error && error.code === code;
1081
+ const FILE_PERMISSION_MASK = 511n;
1082
+ const assertFilePermissions = ({ actual, expected, flag }) => {
1083
+ if ((actual & FILE_PERMISSION_MASK) !== expected) throw new UsageError(`${flag} cannot verify owner-only file permissions on this filesystem`);
1084
+ };
1085
+ /**
1086
+ * Open an output before truncating it, then reject aliases to a protected
1087
+ * descriptor. Path replacement after open cannot redirect descriptor writes.
1088
+ */
1089
+ const writeFileWithoutIdentityCollision = async ({ target, content, targetFlag, forbiddenFlag, forbiddenIdentity }) => {
1090
+ const handle = await open(target, constants.O_WRONLY | constants.O_CREAT);
1091
+ try {
1092
+ const stats = await handle.stat({ bigint: true });
1093
+ if (stats.dev === forbiddenIdentity.device && stats.ino === forbiddenIdentity.inode) throw new UsageError(`${targetFlag} "${target}" aliases ${forbiddenFlag}`);
1094
+ await handle.truncate(0);
1095
+ await handle.writeFile(content);
1096
+ } finally {
1097
+ await handle.close().catch(() => void 0);
1098
+ }
1099
+ };
1100
+ const publishNewPrivateFile = async ({ target, content, flag, afterPublish }) => {
1101
+ if (process.platform !== "linux") throw new UsageError(`${flag} is unavailable on ${process.platform} because owner-only file ACLs cannot be verified`);
1102
+ let handle;
1103
+ try {
1104
+ handle = await open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0);
1105
+ } catch (error) {
1106
+ if (isNodeError(error, "EEXIST")) throw new UsageError(`${flag} refuses to overwrite existing path "${target}"`);
1107
+ throw error;
1108
+ }
1109
+ let committed = false;
1110
+ try {
1111
+ const stats = await handle.stat({ bigint: true });
1112
+ if (!stats.isFile()) throw new Error("private file destination is not a regular file");
1113
+ assertFilePermissions({
1114
+ actual: stats.mode,
1115
+ expected: 0n,
1116
+ flag
1117
+ });
1118
+ await handle.writeFile(content);
1119
+ await handle.sync();
1120
+ await handle.chmod(384);
1121
+ const privateStats = await handle.stat({ bigint: true });
1122
+ assertFilePermissions({
1123
+ actual: privateStats.mode,
1124
+ expected: 384n,
1125
+ flag
1126
+ });
1127
+ await handle.sync();
1128
+ await afterPublish?.({
1129
+ device: stats.dev,
1130
+ inode: stats.ino
1131
+ });
1132
+ committed = true;
1133
+ } finally {
1134
+ if (!committed) {
1135
+ await handle.truncate(0).catch(() => void 0);
1136
+ await handle.sync().catch(() => void 0);
1137
+ await handle.chmod(0).catch(() => void 0);
1138
+ }
1139
+ await handle.close().catch(() => void 0);
1140
+ }
1141
+ };
1142
+ //#endregion
1080
1143
  //#region src/main.ts
1081
1144
  const cliVersion = () => version;
1082
1145
  /**
@@ -1292,9 +1355,27 @@ const buildOperatorConfig = (opts) => {
1292
1355
  redactString: opts.redactString
1293
1356
  };
1294
1357
  };
1295
- const writeOutput = async (path, content) => {
1358
+ const writeOutput = async ({ path, content, forbiddenIdentity }) => {
1296
1359
  if (path === void 0) {
1297
- process.stdout.write(content);
1360
+ await new Promise((complete, reject) => {
1361
+ process.stdout.write(content, (error) => {
1362
+ if (error) {
1363
+ reject(error);
1364
+ return;
1365
+ }
1366
+ complete();
1367
+ });
1368
+ });
1369
+ return;
1370
+ }
1371
+ if (forbiddenIdentity !== void 0) {
1372
+ await writeFileWithoutIdentityCollision({
1373
+ target: path,
1374
+ content,
1375
+ targetFlag: "--output",
1376
+ forbiddenFlag: "--key",
1377
+ forbiddenIdentity
1378
+ });
1298
1379
  return;
1299
1380
  }
1300
1381
  await writeFile(path, content, "utf8");
@@ -1374,7 +1455,10 @@ const runDeanonymise = async (opts, api) => {
1374
1455
  path: opts.output,
1375
1456
  flag: "--output"
1376
1457
  }]);
1377
- await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));
1458
+ await writeOutput({
1459
+ path: opts.output,
1460
+ content: api.deanonymise(input.text, redactionMap)
1461
+ });
1378
1462
  };
1379
1463
  /**
1380
1464
  * Reject any write target (output or key file) whose
@@ -1400,6 +1484,7 @@ const summarize = (entities) => {
1400
1484
  };
1401
1485
  const runAnonymiseSingle = async (opts, runtime, api, input) => {
1402
1486
  const { entities, redaction } = await runtime.redact(input.text, buildOperatorConfig(opts));
1487
+ let outputContent;
1403
1488
  if (opts.json) {
1404
1489
  const jsonEntities = opts.mode === "redact" ? entities.map(({ start, end, label, score, source }) => ({
1405
1490
  start,
@@ -1413,9 +1498,22 @@ const runAnonymiseSingle = async (opts, runtime, api, input) => {
1413
1498
  entities: jsonEntities,
1414
1499
  redactedText: redaction.redactedText
1415
1500
  };
1416
- await writeOutput(input.outputPath, `${JSON.stringify(payload, null, 2)}\n`);
1417
- } else await writeOutput(input.outputPath, redaction.redactedText);
1418
- if (opts.keyPath !== void 0) await writeFile(opts.keyPath, api.exportRedactionKey(redaction.redactionMap, redaction.operatorMap), "utf8");
1501
+ outputContent = `${JSON.stringify(payload, null, 2)}\n`;
1502
+ } else outputContent = redaction.redactedText;
1503
+ if (opts.keyPath !== void 0) await publishNewPrivateFile({
1504
+ target: opts.keyPath,
1505
+ content: api.exportRedactionKey(redaction.redactionMap, redaction.operatorMap),
1506
+ flag: "--key",
1507
+ afterPublish: (identity) => writeOutput({
1508
+ path: input.outputPath,
1509
+ content: outputContent,
1510
+ forbiddenIdentity: identity
1511
+ })
1512
+ });
1513
+ else await writeOutput({
1514
+ path: input.outputPath,
1515
+ content: outputContent
1516
+ });
1419
1517
  if (!opts.quiet) process.stderr.write(`anonymize: ${input.source}: ${summarize(entities)}\n`);
1420
1518
  };
1421
1519
  const runAnonymiseBatch = async (opts, runtime, output, jobs, skipped) => {
@@ -1601,7 +1699,6 @@ const dispatch = async (engine) => {
1601
1699
  */
1602
1700
  const runCli = async (engine) => {
1603
1701
  try {
1604
- await preloadNativeBinding();
1605
1702
  await dispatch(engine);
1606
1703
  } catch (err) {
1607
1704
  const surface = isAnonymizeSurfaceError(err) ? err : mapFsError(err);
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 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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"}
1
+ {"version":3,"file":"cli.mjs","names":["splitList","parseThreshold","required","canonicalPath","isNodeError","publishNewFile","canonicalPath","isNodeError","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/private-file.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 { constants } from \"node:fs\";\nimport { open } from \"node:fs/promises\";\n\nimport { UsageError } from \"./args\";\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 FILE_PERMISSION_MASK = 0o777n;\n\ntype AssertFilePermissionsOptions = {\n actual: bigint;\n expected: bigint;\n flag: string;\n};\n\nconst assertFilePermissions = ({\n actual,\n expected,\n flag,\n}: AssertFilePermissionsOptions): void => {\n if ((actual & FILE_PERMISSION_MASK) !== expected) {\n throw new UsageError(\n `${flag} cannot verify owner-only file permissions on this filesystem`,\n );\n }\n};\n\n/**\n * Create sensitive content at a new path through one descriptor. Failed\n * dependent work leaves an empty mode-000 reservation: pathname cleanup could\n * otherwise unlink a replacement installed by another process.\n */\ntype PublishNewPrivateFileOptions = {\n target: string;\n content: string | Uint8Array;\n flag: string;\n afterPublish?: (identity: FileIdentity) => Promise<void>;\n};\n\nexport type FileIdentity = {\n readonly device: bigint;\n readonly inode: bigint;\n};\n\ntype WriteFileWithoutIdentityCollisionOptions = {\n target: string;\n content: string | Uint8Array;\n targetFlag: string;\n forbiddenFlag: string;\n forbiddenIdentity: FileIdentity;\n};\n\n/**\n * Open an output before truncating it, then reject aliases to a protected\n * descriptor. Path replacement after open cannot redirect descriptor writes.\n */\nexport const writeFileWithoutIdentityCollision = async ({\n target,\n content,\n targetFlag,\n forbiddenFlag,\n forbiddenIdentity,\n}: WriteFileWithoutIdentityCollisionOptions): Promise<void> => {\n const handle = await open(target, constants.O_WRONLY | constants.O_CREAT);\n try {\n const stats = await handle.stat({ bigint: true });\n if (\n stats.dev === forbiddenIdentity.device &&\n stats.ino === forbiddenIdentity.inode\n ) {\n throw new UsageError(\n `${targetFlag} \"${target}\" aliases ${forbiddenFlag}`,\n );\n }\n await handle.truncate(0);\n await handle.writeFile(content);\n } finally {\n await handle.close().catch(() => undefined);\n }\n};\n\nexport const publishNewPrivateFile = async ({\n target,\n content,\n flag,\n afterPublish,\n}: PublishNewPrivateFileOptions): Promise<void> => {\n if (process.platform !== \"linux\") {\n throw new UsageError(\n `${flag} is unavailable on ${process.platform} because owner-only file ACLs cannot be verified`,\n );\n }\n\n let handle;\n try {\n handle = await open(\n target,\n constants.O_WRONLY |\n constants.O_CREAT |\n constants.O_EXCL |\n constants.O_NOFOLLOW,\n 0o000,\n );\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\n let committed = false;\n try {\n const stats = await handle.stat({ bigint: true });\n if (!stats.isFile()) {\n throw new Error(\"private file destination is not a regular file\");\n }\n assertFilePermissions({ actual: stats.mode, expected: 0o000n, flag });\n\n await handle.writeFile(content);\n await handle.sync();\n await handle.chmod(0o600);\n const privateStats = await handle.stat({ bigint: true });\n assertFilePermissions({\n actual: privateStats.mode,\n expected: 0o600n,\n flag,\n });\n await handle.sync();\n await afterPublish?.({ device: stats.dev, inode: stats.ino });\n committed = true;\n } finally {\n if (!committed) {\n await handle.truncate(0).catch(() => undefined);\n await handle.sync().catch(() => undefined);\n await handle.chmod(0o000).catch(() => undefined);\n }\n await handle.close().catch(() => undefined);\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\";\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\";\nimport type { FileIdentity } from \"./private-file\";\nimport {\n publishNewPrivateFile,\n writeFileWithoutIdentityCollision,\n} from \"./private-file\";\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\ntype WriteOutputOptions = {\n path: string | undefined;\n content: string;\n forbiddenIdentity?: FileIdentity;\n};\n\nconst writeOutput = async ({\n path,\n content,\n forbiddenIdentity,\n}: WriteOutputOptions): Promise<void> => {\n if (path === undefined) {\n await new Promise<void>((complete, reject) => {\n process.stdout.write(content, (error) => {\n if (error) {\n reject(error);\n return;\n }\n complete();\n });\n });\n return;\n }\n if (forbiddenIdentity !== undefined) {\n await writeFileWithoutIdentityCollision({\n target: path,\n content,\n targetFlag: \"--output\",\n forbiddenFlag: \"--key\",\n forbiddenIdentity,\n });\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({\n path: opts.output,\n content: api.deanonymise(input.text, redactionMap),\n });\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 let outputContent: string;\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 outputContent = `${JSON.stringify(payload, null, 2)}\\n`;\n } else {\n outputContent = redaction.redactedText;\n }\n\n if (opts.keyPath !== undefined) {\n await publishNewPrivateFile({\n target: opts.keyPath,\n content: api.exportRedactionKey(\n redaction.redactionMap,\n redaction.operatorMap,\n ),\n flag: \"--key\",\n afterPublish: (identity) =>\n writeOutput({\n path: input.outputPath,\n content: outputContent,\n forbiddenIdentity: identity,\n }),\n });\n } else {\n await writeOutput({ path: input.outputPath, content: outputContent });\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 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.7.8",
3
+ "version": "2.8.1",
4
4
  "description": "Command-line PII detection and anonymization powered by @stll/anonymize",
5
5
  "keywords": [
6
6
  "anonymization",
@@ -17,6 +17,8 @@
17
17
  "anonymize": "./dist/cli.mjs"
18
18
  },
19
19
  "files": [
20
+ "LICENSE",
21
+ "NOTICE",
20
22
  "dist"
21
23
  ],
22
24
  "publishConfig": {
@@ -36,14 +38,14 @@
36
38
  "format": "oxfmt ."
37
39
  },
38
40
  "dependencies": {
39
- "@stll/anonymize": "^2.7.8",
40
- "@stll/anonymize-data": "^0.0.9",
41
- "@stll/anonymize-docx": "^2.7.8",
42
- "@stll/anonymize-pdf": "^2.7.8"
41
+ "@stll/anonymize": "^2.8.1",
42
+ "@stll/anonymize-data": "^0.0.10",
43
+ "@stll/anonymize-docx": "^2.8.1",
44
+ "@stll/anonymize-pdf": "^2.8.1"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@types/node": "^26.2.0",
46
- "bun-types": "^1.3.14",
48
+ "bun-types": "1.4.0",
47
49
  "fflate": "^0.8.3",
48
50
  "tsdown": "0.22.14",
49
51
  "typescript": "7.0.2"