@piecemaker-legal/piecemaker 2.0.10 → 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.
- package/dist/assets/{FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js → FindReplaceDialog-KU5TEIBJ-BucOHLdm.js} +1 -1
- package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js → FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js} +1 -1
- package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js → HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js} +4 -4
- package/dist/assets/{ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js → ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js} +1 -1
- package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js → ImagePropertiesDialog-RAB47C6M-D86PckZ2.js} +1 -1
- package/dist/assets/{PageSetupDialog-AIGIXO3E-_PYIO8eb.js → PageSetupDialog-AIGIXO3E-DLETP-7v.js} +1 -1
- package/dist/assets/{SplitCellDialog-IFBKYCD6-Cb9uQOB8.js → SplitCellDialog-IFBKYCD6-CwdQBXeb.js} +1 -1
- package/dist/assets/{TablePropertiesDialog-TMLKGOOW-12vmjb6B.js → TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js} +1 -1
- package/dist/assets/{WatermarkDialog-KRL7WMIS-C-d8rzt2.js → WatermarkDialog-KRL7WMIS-XFJ9gTkC.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-B4OsX7eF.js → abnfDiagram-VCTEODGH-DiCrND_Y.js} +1 -1
- package/dist/assets/{arc-BK1oUIk7.js → arc-DOjgbMYz.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-CfaFI6L7.js → architectureDiagram-5GKGNRK7-brzIFVhl.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-DQEI2azY.js → blockDiagram-I7D4REHJ-C8JhTjQQ.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-DbravpVi.js → c4Diagram-7LVT6UL2-jfYxccyk.js} +1 -1
- package/dist/assets/channel-Dajzu2W1.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BLpZ9huC.js → chunk-2Q5K7J3B-CrlEShp9.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BLlEdmX9.js → chunk-5VM5RSS4-DzPlUHGq.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-D-ZNhI6q.js → chunk-F27PBJKO-Cp9cpeWU.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-vzSiKnhh.js → chunk-IMKFNOWR-BifUldEN.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DfF0k4nb.js → chunk-JWPE2WC7-ggPEIkWu.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-BnwCYsPP.js → chunk-POPQ4Y6H-3h_nayRX.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-DRxc8fhq.js → chunk-SVP7TREG-CH29aG6J.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-Dd2Q-md-.js → chunk-TICWLB2K-PluJUM5k.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DBjkEUp_.js → chunk-XXDRQBXY-DXU39Vwg.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-By6PS4oe.js → cose-bilkent-JH36ORCC-Dz6roYrN.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-YxWpjc5W.js → cynefin-OW5HDTMX-BLzrpSit.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-DrtXzz3p.js → cynefinDiagram-5FMLGOSQ-kfxnERDZ.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-BDpTLKqZ.js → dagre-GXQ25YYZ-DP57bp-N.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-C4LvqeCs.js → diagram-S7CK7UJ4-DAlsrQVW.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-BTpnMeHp.js → diagram-UQ7AKVKN-By5yrMer.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-DucHHWev.js → diagram-VSXAHHWV-Dx89kryN.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-DYHZNmmH.js → diagram-VX7I27RA-B9fARitZ.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BHIH2BGN.js → diagram-Z3DM3KII-DC15CEH1.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-CGiEkwj0.js → ebnfDiagram-PWID7BFC-BKQzghGF.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-AJMHi2Us.js → erDiagram-RLTQ6QDP-CPY6RM7G.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-BqzANBh2.js → flowDiagram-HODETNUW-4dL4GJcf.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-B0xPDY2_.js → ganttDiagram-EL5Y4UJY-D4-cVcfb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-CzPX5CNO.js → gitGraphDiagram-WWUBYQGX-BMeVIEiX.js} +1 -1
- package/dist/assets/{index-Bk2iXKnJ.js → index-DNtFDOni.js} +3 -3
- package/dist/assets/{infoDiagram-27XIBGKW-Cjc44H0O.js → infoDiagram-27XIBGKW-D5qMSUEU.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BWhr2v7r.js → ishikawaDiagram-5VMMS53U-BDnyKRHm.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-Bl4r6RrP.js → journeyDiagram-3NMN7TZE-dGz5ZGzq.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-FV-T0Vy1.js → kanban-definition-UXKFOSKX-D7UAz7o_.js} +1 -1
- package/dist/assets/{layout-CTfb1sAF.js → layout-DKrmEGrz.js} +1 -1
- package/dist/assets/{linear-Dfgi6vlo.js → linear-CrTdMY-p.js} +1 -1
- package/dist/assets/{mermaid.core-DOCGsdlo.js → mermaid.core-CXQOT5G6.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-CymQ86ir.js → mindmap-definition-YA3MSWOX-VgEiYfay.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-1VMq_vi0.js → pegDiagram-XKGWAZYB-9IqT8gGx.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-C5GgXtJU.js → pieDiagram-E7YTZNPT-D98pRE-t.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-C6zpryYf.js → quadrantDiagram-AXDQQJYC-b9PwkYzL.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-D-sv4SWi.js → railroadDiagram-O6MQD6OU-Dz87hsPE.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CkjY-z4B.js → requirementDiagram-BXWQKSXE-CkcV73dK.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-De-d8Q6L.js → sankeyDiagram-P5KCCOFB-UBN31Yrs.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-DCsGSJa9.js → sequenceDiagram-WJ2MYXX4-CpQxd5Za.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-xINZhgER.js → sizeCapture-INFHLROL-mCsFmyzw.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-CuLUTWSL.js → stateDiagram-D77RDMKH-B0MTBN-3.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-Cnbs-M92.js → stateDiagram-v2-MP3YSRHH-DzBPN39W.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-BwxARD7N.js → swimlanes-42K2YHIH-Dzydjm0e.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-BIOvnn-l.js → timeline-definition-24CTP7MA-Dz0toOLu.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-BSD6Pfmt.js → vennDiagram-4TSXK5OY-DOWp7dkf.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-7Zz2iRkF.js → wardleyDiagram-VM6X3IG4-CUxsztUB.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-Ctx17Y1D.js → xychartDiagram-S5SC5T6Z-DS96vNED.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-server/server/piecemaker/agent-instructions.js +148 -0
- package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
- package/dist-server/server/piecemaker/index.js +4 -0
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
- package/server/piecemaker/agent-instructions.ts +148 -0
- package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
- package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +7 -4
- package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
- package/server/piecemaker/anonymizer/service.cjs +7 -6
- package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +12 -1
- package/server/piecemaker/index.ts +4 -0
- package/server/piecemaker/router.cjs +0 -1
- package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
- package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +2 -30
- package/server/piecemaker/vendor/websocket-server/case-registry.cjs +0 -10
- package/dist/assets/channel-DVyNr2Yg.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +0 -8
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { constants, watch, type FSWatcher } from 'fs';
|
|
2
|
+
import { copyFile, lstat, readFile, realpath, rename, stat, writeFile } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import express, { type Request, type Response } from 'express';
|
|
6
|
+
|
|
7
|
+
import { projectsDb } from '@/modules/database/index.js';
|
|
8
|
+
|
|
9
|
+
const CLAUDE_FILE = 'CLAUDE.md';
|
|
10
|
+
const AGENTS_FILE = 'AGENTS.md';
|
|
11
|
+
const WATCHED_FILES = new Set([CLAUDE_FILE, AGENTS_FILE]);
|
|
12
|
+
const PROJECT_REFRESH_INTERVAL_MS = 5000;
|
|
13
|
+
const MIRROR_DEBOUNCE_MS = 300;
|
|
14
|
+
|
|
15
|
+
async function isPresent(filePath: string): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
await lstat(filePath);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function agentInstructionsTemplate(sharedInstructionsFile: string): string {
|
|
25
|
+
return [
|
|
26
|
+
`@${sharedInstructionsFile}`,
|
|
27
|
+
'',
|
|
28
|
+
'# Ce dossier',
|
|
29
|
+
'<!-- Éléments propres à ce dossier : parties, juridiction, échéances, particularités. À compléter. -->',
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function ensureAgentInstructionFiles(projectRoot: string, sharedInstructionsFile: string): Promise<string[]> {
|
|
35
|
+
const claudePath = path.join(projectRoot, CLAUDE_FILE);
|
|
36
|
+
const agentsPath = path.join(projectRoot, AGENTS_FILE);
|
|
37
|
+
const [hasClaude, hasAgents] = await Promise.all([isPresent(claudePath), isPresent(agentsPath)]);
|
|
38
|
+
if (hasClaude && hasAgents) return [];
|
|
39
|
+
if (hasClaude) {
|
|
40
|
+
await copyFile(claudePath, agentsPath, constants.COPYFILE_EXCL);
|
|
41
|
+
return [AGENTS_FILE];
|
|
42
|
+
}
|
|
43
|
+
if (hasAgents) {
|
|
44
|
+
await copyFile(agentsPath, claudePath, constants.COPYFILE_EXCL);
|
|
45
|
+
return [CLAUDE_FILE];
|
|
46
|
+
}
|
|
47
|
+
const template = agentInstructionsTemplate(sharedInstructionsFile);
|
|
48
|
+
await writeFile(claudePath, template, { flag: 'wx' });
|
|
49
|
+
await writeFile(agentsPath, template, { flag: 'wx' });
|
|
50
|
+
return [CLAUDE_FILE, AGENTS_FILE];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lastMirroredContents = new Map<string, Buffer>();
|
|
54
|
+
|
|
55
|
+
export async function mirrorAgentInstructionFiles(projectRoot: string): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
const claudePath = path.join(projectRoot, CLAUDE_FILE);
|
|
58
|
+
const agentsPath = path.join(projectRoot, AGENTS_FILE);
|
|
59
|
+
await Promise.all([stat(claudePath), stat(agentsPath)]);
|
|
60
|
+
if ((await realpath(claudePath)) === (await realpath(agentsPath))) return;
|
|
61
|
+
const [claudeContent, agentsContent] = await Promise.all([readFile(claudePath), readFile(agentsPath)]);
|
|
62
|
+
if (claudeContent.equals(agentsContent)) {
|
|
63
|
+
lastMirroredContents.set(projectRoot, claudeContent);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const lastMirrored = lastMirroredContents.get(projectRoot);
|
|
67
|
+
if (!lastMirrored) return;
|
|
68
|
+
const claudeChanged = agentsContent.equals(lastMirrored);
|
|
69
|
+
if (!claudeChanged && !claudeContent.equals(lastMirrored)) return;
|
|
70
|
+
const [content, targetPath] = claudeChanged ? [claudeContent, agentsPath] : [agentsContent, claudePath];
|
|
71
|
+
const temporaryPath = `${targetPath}.piecemaker-${process.pid}.tmp`;
|
|
72
|
+
await writeFile(temporaryPath, content);
|
|
73
|
+
await rename(temporaryPath, targetPath);
|
|
74
|
+
lastMirroredContents.set(projectRoot, content);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function startAgentInstructionsMirror(listProjectRoots: () => string[]): () => void {
|
|
81
|
+
const watchers = new Map<string, FSWatcher>();
|
|
82
|
+
const pendingMirrors = new Map<string, NodeJS.Timeout>();
|
|
83
|
+
|
|
84
|
+
const scheduleMirror = (projectRoot: string) => {
|
|
85
|
+
clearTimeout(pendingMirrors.get(projectRoot));
|
|
86
|
+
pendingMirrors.set(projectRoot, setTimeout(() => {
|
|
87
|
+
pendingMirrors.delete(projectRoot);
|
|
88
|
+
void mirrorAgentInstructionFiles(projectRoot);
|
|
89
|
+
}, MIRROR_DEBOUNCE_MS).unref());
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const forget = (projectRoot: string) => {
|
|
93
|
+
watchers.get(projectRoot)?.close();
|
|
94
|
+
watchers.delete(projectRoot);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const refreshWatchers = () => {
|
|
98
|
+
let projectRoots: Set<string>;
|
|
99
|
+
try {
|
|
100
|
+
projectRoots = new Set(listProjectRoots().map((projectRoot) => path.resolve(projectRoot)));
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const projectRoot of watchers.keys()) {
|
|
105
|
+
if (!projectRoots.has(projectRoot)) forget(projectRoot);
|
|
106
|
+
}
|
|
107
|
+
for (const projectRoot of projectRoots) {
|
|
108
|
+
if (watchers.has(projectRoot)) continue;
|
|
109
|
+
try {
|
|
110
|
+
const watcher = watch(projectRoot, { persistent: false }, (_event, fileName) => {
|
|
111
|
+
if (!fileName || WATCHED_FILES.has(fileName.toString())) scheduleMirror(projectRoot);
|
|
112
|
+
});
|
|
113
|
+
watcher.on('error', () => forget(projectRoot));
|
|
114
|
+
watchers.set(projectRoot, watcher);
|
|
115
|
+
scheduleMirror(projectRoot);
|
|
116
|
+
} catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
refreshWatchers();
|
|
123
|
+
const interval = setInterval(refreshWatchers, PROJECT_REFRESH_INTERVAL_MS).unref();
|
|
124
|
+
return () => {
|
|
125
|
+
clearInterval(interval);
|
|
126
|
+
for (const timer of pendingMirrors.values()) clearTimeout(timer);
|
|
127
|
+
for (const projectRoot of [...watchers.keys()]) forget(projectRoot);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createAgentInstructionsRouter(sharedInstructionsFile: string) {
|
|
132
|
+
const router = express.Router();
|
|
133
|
+
|
|
134
|
+
router.post('/agent-instructions', async (request: Request, response: Response) => {
|
|
135
|
+
const projectId = typeof request.body?.projectId === 'string' ? request.body.projectId : '';
|
|
136
|
+
const projectRoot = projectId ? projectsDb.getProjectPathById(projectId) : null;
|
|
137
|
+
if (!projectRoot) return response.status(404).json({ error: 'Projet introuvable.' });
|
|
138
|
+
try {
|
|
139
|
+
const created = await ensureAgentInstructionFiles(projectRoot, sharedInstructionsFile);
|
|
140
|
+
await mirrorAgentInstructionFiles(projectRoot);
|
|
141
|
+
response.json({ ok: true, created });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
response.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return router;
|
|
148
|
+
}
|
|
@@ -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
|
-
|
|
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. */
|
|
@@ -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 };
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 };
|