@piecemaker-legal/piecemaker 2.0.10 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/assets/{FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js → FindReplaceDialog-KU5TEIBJ-BBxf1he7.js} +1 -1
  2. package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js → FootnotePropertiesDialog-W3YHM4BV-BYqp6U4x.js} +1 -1
  3. package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js → HyperlinkDialog-ZYH7ZHRF-BVC9zOMs.js} +4 -4
  4. package/dist/assets/{ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js → ImagePositionDialog-HEWYR6Q3-Xn2mC64y.js} +1 -1
  5. package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js → ImagePropertiesDialog-RAB47C6M-CFcdqEif.js} +1 -1
  6. package/dist/assets/{PageSetupDialog-AIGIXO3E-_PYIO8eb.js → PageSetupDialog-AIGIXO3E-DECaeRS9.js} +1 -1
  7. package/dist/assets/{SplitCellDialog-IFBKYCD6-Cb9uQOB8.js → SplitCellDialog-IFBKYCD6-ByYa04H_.js} +1 -1
  8. package/dist/assets/{TablePropertiesDialog-TMLKGOOW-12vmjb6B.js → TablePropertiesDialog-TMLKGOOW-BuyqZB5A.js} +1 -1
  9. package/dist/assets/{WatermarkDialog-KRL7WMIS-C-d8rzt2.js → WatermarkDialog-KRL7WMIS-ByZ1Acr2.js} +1 -1
  10. package/dist/assets/{abnfDiagram-VCTEODGH-B4OsX7eF.js → abnfDiagram-VCTEODGH-BtttzdZA.js} +1 -1
  11. package/dist/assets/{arc-BK1oUIk7.js → arc-DN4m1tLs.js} +1 -1
  12. package/dist/assets/{architectureDiagram-5GKGNRK7-CfaFI6L7.js → architectureDiagram-5GKGNRK7-Bt-zFumG.js} +1 -1
  13. package/dist/assets/{blockDiagram-I7D4REHJ-DQEI2azY.js → blockDiagram-I7D4REHJ-D14V8v4j.js} +1 -1
  14. package/dist/assets/{c4Diagram-7LVT6UL2-DbravpVi.js → c4Diagram-7LVT6UL2-BynDo1-W.js} +1 -1
  15. package/dist/assets/channel-C4gJXzYa.js +1 -0
  16. package/dist/assets/{chunk-2Q5K7J3B-BLpZ9huC.js → chunk-2Q5K7J3B-B8gKhSj9.js} +1 -1
  17. package/dist/assets/{chunk-5VM5RSS4-BLlEdmX9.js → chunk-5VM5RSS4-GvOpglRR.js} +1 -1
  18. package/dist/assets/{chunk-F27PBJKO-D-ZNhI6q.js → chunk-F27PBJKO-CAgZy0qc.js} +1 -1
  19. package/dist/assets/{chunk-IMKFNOWR-vzSiKnhh.js → chunk-IMKFNOWR-gf8Ux8Zd.js} +1 -1
  20. package/dist/assets/{chunk-JWPE2WC7-DfF0k4nb.js → chunk-JWPE2WC7-D4zFFRMp.js} +1 -1
  21. package/dist/assets/{chunk-POPQ4Y6H-BnwCYsPP.js → chunk-POPQ4Y6H-Ct8X_LaT.js} +1 -1
  22. package/dist/assets/{chunk-SVP7TREG-DRxc8fhq.js → chunk-SVP7TREG-g5iH1bs5.js} +1 -1
  23. package/dist/assets/{chunk-TICWLB2K-Dd2Q-md-.js → chunk-TICWLB2K-89-VFKTn.js} +1 -1
  24. package/dist/assets/{chunk-XXDRQBXY-DBjkEUp_.js → chunk-XXDRQBXY-CaQbdObb.js} +1 -1
  25. package/dist/assets/classDiagram-ZZMXUADV-Cm08FeYk.js +1 -0
  26. package/dist/assets/classDiagram-v2-VYDZK3BY-Cm08FeYk.js +1 -0
  27. package/dist/assets/{cose-bilkent-JH36ORCC-By6PS4oe.js → cose-bilkent-JH36ORCC-BiYOGJAU.js} +1 -1
  28. package/dist/assets/{cynefin-OW5HDTMX-YxWpjc5W.js → cynefin-OW5HDTMX-F6mJ-4tj.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-5FMLGOSQ-DrtXzz3p.js → cynefinDiagram-5FMLGOSQ-q-Rggn6r.js} +1 -1
  30. package/dist/assets/{dagre-GXQ25YYZ-BDpTLKqZ.js → dagre-GXQ25YYZ-COqImSUH.js} +1 -1
  31. package/dist/assets/{diagram-S7CK7UJ4-C4LvqeCs.js → diagram-S7CK7UJ4-BAg1dyEc.js} +1 -1
  32. package/dist/assets/{diagram-UQ7AKVKN-BTpnMeHp.js → diagram-UQ7AKVKN-DFJS8qCV.js} +1 -1
  33. package/dist/assets/{diagram-VSXAHHWV-DucHHWev.js → diagram-VSXAHHWV-BfldL35r.js} +1 -1
  34. package/dist/assets/{diagram-VX7I27RA-DYHZNmmH.js → diagram-VX7I27RA-kLjovpYe.js} +1 -1
  35. package/dist/assets/{diagram-Z3DM3KII-BHIH2BGN.js → diagram-Z3DM3KII-Dhwvi3Iu.js} +1 -1
  36. package/dist/assets/{ebnfDiagram-PWID7BFC-CGiEkwj0.js → ebnfDiagram-PWID7BFC-Bp4YE1i7.js} +1 -1
  37. package/dist/assets/{erDiagram-RLTQ6QDP-AJMHi2Us.js → erDiagram-RLTQ6QDP-BKDdaSWz.js} +1 -1
  38. package/dist/assets/{flowDiagram-HODETNUW-BqzANBh2.js → flowDiagram-HODETNUW-zHProVxL.js} +1 -1
  39. package/dist/assets/{ganttDiagram-EL5Y4UJY-B0xPDY2_.js → ganttDiagram-EL5Y4UJY-B-B-BI6j.js} +1 -1
  40. package/dist/assets/{gitGraphDiagram-WWUBYQGX-CzPX5CNO.js → gitGraphDiagram-WWUBYQGX-BTHMg5CH.js} +1 -1
  41. package/dist/assets/index-C5slQNvE.css +1 -0
  42. package/dist/assets/{index-Bk2iXKnJ.js → index-CJP4dZ-c.js} +146 -146
  43. package/dist/assets/{infoDiagram-27XIBGKW-Cjc44H0O.js → infoDiagram-27XIBGKW-mabp4H9I.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-5VMMS53U-BWhr2v7r.js → ishikawaDiagram-5VMMS53U-CLNhcCrM.js} +1 -1
  45. package/dist/assets/{journeyDiagram-3NMN7TZE-Bl4r6RrP.js → journeyDiagram-3NMN7TZE-CGEi2RB6.js} +1 -1
  46. package/dist/assets/{kanban-definition-UXKFOSKX-FV-T0Vy1.js → kanban-definition-UXKFOSKX-B2fLX1fK.js} +1 -1
  47. package/dist/assets/{layout-CTfb1sAF.js → layout-CloKWsFq.js} +1 -1
  48. package/dist/assets/{linear-Dfgi6vlo.js → linear-BFyydI0c.js} +1 -1
  49. package/dist/assets/{mermaid.core-DOCGsdlo.js → mermaid.core-Dxl3LVAh.js} +6 -6
  50. package/dist/assets/{mindmap-definition-YA3MSWOX-CymQ86ir.js → mindmap-definition-YA3MSWOX-FT4pm55s.js} +1 -1
  51. package/dist/assets/{pegDiagram-XKGWAZYB-1VMq_vi0.js → pegDiagram-XKGWAZYB-okyIfJWM.js} +1 -1
  52. package/dist/assets/{pieDiagram-E7YTZNPT-C5GgXtJU.js → pieDiagram-E7YTZNPT-NF61CQz8.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-AXDQQJYC-C6zpryYf.js → quadrantDiagram-AXDQQJYC-uVfvZzzt.js} +1 -1
  54. package/dist/assets/{railroadDiagram-O6MQD6OU-D-sv4SWi.js → railroadDiagram-O6MQD6OU-Dp8IpeoH.js} +1 -1
  55. package/dist/assets/{requirementDiagram-BXWQKSXE-CkjY-z4B.js → requirementDiagram-BXWQKSXE-D8VV0oQe.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-P5KCCOFB-De-d8Q6L.js → sankeyDiagram-P5KCCOFB-D-xhxjob.js} +1 -1
  57. package/dist/assets/{sequenceDiagram-WJ2MYXX4-DCsGSJa9.js → sequenceDiagram-WJ2MYXX4-BAQBK95h.js} +1 -1
  58. package/dist/assets/{sizeCapture-INFHLROL-xINZhgER.js → sizeCapture-INFHLROL-C6rk9SVx.js} +1 -1
  59. package/dist/assets/{stateDiagram-D77RDMKH-CuLUTWSL.js → stateDiagram-D77RDMKH-DshqaEOj.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-MP3YSRHH-Cnbs-M92.js → stateDiagram-v2-MP3YSRHH-BohZIrk5.js} +1 -1
  61. package/dist/assets/{swimlanes-42K2YHIH-BwxARD7N.js → swimlanes-42K2YHIH-DgcPDy4l.js} +1 -1
  62. package/dist/assets/swimlanesDiagram-VR7AAH4N-CuM3Su0K.js +8 -0
  63. package/dist/assets/{timeline-definition-24CTP7MA-BIOvnn-l.js → timeline-definition-24CTP7MA-OgpIe-gQ.js} +1 -1
  64. package/dist/assets/{vennDiagram-4TSXK5OY-BSD6Pfmt.js → vennDiagram-4TSXK5OY-BZzGd6jt.js} +1 -1
  65. package/dist/assets/{wardleyDiagram-VM6X3IG4-7Zz2iRkF.js → wardleyDiagram-VM6X3IG4-1tg-H7l_.js} +1 -1
  66. package/dist/assets/{xychartDiagram-S5SC5T6Z-Ctx17Y1D.js → xychartDiagram-S5SC5T6Z-ByTsX8g0.js} +1 -1
  67. package/dist/index.html +2 -2
  68. package/dist/sw.js +1 -1
  69. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +61 -1
  70. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
  71. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +0 -48
  72. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
  73. package/dist-server/server/piecemaker/agent-instructions.js +148 -0
  74. package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
  75. package/dist-server/server/piecemaker/index.js +4 -0
  76. package/dist-server/server/piecemaker/index.js.map +1 -1
  77. package/dist-server/server/piecemaker/knowledge/pipeline.js +25 -29
  78. package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
  79. package/dist-server/server/piecemaker/knowledge/service.js +1 -16
  80. package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
  81. package/dist-server/server/piecemaker/project-registry.js +5 -5
  82. package/dist-server/server/piecemaker/project-registry.js.map +1 -1
  83. package/package.json +1 -1
  84. package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
  85. package/server/piecemaker/agent-instructions.ts +148 -0
  86. package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
  87. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +1 -1
  88. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +2 -1
  89. package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
  90. package/server/piecemaker/anonymizer/rewrite.cjs +2 -2
  91. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +7 -4
  92. package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
  93. package/server/piecemaker/anonymizer/service.cjs +10 -8
  94. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +15 -32
  95. package/server/piecemaker/index.ts +5 -1
  96. package/server/piecemaker/knowledge/pipeline.ts +24 -31
  97. package/server/piecemaker/knowledge/service.ts +1 -14
  98. package/server/piecemaker/project-registry.ts +6 -6
  99. package/server/piecemaker/router.cjs +0 -1
  100. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +9 -162
  101. package/server/piecemaker/vendor/installer/lib/commandes.mjs +1 -2
  102. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +62 -3
  103. package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
  104. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +23 -69
  105. package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +3 -3
  106. package/server/piecemaker/vendor/mcp/README.md +1 -1
  107. package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +12 -75
  108. package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +0 -40
  109. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +2 -2
  110. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
  111. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +26 -7
  112. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +0 -28
  113. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +0 -13
  114. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +3 -3
  115. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +10 -143
  116. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +1 -1
  117. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +0 -65
  118. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
  119. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +11 -2
  120. package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
  121. package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +9 -5
  122. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +3 -179
  123. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +0 -10
  124. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +3 -0
  125. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +23 -372
  126. package/server/piecemaker/vendor/websocket-server/renamed-originals.cjs +136 -0
  127. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +115 -96
  128. package/dist/assets/channel-DVyNr2Yg.js +0 -1
  129. package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +0 -1
  130. package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +0 -1
  131. package/dist/assets/index-Dc9wNuS4.css +0 -1
  132. package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +0 -8
  133. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +0 -194
  134. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +0 -71
  135. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +0 -260
  136. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit.mjs +0 -83
