@piecemaker-legal/piecemaker 2.0.9 → 2.0.11

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 (177) hide show
  1. package/dist/assets/FindReplaceDialog-KU5TEIBJ-BucOHLdm.js +1 -0
  2. package/dist/assets/FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js +1 -0
  3. package/dist/assets/HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js +144 -0
  4. package/dist/assets/HyperlinkDialog-ZYH7ZHRF-qIPBOssF.css +1 -0
  5. package/dist/assets/ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js +1 -0
  6. package/dist/assets/ImagePropertiesDialog-RAB47C6M-D86PckZ2.js +1 -0
  7. package/dist/assets/PageSetupDialog-AIGIXO3E-DLETP-7v.js +1 -0
  8. package/dist/assets/SplitCellDialog-IFBKYCD6-CwdQBXeb.js +1 -0
  9. package/dist/assets/TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js +1 -0
  10. package/dist/assets/WatermarkDialog-KRL7WMIS-XFJ9gTkC.js +1 -0
  11. package/dist/assets/{abnfDiagram-VCTEODGH-WrsSAHOe.js → abnfDiagram-VCTEODGH-DiCrND_Y.js} +1 -1
  12. package/dist/assets/arc-DOjgbMYz.js +1 -0
  13. package/dist/assets/architectureDiagram-5GKGNRK7-brzIFVhl.js +36 -0
  14. package/dist/assets/{blockDiagram-I7D4REHJ-zGMnF7Ox.js → blockDiagram-I7D4REHJ-C8JhTjQQ.js} +1 -1
  15. package/dist/assets/{c4Diagram-7LVT6UL2-ClWw3bqt.js → c4Diagram-7LVT6UL2-jfYxccyk.js} +1 -1
  16. package/dist/assets/channel-Dajzu2W1.js +1 -0
  17. package/dist/assets/{chunk-2Q5K7J3B-C-9ICPHv.js → chunk-2Q5K7J3B-CrlEShp9.js} +1 -1
  18. package/dist/assets/{chunk-5VM5RSS4-Bawy3teT.js → chunk-5VM5RSS4-DzPlUHGq.js} +1 -1
  19. package/dist/assets/{chunk-F27PBJKO-DQ375RQO.js → chunk-F27PBJKO-Cp9cpeWU.js} +1 -1
  20. package/dist/assets/{chunk-IMKFNOWR-CflNF2f6.js → chunk-IMKFNOWR-BifUldEN.js} +1 -1
  21. package/dist/assets/{chunk-JWPE2WC7-CIXQv0gc.js → chunk-JWPE2WC7-ggPEIkWu.js} +1 -1
  22. package/dist/assets/{chunk-POPQ4Y6H-Cza5j6Qw.js → chunk-POPQ4Y6H-3h_nayRX.js} +1 -1
  23. package/dist/assets/{chunk-SVP7TREG-GQ_VvWZR.js → chunk-SVP7TREG-CH29aG6J.js} +1 -1
  24. package/dist/assets/{chunk-TICWLB2K-BEmU16W5.js → chunk-TICWLB2K-PluJUM5k.js} +1 -1
  25. package/dist/assets/{chunk-XXDRQBXY-C1KpkLYH.js → chunk-XXDRQBXY-DXU39Vwg.js} +1 -1
  26. package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +1 -0
  27. package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +1 -0
  28. package/dist/assets/{cose-bilkent-JH36ORCC-CZE48TRG.js → cose-bilkent-JH36ORCC-Dz6roYrN.js} +1 -1
  29. package/dist/assets/{cynefin-OW5HDTMX-L-tx1eC-.js → cynefin-OW5HDTMX-BLzrpSit.js} +1 -1
  30. package/dist/assets/{cynefinDiagram-5FMLGOSQ-Coa8oJwR.js → cynefinDiagram-5FMLGOSQ-kfxnERDZ.js} +1 -1
  31. package/dist/assets/{dagre-GXQ25YYZ-Bu_3Y75K.js → dagre-GXQ25YYZ-DP57bp-N.js} +1 -1
  32. package/dist/assets/{diagram-S7CK7UJ4-CFiiV4es.js → diagram-S7CK7UJ4-DAlsrQVW.js} +1 -1
  33. package/dist/assets/{diagram-UQ7AKVKN-CCXheGAA.js → diagram-UQ7AKVKN-By5yrMer.js} +1 -1
  34. package/dist/assets/{diagram-VSXAHHWV-B70m4lr_.js → diagram-VSXAHHWV-Dx89kryN.js} +1 -1
  35. package/dist/assets/{diagram-VX7I27RA-CHkiMOVA.js → diagram-VX7I27RA-B9fARitZ.js} +1 -1
  36. package/dist/assets/{diagram-Z3DM3KII-_awpc4Bs.js → diagram-Z3DM3KII-DC15CEH1.js} +1 -1
  37. package/dist/assets/{ebnfDiagram-PWID7BFC-CZzjH86H.js → ebnfDiagram-PWID7BFC-BKQzghGF.js} +1 -1
  38. package/dist/assets/{erDiagram-RLTQ6QDP-9DgiHZg9.js → erDiagram-RLTQ6QDP-CPY6RM7G.js} +1 -1
  39. package/dist/assets/{flowDiagram-HODETNUW-DHY_503b.js → flowDiagram-HODETNUW-4dL4GJcf.js} +1 -1
  40. package/dist/assets/{ganttDiagram-EL5Y4UJY-DPiiYZ_l.js → ganttDiagram-EL5Y4UJY-D4-cVcfb.js} +1 -1
  41. package/dist/assets/{gitGraphDiagram-WWUBYQGX-Ddd_evm7.js → gitGraphDiagram-WWUBYQGX-BMeVIEiX.js} +1 -1
  42. package/dist/assets/{index-tibHq7Qp.js → index-DNtFDOni.js} +140 -140
  43. package/dist/assets/index-Dc9wNuS4.css +1 -0
  44. package/dist/assets/{infoDiagram-27XIBGKW-BPRNhfwb.js → infoDiagram-27XIBGKW-D5qMSUEU.js} +1 -1
  45. package/dist/assets/{ishikawaDiagram-5VMMS53U-BXx6aqkq.js → ishikawaDiagram-5VMMS53U-BDnyKRHm.js} +1 -1
  46. package/dist/assets/{journeyDiagram-3NMN7TZE-BK8TZM29.js → journeyDiagram-3NMN7TZE-dGz5ZGzq.js} +1 -1
  47. package/dist/assets/{kanban-definition-UXKFOSKX-CHioRmxz.js → kanban-definition-UXKFOSKX-D7UAz7o_.js} +6 -6
  48. package/dist/assets/layout-DKrmEGrz.js +1 -0
  49. package/dist/assets/{linear-CTEhUcKG.js → linear-CrTdMY-p.js} +1 -1
  50. package/dist/assets/{mermaid.core-DYUb_UUB.js → mermaid.core-CXQOT5G6.js} +7 -7
  51. package/dist/assets/{mindmap-definition-YA3MSWOX-D7ODZ4oS.js → mindmap-definition-YA3MSWOX-VgEiYfay.js} +1 -1
  52. package/dist/assets/{pegDiagram-XKGWAZYB-8J-_2_kw.js → pegDiagram-XKGWAZYB-9IqT8gGx.js} +1 -1
  53. package/dist/assets/{pieDiagram-E7YTZNPT-ebg0yfMi.js → pieDiagram-E7YTZNPT-D98pRE-t.js} +1 -1
  54. package/dist/assets/processTemplate-GPFDLQMX-B1sVV2O0.js +22 -0
  55. package/dist/assets/{quadrantDiagram-AXDQQJYC-BoWkDiFb.js → quadrantDiagram-AXDQQJYC-b9PwkYzL.js} +1 -1
  56. package/dist/assets/{railroadDiagram-O6MQD6OU-BRJ77ldz.js → railroadDiagram-O6MQD6OU-Dz87hsPE.js} +1 -1
  57. package/dist/assets/{requirementDiagram-BXWQKSXE-Z0JJgktA.js → requirementDiagram-BXWQKSXE-CkcV73dK.js} +1 -1
  58. package/dist/assets/sankeyDiagram-P5KCCOFB-UBN31Yrs.js +40 -0
  59. package/dist/assets/{sequenceDiagram-WJ2MYXX4-BPAjtXwp.js → sequenceDiagram-WJ2MYXX4-CpQxd5Za.js} +1 -1
  60. package/dist/assets/{server.browser-BzI1jrZw.js → server.browser-BY3bL-IX.js} +1 -1
  61. package/dist/assets/{sizeCapture-INFHLROL-BGZPCzM4.js → sizeCapture-INFHLROL-mCsFmyzw.js} +1 -1
  62. package/dist/assets/stateDiagram-D77RDMKH-B0MTBN-3.js +1 -0
  63. package/dist/assets/stateDiagram-v2-MP3YSRHH-DzBPN39W.js +1 -0
  64. package/dist/assets/{swimlanes-42K2YHIH-DvuB15qF.js → swimlanes-42K2YHIH-Dzydjm0e.js} +1 -1
  65. package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +8 -0
  66. package/dist/assets/{timeline-definition-24CTP7MA-ZIziYCIF.js → timeline-definition-24CTP7MA-Dz0toOLu.js} +3 -3
  67. package/dist/assets/{vendor-codemirror-CjW0lHR5.js → vendor-codemirror-CN-sJePf.js} +2 -2
  68. package/dist/assets/{vendor-react-D809iwXR.js → vendor-react-B22wqN3J.js} +5 -5
  69. package/dist/assets/{vennDiagram-4TSXK5OY-C5_B9gWa.js → vennDiagram-4TSXK5OY-DOWp7dkf.js} +4 -4
  70. package/dist/assets/{wardleyDiagram-VM6X3IG4-DMlH9Afw.js → wardleyDiagram-VM6X3IG4-CUxsztUB.js} +1 -1
  71. package/dist/assets/{xychartDiagram-S5SC5T6Z-BldhJt7i.js → xychartDiagram-S5SC5T6Z-DS96vNED.js} +1 -1
  72. package/dist/index.html +4 -4
  73. package/dist/sw.js +1 -1
  74. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js +4 -53
  75. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js +2 -3
  77. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/mistral/mistral-vibe-models.js +30 -0
  79. package/dist-server/server/modules/providers/list/mistral/mistral-vibe-models.js.map +1 -0
  80. package/dist-server/server/modules/providers/tests/mistral-vibe-models.test.js +24 -0
  81. package/dist-server/server/modules/providers/tests/mistral-vibe-models.test.js.map +1 -0
  82. package/dist-server/server/piecemaker/agent-instructions.js +148 -0
  83. package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
  84. package/dist-server/server/piecemaker/anonymizer/lifecycle.js +2 -3
  85. package/dist-server/server/piecemaker/anonymizer/lifecycle.js.map +1 -1
  86. package/dist-server/server/piecemaker/docx-document.js +50 -0
  87. package/dist-server/server/piecemaker/docx-document.js.map +1 -0
  88. package/dist-server/server/piecemaker/index.js +13 -2
  89. package/dist-server/server/piecemaker/index.js.map +1 -1
  90. package/dist-server/server/piecemaker/model-discovery/codex-catalog.js +46 -0
  91. package/dist-server/server/piecemaker/model-discovery/codex-catalog.js.map +1 -0
  92. package/dist-server/server/piecemaker/model-discovery/index.js +42 -0
  93. package/dist-server/server/piecemaker/model-discovery/index.js.map +1 -0
  94. package/dist-server/server/piecemaker/model-discovery/mistral-catalog.js +19 -0
  95. package/dist-server/server/piecemaker/model-discovery/mistral-catalog.js.map +1 -0
  96. package/dist-server/server/piecemaker/model-discovery/stdio-rpc.js +61 -0
  97. package/dist-server/server/piecemaker/model-discovery/stdio-rpc.js.map +1 -0
  98. package/dist-server/server/piecemaker/project-registry.js +29 -0
  99. package/dist-server/server/piecemaker/project-registry.js.map +1 -0
  100. package/package.json +12 -1
  101. package/scripts/piecemaker/cli/disable-anonymizer.mjs +19 -0
  102. package/scripts/piecemaker/cli/lib/config.mjs +0 -1
  103. package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
  104. package/scripts/piecemaker/cli/lib/services.mjs +1 -10
  105. package/scripts/piecemaker/cli/piecemaker.mjs +0 -3
  106. package/server/modules/providers/list/mistral/mistral-models.provider.ts +4 -53
  107. package/server/modules/providers/list/mistral/mistral-runtime.provider.ts +2 -3
  108. package/server/modules/providers/list/mistral/mistral-vibe-models.ts +43 -0
  109. package/server/modules/providers/tests/mistral-vibe-models.test.ts +27 -0
  110. package/server/piecemaker/agent-instructions.ts +148 -0
  111. package/server/piecemaker/anonymizer/client-config.cjs +92 -92
  112. package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
  113. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +9 -16
  114. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +1 -1
  115. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +251 -268
  116. package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
  117. package/server/piecemaker/anonymizer/lifecycle.ts +2 -3
  118. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +13 -4
  119. package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
  120. package/server/piecemaker/anonymizer/service.cjs +109 -179
  121. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +12 -1
  122. package/server/piecemaker/docx-document.ts +54 -0
  123. package/server/piecemaker/harness/decisions.cjs +1 -1
  124. package/server/piecemaker/harness/index.cjs +2 -3
  125. package/server/piecemaker/harness/verification.cjs +1 -1
  126. package/server/piecemaker/index.ts +15 -3
  127. package/server/piecemaker/model-discovery/codex-catalog.ts +67 -0
  128. package/server/piecemaker/model-discovery/index.ts +69 -0
  129. package/server/piecemaker/model-discovery/mistral-catalog.ts +36 -0
  130. package/server/piecemaker/model-discovery/stdio-rpc.ts +79 -0
  131. package/server/piecemaker/project-registry.ts +34 -0
  132. package/server/piecemaker/router.cjs +6 -8
  133. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +31 -77
  134. package/server/piecemaker/vendor/installer/lib/codex-skills.mjs +2 -84
  135. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -24
  136. package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
  137. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +10 -21
  138. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -11
  139. package/server/piecemaker/vendor/installer/steps/09-codex-plugin.mjs +6 -36
  140. package/server/piecemaker/vendor/installer/steps/13-garde-secrets.mjs +30 -12
  141. package/server/piecemaker/vendor/piecemaker-plugin/README.md +2 -9
  142. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +5 -5
  143. package/server/piecemaker/vendor/piecemaker-plugin/scripts/classify-ai-documents.mjs +8 -8
  144. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +2 -2
  145. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +4 -6
  146. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
  147. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +71 -38
  148. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +8 -38
  149. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +82 -61
  150. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
  151. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +4 -55
  152. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +24 -117
  153. package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
  154. package/server/piecemaker/vendor/piecemaker-plugin/skills/tamponnage/SKILL.md +2 -2
  155. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +14 -148
  156. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +16 -42
  157. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +0 -73
  158. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +0 -7
  159. package/server/piecemaker/vendor/websocket-server/stamping-routes.cjs +3 -12
  160. package/server/piecemaker/vendor/websocket-server/workspace-paths.cjs +6 -6
  161. package/dist/assets/arc-DAA6wH-C.js +0 -1
  162. package/dist/assets/architectureDiagram-5GKGNRK7-BGNHr7ef.js +0 -36
  163. package/dist/assets/channel-BAQyfMll.js +0 -1
  164. package/dist/assets/classDiagram-ZZMXUADV-BNGCuvrR.js +0 -1
  165. package/dist/assets/classDiagram-v2-VYDZK3BY-BNGCuvrR.js +0 -1
  166. package/dist/assets/index-WwknP0l4.css +0 -1
  167. package/dist/assets/layout-DEnkyfTY.js +0 -1
  168. package/dist/assets/sankeyDiagram-P5KCCOFB-4PW5HTOP.js +0 -40
  169. package/dist/assets/stateDiagram-D77RDMKH-o-vEcKwz.js +0 -1
  170. package/dist/assets/stateDiagram-v2-MP3YSRHH-BsuDaKkN.js +0 -1
  171. package/dist/assets/swimlanesDiagram-VR7AAH4N-DcCAyKlT.js +0 -8
  172. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +0 -78
  173. package/scripts/piecemaker/hooks/proxy-guard.mjs +0 -251
  174. package/server/piecemaker/anonymizer/providers.cjs +0 -120
  175. package/server/piecemaker/vendor/installer/steps/08-telegram.mjs +0 -482
  176. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/central-mapping.cjs +0 -0
  177. package/server/piecemaker/vendor/websocket-server/telegram-admin.cjs +0 -602
