@piecemaker-legal/piecemaker 1.1.1 → 2.0.2

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 (141) hide show
  1. package/dist/assets/{abnfDiagram-VCTEODGH-DEXWh3LH.js → abnfDiagram-VCTEODGH-BeQS_HnO.js} +1 -1
  2. package/dist/assets/{arc-CoTRLXKW.js → arc-ClYjD3aM.js} +1 -1
  3. package/dist/assets/{architectureDiagram-5GKGNRK7-Cg-jO9oR.js → architectureDiagram-5GKGNRK7-Csdihb1L.js} +1 -1
  4. package/dist/assets/{blockDiagram-I7D4REHJ-ChXwQSIq.js → blockDiagram-I7D4REHJ-BuXey9ht.js} +1 -1
  5. package/dist/assets/{c4Diagram-7LVT6UL2-Bw2VGggd.js → c4Diagram-7LVT6UL2-DdO-ovOy.js} +1 -1
  6. package/dist/assets/channel-DhuuSCHK.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-C1WdqrLP.js → chunk-2Q5K7J3B-D3hzWzSc.js} +1 -1
  8. package/dist/assets/{chunk-5VM5RSS4-BerUZBa1.js → chunk-5VM5RSS4-DeT3cR6k.js} +1 -1
  9. package/dist/assets/{chunk-F27PBJKO-5W7ax2gB.js → chunk-F27PBJKO-bazGi1ru.js} +1 -1
  10. package/dist/assets/{chunk-IMKFNOWR-JgDN1h4T.js → chunk-IMKFNOWR-BE3ZXrPd.js} +1 -1
  11. package/dist/assets/{chunk-JWPE2WC7-ulNHnEdQ.js → chunk-JWPE2WC7-dTAsgByE.js} +1 -1
  12. package/dist/assets/{chunk-POPQ4Y6H-D4aTDC2T.js → chunk-POPQ4Y6H-DKYrzMSd.js} +1 -1
  13. package/dist/assets/{chunk-SVP7TREG-BA5zX4XF.js → chunk-SVP7TREG-BTtrMvhE.js} +1 -1
  14. package/dist/assets/{chunk-TICWLB2K-DhOULVhX.js → chunk-TICWLB2K-CZPHShPS.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-CkpCgrVg.js → chunk-XXDRQBXY-B0byFHaU.js} +1 -1
  16. package/dist/assets/classDiagram-ZZMXUADV-CO0E82Dl.js +1 -0
  17. package/dist/assets/classDiagram-v2-VYDZK3BY-CO0E82Dl.js +1 -0
  18. package/dist/assets/{cose-bilkent-JH36ORCC-0xUSfjfV.js → cose-bilkent-JH36ORCC-BbLCzlaU.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BE6fNu2_.js → cynefin-OW5HDTMX-CQ8ui4YD.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-5FMLGOSQ-rFMnZ9b3.js → cynefinDiagram-5FMLGOSQ-T-GL1aIf.js} +1 -1
  21. package/dist/assets/{dagre-GXQ25YYZ-C1p4ffd7.js → dagre-GXQ25YYZ-CXxVKznH.js} +1 -1
  22. package/dist/assets/{diagram-S7CK7UJ4-BV_3Xj4O.js → diagram-S7CK7UJ4-B_-1Mf_7.js} +1 -1
  23. package/dist/assets/{diagram-UQ7AKVKN-Bb_Evkrm.js → diagram-UQ7AKVKN-Bs-OB8xI.js} +1 -1
  24. package/dist/assets/{diagram-VSXAHHWV-BzgupjxD.js → diagram-VSXAHHWV-BpUqq93o.js} +1 -1
  25. package/dist/assets/{diagram-VX7I27RA-D1c67z76.js → diagram-VX7I27RA-CZ6hSUo6.js} +1 -1
  26. package/dist/assets/{diagram-Z3DM3KII-DsJAJGKE.js → diagram-Z3DM3KII-DVZiDO6S.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-PWID7BFC-B1ge_obq.js → ebnfDiagram-PWID7BFC-D_1izoKK.js} +1 -1
  28. package/dist/assets/{erDiagram-RLTQ6QDP-DrV-tggw.js → erDiagram-RLTQ6QDP-_jSi4sg-.js} +1 -1
  29. package/dist/assets/{flowDiagram-HODETNUW-CVzDiSQ3.js → flowDiagram-HODETNUW-BtBM7ez3.js} +1 -1
  30. package/dist/assets/{ganttDiagram-EL5Y4UJY-CGRdSN9D.js → ganttDiagram-EL5Y4UJY-Bz2nxw7c.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-WWUBYQGX-CAfvFXzL.js → gitGraphDiagram-WWUBYQGX-8Q3I3hoE.js} +1 -1
  32. package/dist/assets/{index-DaxH81ZJ.js → index-CZUj0Tlu.js} +152 -152
  33. package/dist/assets/index-DcfdIMj-.css +1 -0
  34. package/dist/assets/{infoDiagram-27XIBGKW-DTshEkGK.js → infoDiagram-27XIBGKW-Cc6eKjfU.js} +1 -1
  35. package/dist/assets/{ishikawaDiagram-5VMMS53U-DkbshVxp.js → ishikawaDiagram-5VMMS53U-Bd3w97qM.js} +1 -1
  36. package/dist/assets/{journeyDiagram-3NMN7TZE-v5qE8xpn.js → journeyDiagram-3NMN7TZE-Cy6XbK5X.js} +1 -1
  37. package/dist/assets/{kanban-definition-UXKFOSKX-a9CNMFLt.js → kanban-definition-UXKFOSKX-kyLqWDpr.js} +1 -1
  38. package/dist/assets/{layout-DrW4R1TH.js → layout-DXpPSEft.js} +1 -1
  39. package/dist/assets/{linear-Cs3qpXT_.js → linear-DH_h9cDZ.js} +1 -1
  40. package/dist/assets/{mermaid.core-C7Ti-Fty.js → mermaid.core-BghXiB7p.js} +6 -6
  41. package/dist/assets/{mindmap-definition-YA3MSWOX-B0VDdIcG.js → mindmap-definition-YA3MSWOX-C2xxEXsP.js} +1 -1
  42. package/dist/assets/{pegDiagram-XKGWAZYB-CV1L7MjP.js → pegDiagram-XKGWAZYB-DuppHX4-.js} +1 -1
  43. package/dist/assets/{pieDiagram-E7YTZNPT-DSCuzVTV.js → pieDiagram-E7YTZNPT-BCukPZ2a.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-AXDQQJYC-hzX-9AKo.js → quadrantDiagram-AXDQQJYC-Dm_ojY_7.js} +1 -1
  45. package/dist/assets/{railroadDiagram-O6MQD6OU-BbmzIqcp.js → railroadDiagram-O6MQD6OU-BzdOv7BJ.js} +1 -1
  46. package/dist/assets/{requirementDiagram-BXWQKSXE-CQCRRBSL.js → requirementDiagram-BXWQKSXE-DgewPP_g.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-P5KCCOFB-kiaLcW6X.js → sankeyDiagram-P5KCCOFB-vEIMvCSt.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bgx8ST7Q.js → sequenceDiagram-WJ2MYXX4-BL_31NoY.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-BTjJ-ygN.js → sizeCapture-INFHLROL-Ch6u4xo6.js} +1 -1
  50. package/dist/assets/{stateDiagram-D77RDMKH-D48RR1FL.js → stateDiagram-D77RDMKH-ByquMuJv.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-MP3YSRHH-B-XwSR-z.js → stateDiagram-v2-MP3YSRHH-_QRJP_vn.js} +1 -1
  52. package/dist/assets/{swimlanes-42K2YHIH-BhE-QuEZ.js → swimlanes-42K2YHIH-BZyXEHOb.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-VR7AAH4N-sqZwZwbP.js +8 -0
  54. package/dist/assets/{timeline-definition-24CTP7MA-Cm7EUWWt.js → timeline-definition-24CTP7MA-CLBivayK.js} +1 -1
  55. package/dist/assets/{vennDiagram-4TSXK5OY-X-nC9wZ0.js → vennDiagram-4TSXK5OY-3dJ2PRqR.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VM6X3IG4-D33YvEQZ.js → wardleyDiagram-VM6X3IG4-C279LT36.js} +1 -1
  57. package/dist/assets/{xychartDiagram-S5SC5T6Z-lziwBKsM.js → xychartDiagram-S5SC5T6Z-FoqPDrei.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/sw.js +1 -1
  60. package/dist-server/server/index.js +3 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/piecemaker/harness/citations.routes.js +18 -1
  63. package/dist-server/server/piecemaker/harness/citations.routes.js.map +1 -1
  64. package/dist-server/server/piecemaker/harness/legifrance-html.js +112 -0
  65. package/dist-server/server/piecemaker/harness/legifrance-html.js.map +1 -0
  66. package/dist-server/server/piecemaker/index.js +7 -3
  67. package/dist-server/server/piecemaker/index.js.map +1 -1
  68. package/dist-server/server/piecemaker/knowledge/index.js +13 -6
  69. package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
  70. package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
  71. package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
  72. package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
  73. package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
  74. package/dist-server/server/piecemaker/library/index.js +14 -41
  75. package/dist-server/server/piecemaker/library/index.js.map +1 -1
  76. package/dist-server/server/piecemaker/library/store.js +11 -3
  77. package/dist-server/server/piecemaker/library/store.js.map +1 -1
  78. package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
  79. package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
  80. package/dist-server/server/piecemaker/timesheet/service.js +8 -7
  81. package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
  82. package/dist-server/server/piecemaker/timesheet/store.js +5 -3
  83. package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
  84. package/package.json +1 -1
  85. package/product.config.json +2 -0
  86. package/scripts/piecemaker/build-app-icon.py +88 -0
  87. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
  88. package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
  89. package/scripts/release/assert-piecemaker-version.mjs +25 -0
  90. package/server/index.ts +4 -1
  91. package/server/piecemaker/anonymizer/client-config.cjs +45 -162
  92. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
  93. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
  94. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
  95. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
  96. package/server/piecemaker/anonymizer/providers.cjs +14 -135
  97. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
  98. package/server/piecemaker/anonymizer/service.cjs +144 -28
  99. package/server/piecemaker/harness/citations.routes.ts +23 -1
  100. package/server/piecemaker/harness/legifrance-html.ts +107 -0
  101. package/server/piecemaker/index.ts +8 -3
  102. package/server/piecemaker/knowledge/index.ts +14 -6
  103. package/server/piecemaker/knowledge/local-routes.ts +72 -0
  104. package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
  105. package/server/piecemaker/library/index.ts +14 -34
  106. package/server/piecemaker/library/store.ts +11 -3
  107. package/server/piecemaker/library/workspace-installation.ts +20 -3
  108. package/server/piecemaker/timesheet/service.ts +9 -7
  109. package/server/piecemaker/timesheet/store.ts +5 -3
  110. package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
  111. package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
  112. package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
  113. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
  114. package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
  115. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
  116. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
  117. package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
  118. package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
  119. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
  120. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
  121. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
  122. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
  123. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
  124. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
  125. package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
  126. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
  127. package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
  128. package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
  129. package/dist/assets/channel-DdzrPZ6Y.js +0 -1
  130. package/dist/assets/classDiagram-ZZMXUADV-Dim2o8E5.js +0 -1
  131. package/dist/assets/classDiagram-v2-VYDZK3BY-Dim2o8E5.js +0 -1
  132. package/dist/assets/index-D_Dc_nQv.css +0 -1
  133. package/dist/assets/swimlanesDiagram-VR7AAH4N-DR525d5V.js +0 -8
  134. package/server/piecemaker/anonymizer/proxy.cjs +0 -365
  135. package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
  136. package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
  137. package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
  138. package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
  139. package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
  140. package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
  141. package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
@@ -1,365 +0,0 @@
1
- /**
2
- * Proxy PII local — le seul point de passage entre les clients IA lancés par
3
- * CloudCLI (chat comme terminal) et les API des fournisseurs.
4
- *
5
- * Sans Python, sans venv, sans service de session à installer : il vit dans
6
- * le cycle de vie du serveur CloudCLI plutôt qu'à côté.
7
- *
8
- * Sens sortant : nom réel → code. Aucun nom de partie ne quitte la machine.
9
- * Sens entrant : code → nom réel. Claude Code retrouve un texte lisible, ses
10
- * outils continuent d'ouvrir les fichiers sous leur vrai nom, et
11
- * l'interface n'a plus qu'à surligner ce qui a été protégé.
12
- *
13
- * Sans mapping, le proxy est un relais transparent : le brancher ne peut pas
14
- * casser une installation qui n'a encore anonymisé aucun dossier.
15
- *
16
- * Une route par fournisseur, distinguée par le préfixe d'URL. Un chemin qui ne
17
- * correspond à aucune route est refusé : router par défaut reviendrait à
18
- * laisser passer non filtré ce que l'on n'a pas su reconnaître.
19
- */
20
- const http = require('node:http');
21
- const https = require('node:https');
22
-
23
- const { anonymize, deanonymize } = require('./dictionary.cjs');
24
- const { createSseRewriter, rewriteJsonBody } = require('./rewrite.cjs');
25
- const { createCollecteurTexte } = require('../harness/flux-sse.cjs');
26
-
27
- const DEFAULT_UPSTREAM = 'https://api.anthropic.com';
28
-
29
- /**
30
- * Routes servies par défaut. `/chatgpt` vise le point d'entrée Codex de
31
- * ChatGPT, où le CLI parle le protocole Responses avec l'authentification
32
- * ChatGPT.
33
- */
34
- const DEFAULT_ROUTES = [
35
- { provider: 'claude', prefix: '/anthropic', upstream: 'https://api.anthropic.com' },
36
- { provider: 'codex', prefix: '/chatgpt', upstream: 'https://chatgpt.com/backend-api/codex' },
37
- { provider: 'opencode', prefix: '/openai', upstream: 'https://api.openai.com' },
38
- ];
39
-
40
- /** Port fixe : une base écrite dans un fichier de configuration doit survivre à un redémarrage. */
41
- const DEFAULT_PORT = 4111;
42
-
43
- /**
44
- * En-têtes que le proxy possède et ne relaie donc jamais tels quels. Le corps
45
- * change de taille à la réécriture (`content-length`), et `accept-encoding` est
46
- * retiré pour recevoir du texte clair : compresser en amont rendrait la
47
- * dé-anonymisation impossible sans décompresser tout le flux.
48
- */
49
- const DROPPED_REQUEST_HEADERS = new Set(['host', 'content-length', 'accept-encoding', 'connection']);
50
- const DROPPED_RESPONSE_HEADERS = new Set(['content-length', 'content-encoding', 'connection', 'transfer-encoding']);
51
-
52
- function pickHeaders(source, dropped) {
53
- const output = {};
54
- for (const [key, value] of Object.entries(source)) {
55
- if (dropped.has(key.toLowerCase())) continue;
56
- output[key] = value;
57
- }
58
- return output;
59
- }
60
-
61
- function readBody(stream, limitBytes) {
62
- return new Promise((resolve, reject) => {
63
- const chunks = [];
64
- let size = 0;
65
- stream.on('data', (chunk) => {
66
- size += chunk.length;
67
- if (size > limitBytes) {
68
- reject(new Error(`Corps de requête trop volumineux (> ${limitBytes} octets).`));
69
- stream.destroy();
70
- return;
71
- }
72
- chunks.push(chunk);
73
- });
74
- stream.on('end', () => resolve(Buffer.concat(chunks)));
75
- stream.on('error', reject);
76
- });
77
- }
78
-
79
- function isJsonLike(contentType) {
80
- return /\bjson\b/i.test(String(contentType || ''));
81
- }
82
-
83
- function isEventStream(contentType) {
84
- return /text\/event-stream/i.test(String(contentType || ''));
85
- }
86
-
87
- /**
88
- * Texte assistant d'un corps JSON complet (non streamé), pour le harnais de
89
- * citations : formes Anthropic Messages (`content[]`, blocs `{type:'text'}`)
90
- * et Responses d'OpenAI (`output[].content[]`, blocs `{type:'output_text'}`).
91
- * Ne jette jamais : un corps illisible ou d'une autre forme renvoie `''`.
92
- */
93
- function extractAssistantTextFromJson(raw) {
94
- try {
95
- const parsed = JSON.parse(typeof raw === 'string' ? raw : raw.toString('utf8'));
96
- if (!parsed || typeof parsed !== 'object') return '';
97
- if (Array.isArray(parsed.content)) {
98
- return parsed.content
99
- .filter((block) => block?.type === 'text' && typeof block.text === 'string')
100
- .map((block) => block.text)
101
- .join('');
102
- }
103
- if (Array.isArray(parsed.output)) {
104
- return parsed.output
105
- .flatMap((item) => (Array.isArray(item?.content) ? item.content : []))
106
- .filter((block) => block?.type === 'output_text' && typeof block.text === 'string')
107
- .map((block) => block.text)
108
- .join('');
109
- }
110
- return '';
111
- } catch {
112
- return '';
113
- }
114
- }
115
-
116
- /**
117
- * @param {object} options
118
- * @param {{ get: () => object }} options.dictionary Chargeur de mapping à chaud.
119
- * @param {Array<{provider: string, prefix: string, upstream: string}>} [options.routes]
120
- * Table de routage ; le préfixe le plus long l'emporte.
121
- * @param {number} [options.port] Port préféré ; 0 = port éphémère.
122
- * @param {object|null} [options.harness] Harnais de citations vérifiées
123
- * (`../harness/index.cjs`), absent par défaut : sans lui, le proxy se
124
- * comporte exactement comme s'il n'existait pas.
125
- */
126
- function createAnonymizerProxy({
127
- dictionary,
128
- routes = DEFAULT_ROUTES,
129
- port = DEFAULT_PORT,
130
- host = '127.0.0.1',
131
- maxBodyBytes = 64 * 1024 * 1024,
132
- onError = () => {},
133
- harness = null,
134
- } = {}) {
135
- // Préfixe le plus long d'abord : `/anthropic/v1` doit gagner sur `/anthropic`
136
- // si les deux sont déclarés un jour.
137
- const table = [...routes]
138
- .map((route) => ({ ...route, target: new URL(route.upstream) }))
139
- .sort((left, right) => right.prefix.length - left.prefix.length);
140
- const stats = { requests: 0, anonymized: 0, deanonymized: 0, failures: 0, lastError: null };
141
-
142
- // Observation du harnais de citations, en espace clair des deux côtés
143
- // (corps brut du client à l'aller, texte livré au client au retour) — voir
144
- // `../harness/index.cjs`. Ne doit jamais retarder ni faire échouer une
145
- // requête ou une réponse déjà en cours, même si le harnais fourni est
146
- // défaillant (double garde : `index.cjs` ne jette déjà jamais, mais le
147
- // proxy ne doit dépendre de cette garantie).
148
- function observerRequete(rawBody, contentType, session) {
149
- if (!harness) return;
150
- try {
151
- harness.observerRequete(rawBody, contentType, { session });
152
- } catch {
153
- // jamais bloquant pour la requête en cours
154
- }
155
- }
156
-
157
- function observerReponse(texteAssistant, session) {
158
- if (!harness) return;
159
- try {
160
- Promise.resolve(harness.observerReponse(texteAssistant, { session })).catch(() => {});
161
- } catch {
162
- // jamais bloquant pour une réponse déjà envoyée au client
163
- }
164
- }
165
-
166
- function matchRoute(url) {
167
- return table.find((route) => url === route.prefix || url.startsWith(`${route.prefix}/`) || url.startsWith(`${route.prefix}?`));
168
- }
169
-
170
- const server = http.createServer((clientRequest, clientResponse) => {
171
- stats.requests += 1;
172
- const current = dictionary.get();
173
-
174
- const route = matchRoute(clientRequest.url || '');
175
- if (!route) {
176
- stats.failures += 1;
177
- stats.lastError = `Chemin non routé : ${clientRequest.url}`;
178
- clientResponse.writeHead(404, { 'content-type': 'application/json' });
179
- clientResponse.end(JSON.stringify({
180
- error: {
181
- type: 'piecemaker_proxy',
182
- message: `Aucune route d'anonymisation pour ${clientRequest.url}. Requête refusée plutôt que relayée sans filtrage.`,
183
- },
184
- }));
185
- return;
186
- }
187
- const { target } = route;
188
- const transport = target.protocol === 'http:' ? http : https;
189
- const forwardedPath = `${target.pathname.replace(/\/$/, '')}${clientRequest.url.slice(route.prefix.length) || '/'}`;
190
-
191
- void (async () => {
192
- let body = Buffer.alloc(0);
193
- try {
194
- body = await readBody(clientRequest, maxBodyBytes);
195
- } catch (error) {
196
- stats.failures += 1;
197
- stats.lastError = error.message;
198
- clientResponse.writeHead(413, { 'content-type': 'application/json' });
199
- clientResponse.end(JSON.stringify({ error: { type: 'piecemaker_proxy', message: error.message } }));
200
- return;
201
- }
202
-
203
- // Corps brut, AVANT anonymisation : le harnais doit voir le même texte
204
- // que le client a envoyé, pas les codes qui partent vers le fournisseur.
205
- observerRequete(body, clientRequest.headers['content-type'], route.provider);
206
-
207
- let outgoing = body;
208
- if (body.length && !current.empty && isJsonLike(clientRequest.headers['content-type'])) {
209
- const rewritten = rewriteJsonBody(body, (text) => anonymize(text, current));
210
- outgoing = Buffer.from(rewritten, 'utf8');
211
- if (!outgoing.equals(body)) stats.anonymized += 1;
212
- }
213
-
214
- const headers = pickHeaders(clientRequest.headers, DROPPED_REQUEST_HEADERS);
215
- headers.host = target.host;
216
- headers['accept-encoding'] = 'identity';
217
- if (outgoing.length) headers['content-length'] = String(outgoing.length);
218
-
219
- const upstreamRequest = transport.request(
220
- {
221
- protocol: target.protocol,
222
- hostname: target.hostname,
223
- port: target.port || (target.protocol === 'http:' ? 80 : 443),
224
- method: clientRequest.method,
225
- path: forwardedPath,
226
- headers,
227
- },
228
- (upstreamResponse) => {
229
- const responseHeaders = pickHeaders(upstreamResponse.headers, DROPPED_RESPONSE_HEADERS);
230
- clientResponse.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
231
-
232
- if (current.empty) {
233
- // Rien à dé-anonymiser, mais le harnais observe quand même ce qui
234
- // est livré au client — inchangé, aucune réécriture ici.
235
- if (isEventStream(upstreamResponse.headers['content-type'])) {
236
- const collecteur = createCollecteurTexte();
237
- upstreamResponse.setEncoding('utf8');
238
- upstreamResponse.on('data', (chunk) => {
239
- collecteur.push(chunk);
240
- clientResponse.write(chunk);
241
- });
242
- upstreamResponse.on('end', () => {
243
- clientResponse.end();
244
- observerReponse(collecteur.texte(), route.provider);
245
- });
246
- upstreamResponse.on('error', () => clientResponse.end());
247
- return;
248
- }
249
- if (isJsonLike(upstreamResponse.headers['content-type'])) {
250
- const chunks = [];
251
- upstreamResponse.on('data', (chunk) => chunks.push(chunk));
252
- upstreamResponse.on('end', () => {
253
- const raw = Buffer.concat(chunks);
254
- clientResponse.end(raw);
255
- observerReponse(extractAssistantTextFromJson(raw), route.provider);
256
- });
257
- upstreamResponse.on('error', () => clientResponse.end());
258
- return;
259
- }
260
- upstreamResponse.pipe(clientResponse);
261
- return;
262
- }
263
-
264
- const revert = (text) => deanonymize(text, current);
265
-
266
- if (isEventStream(upstreamResponse.headers['content-type'])) {
267
- const rewriter = createSseRewriter(revert);
268
- const collecteur = createCollecteurTexte();
269
- upstreamResponse.setEncoding('utf8');
270
- upstreamResponse.on('data', (chunk) => {
271
- const out = rewriter.push(chunk);
272
- if (out) {
273
- clientResponse.write(out);
274
- collecteur.push(out);
275
- }
276
- });
277
- upstreamResponse.on('end', () => {
278
- const tail = rewriter.end();
279
- if (tail) {
280
- clientResponse.write(tail);
281
- collecteur.push(tail);
282
- }
283
- stats.deanonymized += 1;
284
- clientResponse.end();
285
- observerReponse(collecteur.texte(), route.provider);
286
- });
287
- upstreamResponse.on('error', () => clientResponse.end());
288
- return;
289
- }
290
-
291
- if (isJsonLike(upstreamResponse.headers['content-type'])) {
292
- const chunks = [];
293
- upstreamResponse.on('data', (chunk) => chunks.push(chunk));
294
- upstreamResponse.on('end', () => {
295
- const raw = Buffer.concat(chunks);
296
- const rewritten = rewriteJsonBody(raw, revert);
297
- stats.deanonymized += 1;
298
- clientResponse.end(rewritten);
299
- observerReponse(extractAssistantTextFromJson(rewritten), route.provider);
300
- });
301
- upstreamResponse.on('error', () => clientResponse.end());
302
- return;
303
- }
304
-
305
- upstreamResponse.pipe(clientResponse);
306
- },
307
- );
308
-
309
- upstreamRequest.on('error', (error) => {
310
- stats.failures += 1;
311
- stats.lastError = error.message;
312
- onError(error);
313
- if (clientResponse.headersSent) {
314
- clientResponse.end();
315
- return;
316
- }
317
- clientResponse.writeHead(502, { 'content-type': 'application/json' });
318
- clientResponse.end(JSON.stringify({
319
- error: { type: 'piecemaker_proxy', message: `Relais indisponible : ${error.message}` },
320
- }));
321
- });
322
-
323
- if (outgoing.length) upstreamRequest.write(outgoing);
324
- upstreamRequest.end();
325
- })();
326
- });
327
-
328
- function listenOn(candidate) {
329
- return new Promise((resolve, reject) => {
330
- const onError = (error) => reject(error);
331
- server.once('error', onError);
332
- server.listen(candidate, host, () => {
333
- server.removeListener('error', onError);
334
- const address = server.address();
335
- resolve({ port: address.port, origin: `http://${host}:${address.port}` });
336
- });
337
- });
338
- }
339
-
340
- return {
341
- stats,
342
- get listening() { return server.listening; },
343
- routes: table.map(({ provider, prefix, target }) => ({ provider, prefix, upstream: target.origin })),
344
- /**
345
- * Écoute le port demandé, et lui seul. La base écrite dans les
346
- * configurations des clients leur survit sur disque : un port qui dérive
347
- * la rend définitivement fausse pour la session suivante. Un port déjà
348
- * pris signale une instance encore vivante, pas une invitation à bouger.
349
- */
350
- async listen() {
351
- try {
352
- return await listenOn(port);
353
- } catch (error) {
354
- if (error.code !== 'EADDRINUSE') throw error;
355
- throw new Error(`Port ${port} déjà occupé : une instance du proxy PII écoute encore. `
356
- + `Arrêtez-la (lsof -nP -iTCP:${port} -sTCP:LISTEN) avant de redémarrer.`);
357
- }
358
- },
359
- close() {
360
- return new Promise((resolve) => server.close(() => resolve()));
361
- },
362
- };
363
- }
364
-
365
- module.exports = { DEFAULT_UPSTREAM, createAnonymizerProxy };
@@ -1,189 +0,0 @@
1
- import Cocoa
2
- import WebKit
3
-
4
- private let kAdminURL = "https://localhost:43098/admin/"
5
- private let kRetryInterval: TimeInterval = 1.0
6
- private let kMaxRetries = 30
7
- private let kSections = [
8
- (label: "Dossiers", id: "history"),
9
- (label: "Configuration", id: "configuration"),
10
- (label: "Tampon et pièces", id: "pieces"),
11
- (label: "Skills et agents", id: "files"),
12
- ]
13
-
14
- class AppDelegate: NSObject, NSApplicationDelegate {
15
- var window: NSWindow!
16
- var webView: WKWebView!
17
- var sectionControl: NSSegmentedControl!
18
- var statusLabel: NSTextField!
19
- var retryCount = 0
20
-
21
- func applicationDidFinishLaunching(_ notification: Notification) {
22
- startServer()
23
-
24
- let config = WKWebViewConfiguration()
25
- config.preferences.setValue(true, forKey: "developerExtrasEnabled")
26
- config.userContentController.add(self, name: "piecemakerShell")
27
- config.userContentController.addUserScript(WKUserScript(
28
- source: "document.documentElement.dataset.nativeShell = 'macos';",
29
- injectionTime: .atDocumentStart,
30
- forMainFrameOnly: true
31
- ))
32
- webView = WKWebView(frame: .zero, configuration: config)
33
- webView.navigationDelegate = self
34
-
35
- let screen = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800)
36
- let w: CGFloat = min(1280, screen.width * 0.85)
37
- let h: CGFloat = min(900, screen.height * 0.85)
38
- let x = screen.origin.x + (screen.width - w) / 2
39
- let y = screen.origin.y + (screen.height - h) / 2
40
-
41
- window = NSWindow(
42
- contentRect: NSRect(x: x, y: y, width: w, height: h),
43
- styleMask: [.titled, .closable, .miniaturizable, .resizable],
44
- backing: .buffered,
45
- defer: false
46
- )
47
- window.title = "PieceMaker"
48
- window.contentView = webView
49
- installTitlebarControls()
50
- window.makeKeyAndOrderFront(nil)
51
-
52
- loadAdmin()
53
- }
54
-
55
- func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
56
-
57
- private func installTitlebarControls() {
58
- sectionControl = NSSegmentedControl(
59
- labels: kSections.map { $0.label },
60
- trackingMode: .selectOne,
61
- target: self,
62
- action: #selector(selectSection(_:))
63
- )
64
- sectionControl.controlSize = .small
65
- sectionControl.segmentStyle = .texturedRounded
66
- sectionControl.selectedSegment = 0
67
- sectionControl.setAccessibilityLabel("Sections PieceMaker")
68
- sectionControl.sizeToFit()
69
-
70
- let navigationContainer = NSView(frame: NSRect(
71
- x: 0,
72
- y: 0,
73
- width: sectionControl.frame.width + 12,
74
- height: max(28, sectionControl.frame.height)
75
- ))
76
- sectionControl.frame.origin = NSPoint(
77
- x: 6,
78
- y: (navigationContainer.frame.height - sectionControl.frame.height) / 2
79
- )
80
- navigationContainer.addSubview(sectionControl)
81
- let navigationAccessory = NSTitlebarAccessoryViewController()
82
- navigationAccessory.layoutAttribute = .left
83
- navigationAccessory.view = navigationContainer
84
- window.addTitlebarAccessoryViewController(navigationAccessory)
85
-
86
- statusLabel = NSTextField(labelWithString: "Connexion…")
87
- statusLabel.font = .systemFont(ofSize: 11, weight: .semibold)
88
- statusLabel.textColor = .secondaryLabelColor
89
- statusLabel.alignment = .right
90
- statusLabel.sizeToFit()
91
- let statusContainer = NSView(frame: NSRect(x: 0, y: 0, width: 142, height: 28))
92
- statusLabel.frame = NSRect(x: 0, y: 5, width: 134, height: 18)
93
- statusContainer.addSubview(statusLabel)
94
- let statusAccessory = NSTitlebarAccessoryViewController()
95
- statusAccessory.layoutAttribute = .right
96
- statusAccessory.view = statusContainer
97
- window.addTitlebarAccessoryViewController(statusAccessory)
98
- }
99
-
100
- @objc private func selectSection(_ sender: NSSegmentedControl) {
101
- guard sender.selectedSegment >= 0, sender.selectedSegment < kSections.count else { return }
102
- let section = kSections[sender.selectedSegment].id
103
- webView.evaluateJavaScript("window.piecemakerNavigateTo?.('\(section)')")
104
- }
105
-
106
- private func startServer() {
107
- var node = "/usr/local/bin/node"
108
- var cli = NSString(string: "~/PieceMaker/installer/bin/piecemaker.mjs").expandingTildeInPath
109
-
110
- if let bundlePlist = Bundle.main.url(forResource: "environment", withExtension: "plist"),
111
- let dict = NSDictionary(contentsOf: bundlePlist) {
112
- if let n = dict["PIECEMAKER_NODE"] as? String { node = n }
113
- if let c = dict["PIECEMAKER_CLI"] as? String { cli = c }
114
- }
115
- if let n = ProcessInfo.processInfo.environment["PIECEMAKER_NODE"] { node = n }
116
- if let c = ProcessInfo.processInfo.environment["PIECEMAKER_CLI"] { cli = c }
117
-
118
- let process = Process()
119
- process.executableURL = URL(fileURLWithPath: node)
120
- process.arguments = [cli, "start"]
121
- process.standardOutput = FileHandle.nullDevice
122
- process.standardError = FileHandle.nullDevice
123
- try? process.run()
124
- process.waitUntilExit()
125
- }
126
-
127
- private func loadAdmin() {
128
- guard let url = URL(string: kAdminURL) else { return }
129
- webView.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData))
130
- }
131
- }
132
-
133
- extension AppDelegate: WKScriptMessageHandler {
134
- func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
135
- guard message.name == "piecemakerShell",
136
- let payload = message.body as? [String: Any],
137
- let kind = payload["kind"] as? String else { return }
138
-
139
- if kind == "section", let section = payload["value"] as? String,
140
- let index = kSections.firstIndex(where: { $0.id == section }) {
141
- sectionControl.selectedSegment = index
142
- return
143
- }
144
-
145
- if kind == "status", let label = payload["label"] as? String {
146
- statusLabel.stringValue = label
147
- statusLabel.textColor = payload["value"] as? String == "ok" ? .systemGreen : .systemRed
148
- }
149
- }
150
- }
151
-
152
- extension AppDelegate: WKNavigationDelegate {
153
- func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
154
- retryCount += 1
155
- guard retryCount <= kMaxRetries else {
156
- webView.loadHTMLString(
157
- "<html><body style='font-family:system-ui;padding:40px;color:#666'>"
158
- + "<h2>Serveur inaccessible</h2>"
159
- + "<p>Le serveur local PieceMaker n'a pas répondu après \(kMaxRetries) tentatives.</p>"
160
- + "<p>Lancez <code>piecemaker start</code> dans le terminal, puis réessayez.</p>"
161
- + "</body></html>",
162
- baseURL: nil
163
- )
164
- return
165
- }
166
- DispatchQueue.main.asyncAfter(deadline: .now() + kRetryInterval) { [weak self] in
167
- self?.loadAdmin()
168
- }
169
- }
170
-
171
- func webView(
172
- _ webView: WKWebView,
173
- respondTo challenge: URLAuthenticationChallenge
174
- ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
175
- guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
176
- challenge.protectionSpace.host == "localhost",
177
- let trust = challenge.protectionSpace.serverTrust else {
178
- return (.performDefaultHandling, nil)
179
- }
180
- return (.useCredential, URLCredential(trust: trust))
181
- }
182
- }
183
-
184
- let app = NSApplication.shared
185
- let delegate = AppDelegate()
186
- app.delegate = delegate
187
- app.setActivationPolicy(.regular)
188
- app.activate(ignoringOtherApps: true)
189
- app.run()