@@ -30,6 +30,19 @@ const EMPTY = Object.freeze({
30
30
  empty: true,
31
31
  });
32
32
 
33
+ const MEMO_MIN_LENGTH = 256;
34
+ const MEMO_BUDGET = 16 * 1024 * 1024;
35
+ const memos = new WeakMap();
36
+
37
+ function memoFor(dictionary) {
38
+ let memo = memos.get(dictionary);
39
+ if (!memo) {
40
+ memo = { entries: new Map(), size: 0 };
41
+ memos.set(dictionary, memo);
42
+ }
43
+ return memo;
44
+ }
45
+
33
46
  function plainObject(value) {
34
47
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
35
48
  }
@@ -49,8 +62,9 @@ function plainObject(value) {
49
62
  * ne part pas codé serait un mensonge à l'écran.
50
63
  */
51
64
  function buildDictionary(document, stamp) {
52
- const mapping = plainObject(document?.mapping);
53
- const reverse = plainObject(document?.reverse_mapping);
65
+ const mapping = Object.freeze({ ...plainObject(document?.mapping) });
66
+ const reverse = Object.freeze(Object.fromEntries(Object.entries(plainObject(document?.reverse_mapping))
67
+ .map(([code, variants]) => [code, Array.isArray(variants) ? Object.freeze([...variants]) : variants])));
54
68
  const canonical = {};
55
69
  for (const [code, variants] of Object.entries(reverse)) {
56
70
  const name = Array.isArray(variants) ? variants[0] : variants;
@@ -89,7 +103,24 @@ function buildDictionary(document, stamp) {
89
103
 
90
104
  /** Nom réel → code, sur tout ce qui part vers l'API. */
91
105
  function anonymize(text, dictionary) {
92
- return dictionary.empty ? text : applyMapping(text, dictionary.mapping);
106
+ if (dictionary.empty) return text;
107
+ if (typeof text !== 'string' || text.length < MEMO_MIN_LENGTH) return applyMapping(text, dictionary.mapping);
108
+ const memo = memoFor(dictionary);
109
+ const cached = memo.entries.get(text);
110
+ if (cached !== undefined) {
111
+ memo.entries.delete(text);
112
+ memo.entries.set(text, cached);
113
+ return cached;
114
+ }
115
+ const result = applyMapping(text, dictionary.mapping);
116
+ memo.entries.set(text, result);
117
+ memo.size += text.length + (result === text ? 0 : result.length);
118
+ for (const [key, value] of memo.entries) {
119
+ if (memo.size <= MEMO_BUDGET) break;
120
+ memo.entries.delete(key);
121
+ memo.size -= key.length + (value === key ? 0 : value.length);
122
+ }
123
+ return result;
93
124
  }
94
125
 
95
126
  /** Code → nom réel, sur tout ce qui revient de l'API vers la machine. */
@@ -49,7 +49,7 @@ async function startHudsuckerSession({
49
49
  databasePath,
50
50
  upstreamPort,
51
51
  harness = null,
52
- hosts = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'],
52
+ hosts = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com', 'api.mistral.ai'],
53
53
  directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-hudsucker-')),
54
54
  } = {}) {
55
55
  const binary = hudsuckerBinary();
@@ -70,6 +70,7 @@ fn provider_for(host: &str) -> &'static str {
70
70
  match host {
71
71
  "chatgpt.com" => "codex",
72
72
  "api.openai.com" => "opencode",
73
+ "api.mistral.ai" => "mistral",
73
74
  _ => "claude",
74
75
  }
75
76
  }
@@ -471,7 +472,7 @@ async fn main() {
471
472
  let hosts = parse_list(&arg("--hosts"));
472
473
  let upstream = parse_map(&arg("--upstream-map"));
473
474
  if listen.is_empty() || rewriter.is_empty() || ca_cert.is_empty() || ca_key.is_empty() || hosts.is_empty() {
474
- eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:0 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com");
475
+ eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:0 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com,api.mistral.ai");
475
476
  std::process::exit(2);
476
477
  }
477
478
  exit_when_parent_dies();
@@ -0,0 +1,343 @@
1
+ const { execFileSync, spawn } = require('node:child_process');
2
+ const fs = require('node:fs');
3
+ const http = require('node:http');
4
+ const https = require('node:https');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const tls = require('node:tls');
8
+
9
+ const Database = require('better-sqlite3');
10
+
11
+ const { startRewriterWorker } = require('./rewriter-worker.cjs');
12
+
13
+ const HOST = 'api.anthropic.com';
14
+ const NAME = 'Jean Dupont';
15
+ const CODE = 'PERSONNE_PHYSIQUE_01';
16
+ const CHECK_TIMEOUT_MS = 10000;
17
+ const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
18
+
19
+ function defaultBinary() {
20
+ const name = process.platform === 'win32' ? 'piecemaker-hudsucker.exe' : 'piecemaker-hudsucker';
21
+ return [
22
+ path.join(__dirname, 'bin', name),
23
+ path.join(__dirname, 'hudsucker-proxy', 'target', 'release', name),
24
+ ].find((candidate) => fs.existsSync(candidate)) || path.join(__dirname, 'bin', name);
25
+ }
26
+
27
+ function mappingDatabase(directory) {
28
+ const file = path.join(directory, 'auth.db');
29
+ const database = new Database(file);
30
+ database.exec(`
31
+ CREATE TABLE piecemaker_nodes (project_id TEXT, id TEXT, label TEXT, PRIMARY KEY (project_id, id));
32
+ CREATE TABLE piecemaker_mappings (project_id TEXT, node_id TEXT, real_value TEXT, masked_value TEXT, updated_at TEXT);
33
+ `);
34
+ database.prepare('INSERT INTO piecemaker_nodes VALUES (?, ?, ?)').run('smoke', 'n0', NAME);
35
+ database.prepare('INSERT INTO piecemaker_mappings VALUES (?, ?, ?, ?, ?)').run('smoke', 'n0', NAME, CODE, '2026-01-01T00:00:00.000Z');
36
+ database.close();
37
+ return file;
38
+ }
39
+
40
+ function temporaryAuthority(directory) {
41
+ const cert = path.join(directory, 'ca.crt');
42
+ const key = path.join(directory, 'ca.key');
43
+ execFileSync('openssl', [
44
+ 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
45
+ '-keyout', key, '-out', cert, '-days', '1',
46
+ '-subj', '/CN=PieceMaker Smoke CA',
47
+ '-addext', 'basicConstraints=critical,CA:true,pathlen:0',
48
+ '-addext', 'keyUsage=critical,keyCertSign,cRLSign',
49
+ ], { stdio: 'ignore' });
50
+ return { cert, key };
51
+ }
52
+
53
+ function sseEvent(text) {
54
+ return `event: content_block_delta\ndata: ${JSON.stringify({
55
+ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text },
56
+ })}\n\n`;
57
+ }
58
+
59
+ function startUpstream() {
60
+ const seen = [];
61
+ const server = http.createServer((request, response) => {
62
+ const chunks = [];
63
+ request.on('data', (chunk) => chunks.push(chunk));
64
+ request.on('end', () => {
65
+ const body = Buffer.concat(chunks).toString('utf8');
66
+ seen.push(body);
67
+ if (request.headers['x-smoke-mode'] === 'sse') {
68
+ response.writeHead(200, { 'content-type': 'text/event-stream' });
69
+ for (const text of ['Le client ', 'PERSONNE_', 'PHYSIQUE', '_01', ' a signé.']) response.write(sseEvent(text));
70
+ response.end('event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n');
71
+ return;
72
+ }
73
+ response.writeHead(200, { 'content-type': 'application/json' });
74
+ response.end(JSON.stringify({ echo: body }));
75
+ });
76
+ });
77
+ return new Promise((resolve) => {
78
+ server.listen(0, '127.0.0.1', () => resolve({ seen, server, port: server.address().port }));
79
+ });
80
+ }
81
+
82
+ function sandboxProfile(directory) {
83
+ const writable = JSON.stringify(fs.realpathSync(directory));
84
+ return [
85
+ '(version 1)',
86
+ '(allow default)',
87
+ '(deny network-outbound)',
88
+ '(allow network-outbound (remote ip "localhost:*"))',
89
+ '(allow network-outbound (remote unix-socket))',
90
+ '(deny file-write*)',
91
+ `(allow file-write* (subpath ${writable}) (literal "/dev/null") (literal "/dev/tty"))`,
92
+ ].join('\n');
93
+ }
94
+
95
+ function launch(binary, args, directory, sandboxed) {
96
+ const env = {
97
+ ...process.env,
98
+ HOME: directory,
99
+ USERPROFILE: directory,
100
+ HTTPS_PROXY: '', HTTP_PROXY: '', ALL_PROXY: '', https_proxy: '', http_proxy: '', all_proxy: '',
101
+ };
102
+ const options = { cwd: directory, env, stdio: ['pipe', 'pipe', 'pipe'] };
103
+ if (sandboxed) return spawn(SANDBOX_EXEC, ['-p', sandboxProfile(directory), binary, ...args], options);
104
+ return spawn(binary, args, options);
105
+ }
106
+
107
+ function waitForListening(child, logs) {
108
+ return new Promise((resolve, reject) => {
109
+ let buffer = '';
110
+ const timer = setTimeout(() => reject(new Error(`aucune écoute après 15 s : ${logs.text || buffer}`)), 15000);
111
+ child.stdout.on('data', (chunk) => {
112
+ buffer += chunk;
113
+ const match = buffer.match(/listening 127\.0\.0\.1:(\d+)/);
114
+ if (!match) return;
115
+ clearTimeout(timer);
116
+ if (Number(match[1]) === 0) reject(new Error('port annoncé 0 : binaire antérieur au source, à recompiler'));
117
+ else resolve(Number(match[1]));
118
+ });
119
+ child.once('error', (error) => {
120
+ clearTimeout(timer);
121
+ reject(error);
122
+ });
123
+ child.once('exit', (code, signal) => {
124
+ clearTimeout(timer);
125
+ reject(new Error(`sorti (${code ?? signal}) : ${logs.text || buffer}`));
126
+ });
127
+ });
128
+ }
129
+
130
+ function withTimeout(promise, label) {
131
+ let timer;
132
+ return Promise.race([
133
+ promise,
134
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} : délai dépassé`)), CHECK_TIMEOUT_MS); }),
135
+ ]).finally(() => clearTimeout(timer));
136
+ }
137
+
138
+ function health(port) {
139
+ return new Promise((resolve, reject) => {
140
+ http.get({ host: '127.0.0.1', port, path: '/health' }, (response) => {
141
+ let body = '';
142
+ response.on('data', (chunk) => { body += chunk; });
143
+ response.on('end', () => resolve({ status: response.statusCode, body }));
144
+ }).on('error', reject);
145
+ });
146
+ }
147
+
148
+ function refuseDirectResolution(hostname, _options, callback) {
149
+ callback(new Error(`résolution directe de ${hostname} interdite : tout doit passer par le proxy`));
150
+ }
151
+
152
+ function postThroughProxy(port, ca, { body, contentType = 'application/json', mode = 'json' }) {
153
+ return new Promise((resolve, reject) => {
154
+ const connect = http.request({ host: '127.0.0.1', port, method: 'CONNECT', path: `${HOST}:443`, headers: { host: `${HOST}:443` } });
155
+ connect.once('error', reject);
156
+ connect.once('connect', (answer, socket) => {
157
+ if (answer.statusCode !== 200) {
158
+ socket.destroy();
159
+ reject(new Error(`CONNECT refusé (${answer.statusCode})`));
160
+ return;
161
+ }
162
+ const request = https.request({
163
+ host: HOST,
164
+ path: '/v1/messages',
165
+ method: 'POST',
166
+ lookup: refuseDirectResolution,
167
+ headers: { 'content-type': contentType, 'content-length': Buffer.byteLength(body), 'x-smoke-mode': mode },
168
+ createConnection: () => tls.connect({ socket, servername: HOST, ca }),
169
+ }, (response) => {
170
+ let text = '';
171
+ response.setEncoding('utf8');
172
+ response.on('data', (chunk) => { text += chunk; });
173
+ response.on('end', () => {
174
+ socket.destroy();
175
+ resolve({ status: response.statusCode, body: text });
176
+ });
177
+ });
178
+ request.once('error', reject);
179
+ request.end(body);
180
+ });
181
+ connect.end();
182
+ });
183
+ }
184
+
185
+ function streamedText(body) {
186
+ return body
187
+ .split('\n')
188
+ .filter((line) => line.startsWith('data: '))
189
+ .map((line) => {
190
+ try {
191
+ return JSON.parse(line.slice(6))?.delta?.text || '';
192
+ } catch {
193
+ return '';
194
+ }
195
+ })
196
+ .join('');
197
+ }
198
+
199
+ function waitForExit(child, timeoutMs) {
200
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
201
+ return new Promise((resolve) => {
202
+ const timer = setTimeout(() => resolve(false), timeoutMs);
203
+ child.once('exit', () => {
204
+ clearTimeout(timer);
205
+ resolve(true);
206
+ });
207
+ });
208
+ }
209
+
210
+ async function runHudsuckerSmokeTest({ binary: requestedBinary = defaultBinary(), caCert = null, caKey = null, sandbox = true, report = () => {} } = {}) {
211
+ const binary = path.resolve(requestedBinary);
212
+ const checks = [];
213
+ const record = (name, ok, detail = '') => {
214
+ checks.push({ name, ok, detail });
215
+ report({ name, ok, detail });
216
+ return ok;
217
+ };
218
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-hudsucker-smoke-'));
219
+ const sandboxed = sandbox && process.platform === 'darwin' && fs.existsSync(SANDBOX_EXEC);
220
+ let child = null;
221
+ let bridge = null;
222
+ let upstream = null;
223
+ try {
224
+ if (!fs.existsSync(binary)) {
225
+ record('binaire présent', false, binary);
226
+ return { ok: false, sandboxed, checks };
227
+ }
228
+ const authority = caCert && caKey ? { cert: caCert, key: caKey } : temporaryAuthority(directory);
229
+ for (const file of [authority.cert, authority.key]) {
230
+ if (!fs.existsSync(file)) {
231
+ record('autorité locale présente', false, file);
232
+ return { ok: false, sandboxed, checks };
233
+ }
234
+ }
235
+ const ca = fs.readFileSync(authority.cert);
236
+
237
+ bridge = await startRewriterWorker({ databasePath: mappingDatabase(directory), homeDir: directory });
238
+ upstream = await startUpstream();
239
+
240
+ const logs = { text: '' };
241
+ child = launch(binary, [
242
+ '--listen', '127.0.0.1:0',
243
+ '--rewriter', `http://127.0.0.1:${bridge.port}`,
244
+ '--ca-cert', authority.cert,
245
+ '--ca-key', authority.key,
246
+ '--hosts', HOST,
247
+ '--upstream-map', `${HOST}=127.0.0.1:${upstream.port}`,
248
+ ], directory, sandboxed);
249
+ child.stderr.on('data', (chunk) => { logs.text += chunk; });
250
+
251
+ let port;
252
+ try {
253
+ port = await waitForListening(child, logs);
254
+ record('démarrage', true, `écoute sur 127.0.0.1:${port}${sandboxed ? ', dans sandbox-exec' : ''}`);
255
+ } catch (error) {
256
+ record('démarrage', false, error.message);
257
+ return { ok: false, sandboxed, checks };
258
+ }
259
+
260
+ const step = async (name, run) => {
261
+ try {
262
+ const detail = await withTimeout(run(), name);
263
+ record(name, true, detail || '');
264
+ } catch (error) {
265
+ record(name, false, error.message);
266
+ }
267
+ };
268
+
269
+ await step('santé', async () => {
270
+ const { status, body } = await health(port);
271
+ if (status !== 200 || !body.includes('"rewriter":"ok"')) throw new Error(`HTTP ${status} ${body}`);
272
+ });
273
+
274
+ await step('anonymisation JSON aller-retour', async () => {
275
+ const before = upstream.seen.length;
276
+ const response = await postThroughProxy(port, ca, {
277
+ body: JSON.stringify({ messages: [{ role: 'user', content: `Contrat de ${NAME}` }] }),
278
+ });
279
+ const sent = upstream.seen[before] || '';
280
+ if (response.status !== 200) throw new Error(`HTTP ${response.status} ${response.body}`);
281
+ if (sent.includes(NAME) || !sent.includes(CODE)) throw new Error(`le serveur distant a reçu : ${sent}`);
282
+ if (!response.body.includes(NAME) || response.body.includes(CODE)) throw new Error(`réponse non restituée : ${response.body}`);
283
+ return `le distant voit ${CODE}, le client lit « ${NAME} »`;
284
+ });
285
+
286
+ await step('flux SSE restitué', async () => {
287
+ const response = await postThroughProxy(port, ca, { body: JSON.stringify({ stream: true, messages: [] }), mode: 'sse' });
288
+ const text = streamedText(response.body);
289
+ if (response.status !== 200 || text !== `Le client ${NAME} a signé.`) throw new Error(`HTTP ${response.status}, texte « ${text} »`);
290
+ });
291
+
292
+ await step('refus d’un corps non JSON', async () => {
293
+ const before = upstream.seen.length;
294
+ const response = await postThroughProxy(port, ca, { body: `Contrat de ${NAME}`, contentType: 'text/plain' });
295
+ if (response.status !== 415 || upstream.seen.length !== before) throw new Error(`HTTP ${response.status}, transmis : ${upstream.seen.length - before}`);
296
+ });
297
+
298
+ await step('refus si le réécrivain tombe', async () => {
299
+ await bridge.close();
300
+ bridge = null;
301
+ const before = upstream.seen.length;
302
+ const response = await postThroughProxy(port, ca, { body: JSON.stringify({ messages: [{ role: 'user', content: NAME }] }) });
303
+ if (response.status !== 502 || upstream.seen.length !== before) throw new Error(`HTTP ${response.status}, transmis : ${upstream.seen.length - before}`);
304
+ });
305
+
306
+ await step('arrêt avec son parent', async () => {
307
+ child.stdin.end();
308
+ if (!(await waitForExit(child, 3000))) throw new Error('toujours en vie 3 s après la fermeture de son entrée');
309
+ });
310
+
311
+ return { ok: checks.every((check) => check.ok), sandboxed, checks };
312
+ } catch (error) {
313
+ record('banc de test', false, error.message);
314
+ return { ok: false, sandboxed, checks };
315
+ } finally {
316
+ if (child && child.exitCode === null && child.signalCode === null) {
317
+ child.kill('SIGKILL');
318
+ await waitForExit(child, 2000);
319
+ }
320
+ if (bridge) await bridge.close();
321
+ if (upstream) await new Promise((resolve) => upstream.server.close(resolve));
322
+ fs.rmSync(directory, { recursive: true, force: true });
323
+ }
324
+ }
325
+
326
+ function argument(name) {
327
+ const index = process.argv.indexOf(name);
328
+ return index === -1 ? null : process.argv[index + 1] || null;
329
+ }
330
+
331
+ if (require.main === module) {
332
+ runHudsuckerSmokeTest({
333
+ binary: argument('--binary') || defaultBinary(),
334
+ caCert: argument('--ca-cert'),
335
+ caKey: argument('--ca-key'),
336
+ sandbox: !process.argv.includes('--no-sandbox'),
337
+ report: ({ name, ok, detail }) => console.log(`${ok ? 'OK ' : 'ÉCHEC'} ${name}${detail ? ` — ${detail}` : ''}`),
338
+ }).then((result) => {
339
+ process.exitCode = result.ok ? 0 : 1;
340
+ });
341
+ }
342
+
343
+ module.exports = { runHudsuckerSmokeTest };
@@ -218,7 +218,7 @@ function createSseRewriter(transform) {
218
218
  return lines(JSON.stringify(parsed), undefined);
219
219
  }
220
220
  const keys = [...held.keys()].filter((key) => key.startsWith(keyPrefix));
221
- return withFlush(keys, JSON.stringify(parsed), parsed.type);
221
+ return withFlush(keys, JSON.stringify(rewriteJsonValue(parsed, transform)), parsed.type);
222
222
  }
223
223
 
224
224
  function rewriteResponses(parsed) {
@@ -236,7 +236,7 @@ function createSseRewriter(transform) {
236
236
  const keys = eventType === 'response.completed'
237
237
  ? [...held.keys()]
238
238
  : eventType.endsWith('.done') ? [key] : [];
239
- return withFlush(keys, JSON.stringify(parsed), parsed.type);
239
+ return withFlush(keys, JSON.stringify(rewriteJsonValue(parsed, transform)), parsed.type);
240
240
  }
241
241
 
242
242
  function rewriteChatChunk(parsed) {
@@ -84,6 +84,7 @@ function startRewriterBridge({ dictionary, harness = null }) {
84
84
  const server = http.createServer((request, response) => {
85
85
  const provider = header(request, 'x-piecemaker-provider') || 'claude';
86
86
  const current = () => dictionary.get();
87
+ const outbound = () => (dictionary.getForOutbound ? dictionary.getForOutbound() : dictionary.get());
87
88
 
88
89
  if (request.method === 'GET' && request.url === '/health') {
89
90
  response.writeHead(200);
@@ -95,14 +96,14 @@ function startRewriterBridge({ dictionary, harness = null }) {
95
96
  readBody(request).then((body) => {
96
97
  stats.requests += 1;
97
98
  const contentType = header(request, 'content-type') || 'application/json';
98
- observeRequest(harness, body, contentType, provider);
99
- const dict = current();
99
+ const dict = outbound();
100
100
  const rewritten = !body.length || dict.empty
101
101
  ? body
102
102
  : Buffer.from(rewriteJsonBody(body, (text) => anonymize(text, dict)), 'utf8');
103
103
  if (!rewritten.equals(body)) stats.anonymized += 1;
104
104
  response.writeHead(200, { 'content-type': 'application/octet-stream', 'content-length': rewritten.length });
105
105
  response.end(rewritten);
106
+ setImmediate(() => observeRequest(harness, body, contentType, provider));
106
107
  }).catch((error) => {
107
108
  stats.failures += 1;
108
109
  stats.lastError = error.message;
@@ -165,15 +166,16 @@ function startRewriterBridge({ dictionary, harness = null }) {
165
166
  const id = header(request, 'x-piecemaker-ws');
166
167
  const direction = header(request, 'x-piecemaker-direction');
167
168
  readBody(request).then((body) => {
168
- const dict = current();
169
169
  const text = body.toString('utf8');
170
170
  if (direction === 'out') {
171
- observeRequest(harness, body, 'application/json', provider);
171
+ const dict = outbound();
172
172
  const rewritten = dict.empty ? text : rewriteJsonBody(text, (value) => anonymize(value, dict));
173
173
  response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
174
174
  response.end(rewritten);
175
+ setImmediate(() => observeRequest(harness, body, 'application/json', provider));
175
176
  return;
176
177
  }
178
+ const dict = current();
177
179
  let entry = sessions.get(id);
178
180
  if (!entry) {
179
181
  entry = { rewriter: createSseRewriter((value) => deanonymize(value, dict)) };
@@ -208,6 +210,7 @@ function startRewriterBridge({ dictionary, harness = null }) {
208
210
  response.writeHead(404);
209
211
  response.end();
210
212
  });
213
+ server.requestTimeout = 0;
211
214
 
212
215
  return new Promise((resolve, reject) => {
213
216
  server.once('error', reject);
@@ -0,0 +1,85 @@
1
+ const path = require('node:path');
2
+ const { SHARE_ENV, Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
3
+
4
+ const STATS_INTERVAL_MS = 1000;
5
+ const CLOSE_TIMEOUT_MS = 2000;
6
+
7
+ function runInsideWorker() {
8
+ const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
9
+ const { startRewriterBridge } = require('./rewriter-bridge.cjs');
10
+ const { createHarnessJuridique } = require('../harness/index.cjs');
11
+
12
+ const dictionary = createSqliteDictionaryLoader({ databasePath: workerData.databasePath });
13
+ const harness = createHarnessJuridique({ homeDir: workerData.homeDir, verifyResponses: false });
14
+
15
+ startRewriterBridge({ dictionary, harness }).then((bridge) => {
16
+ let published = '';
17
+ const publishStats = () => {
18
+ const snapshot = JSON.stringify(bridge.stats);
19
+ if (snapshot === published) return;
20
+ published = snapshot;
21
+ parentPort.postMessage({ type: 'stats', stats: bridge.stats });
22
+ };
23
+ const timer = setInterval(publishStats, STATS_INTERVAL_MS);
24
+ parentPort.on('message', (message) => {
25
+ if (message?.type !== 'close') return;
26
+ clearInterval(timer);
27
+ publishStats();
28
+ bridge.close().finally(() => {
29
+ dictionary.close();
30
+ parentPort.postMessage({ type: 'closed' });
31
+ });
32
+ });
33
+ parentPort.postMessage({ type: 'listening', port: bridge.port });
34
+ }, (error) => {
35
+ dictionary.close();
36
+ parentPort.postMessage({ type: 'error', message: error.message });
37
+ });
38
+ }
39
+
40
+ function startRewriterWorker({ databasePath, homeDir }) {
41
+ const worker = new Worker(path.join(__dirname, 'rewriter-worker.cjs'), {
42
+ workerData: { databasePath, homeDir },
43
+ env: SHARE_ENV,
44
+ });
45
+ const stats = { requests: 0, anonymized: 0, deanonymized: 0, failures: 0, lastError: null };
46
+
47
+ return new Promise((resolve, reject) => {
48
+ const fail = (error) => {
49
+ worker.terminate();
50
+ reject(error);
51
+ };
52
+ worker.on('error', (error) => {
53
+ stats.failures += 1;
54
+ stats.lastError = error.message;
55
+ fail(error);
56
+ });
57
+ worker.once('exit', (code) => reject(new Error(`réécrivain arrêté (${code})`)));
58
+ worker.on('message', (message) => {
59
+ if (message?.type === 'stats') Object.assign(stats, message.stats);
60
+ if (message?.type === 'error') fail(new Error(message.message));
61
+ if (message?.type !== 'listening') return;
62
+ resolve({
63
+ port: message.port,
64
+ stats,
65
+ close() {
66
+ return new Promise((done) => {
67
+ const timer = setTimeout(() => worker.terminate().then(() => done()), CLOSE_TIMEOUT_MS);
68
+ worker.once('exit', () => {
69
+ clearTimeout(timer);
70
+ done();
71
+ });
72
+ worker.on('message', (reply) => {
73
+ if (reply?.type === 'closed') worker.terminate();
74
+ });
75
+ worker.postMessage({ type: 'close' });
76
+ });
77
+ },
78
+ });
79
+ });
80
+ });
81
+ }
82
+
83
+ if (!isMainThread && workerData?.databasePath) runInsideWorker();
84
+
85
+ module.exports = { startRewriterWorker };
@@ -5,16 +5,16 @@ const path = require('node:path');
5
5
  const tls = require('node:tls');
6
6
 
7
7
  const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
8
- const { startRewriterBridge } = require('./rewriter-bridge.cjs');
8
+ const { startRewriterWorker } = require('./rewriter-worker.cjs');
9
9
  const { removeLegacyProxyConfig } = require('./client-config.cjs');
10
- const { createHarnessJuridique } = require('../harness/index.cjs');
11
10
 
12
11
  const DEFAULT_UPSTREAM = 'https://api.anthropic.com';
13
- const INTERCEPTED_HOSTS = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'];
12
+ const INTERCEPTED_HOSTS = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com', 'api.mistral.ai'];
14
13
  const COVERAGE = {
15
14
  claude: { state: 'filtered', detail: null, file: null },
16
15
  codex: { state: 'filtered', detail: null, file: null },
17
16
  opencode: { state: 'filtered', detail: null, file: null },
17
+ mistral: { state: 'filtered', detail: null, file: null },
18
18
  cursor: { state: 'blocked', detail: 'not-interceptable', file: null },
19
19
  };
20
20
 
@@ -52,7 +52,9 @@ function hudsuckerBinary() {
52
52
  path.join(__dirname, 'hudsucker-proxy', 'target', 'release', name),
53
53
  path.join(__dirname, 'hudsucker-proxy', 'target', 'debug', name),
54
54
  ];
55
- return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
55
+ const present = candidates.filter((candidate) => fs.existsSync(candidate));
56
+ if (!present.length) return candidates[0];
57
+ return present.reduce((newest, candidate) => (fs.statSync(candidate).mtimeMs > fs.statSync(newest).mtimeMs ? candidate : newest));
56
58
  }
57
59
 
58
60
  function certificatePaths() {
@@ -136,8 +138,8 @@ function proxyEnvironment(proxyUrl, caFile, trustBundle) {
136
138
  }
137
139
 
138
140
  function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = console }) {
139
- const dictionary = createSqliteDictionaryLoader({ databasePath: process.env.DATABASE_PATH || path.join(homeDir, 'auth.db') });
140
- const harness = createHarnessJuridique({ homeDir, verifyResponses: false });
141
+ const databasePath = process.env.DATABASE_PATH || path.join(homeDir, 'auth.db');
142
+ const dictionary = createSqliteDictionaryLoader({ databasePath });
141
143
  const binDir = path.join(homeDir, 'bin');
142
144
  const state = { enabled: false, reason: 'not_started', origin: null, upstream: null, coverage: {} };
143
145
  let proxy = null;
@@ -165,7 +167,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
165
167
  const hosts = [...new Set([...INTERCEPTED_HOSTS, new URL(upstream).hostname])];
166
168
 
167
169
  try {
168
- bridge = await startRewriterBridge({ dictionary, harness });
170
+ bridge = await startRewriterWorker({ databasePath, homeDir });
169
171
  } catch (error) {
170
172
  state.reason = `listen_failed: ${error.message}`;
171
173
  logger.warn?.(`[piecemaker] réécriture PII non démarrée : ${error.message}`);
@@ -221,7 +223,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
221
223
  upstream,
222
224
  coverage: COVERAGE,
223
225
  });
224
- logger.log?.(`[piecemaker] anonymisation active sur ${origin} (claude, codex, opencode ; cursor bloqué)`);
226
+ logger.log?.(`[piecemaker] anonymisation active sur ${origin} (claude, codex, opencode, mistral ; cursor bloqué)`);
225
227
  return state;
226
228
  }
227
229