@@ -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 };
@@ -1,15 +1,14 @@
1
1
  type AnonymizerService = {
2
2
  start(): Promise<unknown>;
3
3
  stop(): Promise<unknown>;
4
- status(): { enabled: boolean; listening: boolean; coverage: Record<string, { state: string }> };
4
+ status(): { enabled: boolean; listening: boolean; reason?: string };
5
5
  };
6
6
 
7
7
  export async function startRequiredAnonymizer(service: AnonymizerService) {
8
8
  const ensureReady = () => {
9
9
  const status = service.status();
10
+ if (status.reason === 'disabled') return;
10
11
  if (!status.enabled || !status.listening) throw new Error('PieceMaker : le proxy PII est indisponible. Démarrage IA refusé.');
11
- const missing = ['claude', 'codex'].filter((provider) => status.coverage[provider]?.state !== 'filtered');
12
- if (missing.length) throw new Error(`PieceMaker : routage PII non configuré pour ${missing.join(', ')}. Démarrage IA refusé.`);
13
12
  };
14
13
  try {
15
14
  await service.start();
@@ -84,19 +84,26 @@ 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());
88
+
89
+ if (request.method === 'GET' && request.url === '/health') {
90
+ response.writeHead(200);
91
+ response.end('ok');
92
+ return;
93
+ }
87
94
 
88
95
  if (request.method === 'POST' && request.url === '/v1/request') {
89
96
  readBody(request).then((body) => {
90
97
  stats.requests += 1;
91
98
  const contentType = header(request, 'content-type') || 'application/json';
92
- observeRequest(harness, body, contentType, provider);
93
- const dict = current();
99
+ const dict = outbound();
94
100
  const rewritten = !body.length || dict.empty
95
101
  ? body
96
102
  : Buffer.from(rewriteJsonBody(body, (text) => anonymize(text, dict)), 'utf8');
97
103
  if (!rewritten.equals(body)) stats.anonymized += 1;
98
104
  response.writeHead(200, { 'content-type': 'application/octet-stream', 'content-length': rewritten.length });
99
105
  response.end(rewritten);
106
+ setImmediate(() => observeRequest(harness, body, contentType, provider));
100
107
  }).catch((error) => {
101
108
  stats.failures += 1;
102
109
  stats.lastError = error.message;
@@ -159,15 +166,16 @@ function startRewriterBridge({ dictionary, harness = null }) {
159
166
  const id = header(request, 'x-piecemaker-ws');
160
167
  const direction = header(request, 'x-piecemaker-direction');
161
168
  readBody(request).then((body) => {
162
- const dict = current();
163
169
  const text = body.toString('utf8');
164
170
  if (direction === 'out') {
165
- observeRequest(harness, body, 'application/json', provider);
171
+ const dict = outbound();
166
172
  const rewritten = dict.empty ? text : rewriteJsonBody(text, (value) => anonymize(value, dict));
167
173
  response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
168
174
  response.end(rewritten);
175
+ setImmediate(() => observeRequest(harness, body, 'application/json', provider));
169
176
  return;
170
177
  }
178
+ const dict = current();
171
179
  let entry = sessions.get(id);
172
180
  if (!entry) {
173
181
  entry = { rewriter: createSseRewriter((value) => deanonymize(value, dict)) };
@@ -202,6 +210,7 @@ function startRewriterBridge({ dictionary, harness = null }) {
202
210
  response.writeHead(404);
203
211
  response.end();
204
212
  });
213
+ server.requestTimeout = 0;
205
214
 
206
215
  return new Promise((resolve, reject) => {
207
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 };