@nosserb/prmt 0.0.0-stage → 2.0.0

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 (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -2
  3. package/bin/prmt.mjs +174 -0
  4. package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
  5. package/dist/assets/Audits-yUBm0EpW.js +1 -0
  6. package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
  7. package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
  8. package/dist/assets/Carte-CPuiR1Ha.css +1 -0
  9. package/dist/assets/Carte-xycu1mQn.js +1 -0
  10. package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
  11. package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
  12. package/dist/assets/Field-C7c2mCbb.js +2 -0
  13. package/dist/assets/Journal-BLIIlh80.css +1 -0
  14. package/dist/assets/Journal-BaelFjwV.js +1 -0
  15. package/dist/assets/Landing-B2jxV3zs.css +1 -0
  16. package/dist/assets/Landing-Dr612Zdu.js +26 -0
  17. package/dist/assets/Register-DA_Aq5f8.js +1 -0
  18. package/dist/assets/Reports-8wfDNSxH.js +1 -0
  19. package/dist/assets/Reports-B8GXN95N.css +1 -0
  20. package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
  21. package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
  22. package/dist/assets/SettingsPage-BElIP46c.css +1 -0
  23. package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
  24. package/dist/assets/Shell-BAyHVljp.css +1 -0
  25. package/dist/assets/Shell-CQRlx9-V.js +2 -0
  26. package/dist/assets/Table-DUmGel5e.js +1 -0
  27. package/dist/assets/Tabs-CfhbELZk.js +1 -0
  28. package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
  29. package/dist/assets/Terminal-CEfj38NV.css +1 -0
  30. package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
  31. package/dist/assets/auditViews-BMza2Sdz.js +1 -0
  32. package/dist/assets/auditViews-BePWj26h.css +1 -0
  33. package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
  34. package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
  35. package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
  36. package/dist/assets/charts-BcijSJ4J.js +1 -0
  37. package/dist/assets/circle-stop-C33BAcKW.js +1 -0
  38. package/dist/assets/command-ChA49Xk7.js +1 -0
  39. package/dist/assets/command-Ck4pdQCv.css +1 -0
  40. package/dist/assets/download-CeuKOenD.js +1 -0
  41. package/dist/assets/eye-CXXHBBpr.js +1 -0
  42. package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  43. package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  44. package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  45. package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  46. package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  47. package/dist/assets/habillage-B5mMBadn.js +1 -0
  48. package/dist/assets/index-BH1etemR.css +1 -0
  49. package/dist/assets/index-COSiM-R9.js +14 -0
  50. package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  51. package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  52. package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  53. package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  54. package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  55. package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  56. package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
  57. package/dist/assets/layers-BWBAp2CZ.png +0 -0
  58. package/dist/assets/markdown-DF87LQh1.css +1 -0
  59. package/dist/assets/markdown-o5sMhPHN.js +4 -0
  60. package/dist/assets/marker-icon-hN30_KVU.png +0 -0
  61. package/dist/assets/play-Daw4K_73.js +1 -0
  62. package/dist/assets/plus-D_QHm8ud.js +1 -0
  63. package/dist/assets/trash-DLpHM-s8.js +1 -0
  64. package/dist/assets/web-6j1-aY67.js +1 -0
  65. package/dist/favicon.svg +1 -0
  66. package/dist/index.html +18 -0
  67. package/docs/ANALYSE.md +87 -0
  68. package/docs/SECURITE.md +188 -0
  69. package/lib/scripts/lier.js +15 -0
  70. package/lib/scripts/searxng.js +91 -0
  71. package/lib/server/app.js +444 -0
  72. package/lib/server/audits/modules/local.js +429 -0
  73. package/lib/server/audits/modules/network.js +331 -0
  74. package/lib/server/audits/modules/web.js +302 -0
  75. package/lib/server/audits/net.js +141 -0
  76. package/lib/server/audits/registry.js +202 -0
  77. package/lib/server/audits/types.js +58 -0
  78. package/lib/server/brain/brain.js +624 -0
  79. package/lib/server/brain/conversations.js +82 -0
  80. package/lib/server/brain/ollama.js +197 -0
  81. package/lib/server/brain/prompt.js +188 -0
  82. package/lib/server/brain/provider.js +1 -0
  83. package/lib/server/cli/bridge.js +292 -0
  84. package/lib/server/cli/pont.js +62 -0
  85. package/lib/server/config.js +68 -0
  86. package/lib/server/engine/analyzer.js +205 -0
  87. package/lib/server/engine/catalog.js +229 -0
  88. package/lib/server/engine/decoder.js +91 -0
  89. package/lib/server/engine/engine.js +194 -0
  90. package/lib/server/engine/paths.js +129 -0
  91. package/lib/server/engine/policy.js +397 -0
  92. package/lib/server/engine/relax.js +10 -0
  93. package/lib/server/engine/runner.js +193 -0
  94. package/lib/server/errors.js +23 -0
  95. package/lib/server/http/guard.js +75 -0
  96. package/lib/server/http/router.js +154 -0
  97. package/lib/server/http/static.js +76 -0
  98. package/lib/server/index.js +40 -0
  99. package/lib/server/journal.js +139 -0
  100. package/lib/server/lien.js +33 -0
  101. package/lib/server/savoir/base.js +284 -0
  102. package/lib/server/savoir/lecture.js +406 -0
  103. package/lib/server/savoir/memoire.js +115 -0
  104. package/lib/server/search/categories.js +179 -0
  105. package/lib/server/search/fiche.js +326 -0
  106. package/lib/server/search/lieux.js +134 -0
  107. package/lib/server/search/overpass.js +216 -0
  108. package/lib/server/search/searxng.js +223 -0
  109. package/lib/server/search/tiles.js +103 -0
  110. package/lib/server/status.js +157 -0
  111. package/lib/server/stores.js +141 -0
  112. package/lib/server/telephone/acces.js +457 -0
  113. package/lib/server/telephone/certificat.js +79 -0
  114. package/lib/server/telephone/qr.js +308 -0
  115. package/lib/shared/analysis.js +40 -0
  116. package/lib/shared/schemas.js +105 -0
  117. package/lib/shared/types.js +10 -0
  118. package/package.json +81 -4
  119. package/searxng/settings.yml +35 -0
  120. package/tui/Modelfile +23 -0
  121. package/tui/README.md +103 -0
  122. package/tui/prmt_tui.py +65 -0
  123. package/tui/promethee/__init__.py +1 -0
  124. package/tui/promethee/__main__.py +4 -0
  125. package/tui/promethee/accueil.py +66 -0
  126. package/tui/promethee/agents.py +220 -0
  127. package/tui/promethee/app/__init__.py +4 -0
  128. package/tui/promethee/app/application.py +299 -0
  129. package/tui/promethee/app/clavier.py +99 -0
  130. package/tui/promethee/app/commandes_slash.py +164 -0
  131. package/tui/promethee/app/contexte.py +188 -0
  132. package/tui/promethee/app/definitions.py +518 -0
  133. package/tui/promethee/app/ecran.py +177 -0
  134. package/tui/promethee/app/envoi.py +141 -0
  135. package/tui/promethee/app/historique.py +373 -0
  136. package/tui/promethee/app/modele.py +108 -0
  137. package/tui/promethee/app/notes.py +63 -0
  138. package/tui/promethee/app/outils.py +589 -0
  139. package/tui/promethee/app/plateforme.py +409 -0
  140. package/tui/promethee/app/saisie.py +100 -0
  141. package/tui/promethee/app/taches.py +92 -0
  142. package/tui/promethee/app/tour.py +539 -0
  143. package/tui/promethee/app/validation.py +210 -0
  144. package/tui/promethee/appels.py +35 -0
  145. package/tui/promethee/commandes.py +154 -0
  146. package/tui/promethee/completion.py +140 -0
  147. package/tui/promethee/config.py +12 -0
  148. package/tui/promethee/consignes.py +278 -0
  149. package/tui/promethee/conversation.py +94 -0
  150. package/tui/promethee/etat.py +253 -0
  151. package/tui/promethee/fichiers.py +354 -0
  152. package/tui/promethee/journal.py +116 -0
  153. package/tui/promethee/logo.py +132 -0
  154. package/tui/promethee/memoire.py +50 -0
  155. package/tui/promethee/ollama.py +52 -0
  156. package/tui/promethee/plan.py +91 -0
  157. package/tui/promethee/politique.py +429 -0
  158. package/tui/promethee/preferences.py +65 -0
  159. package/tui/promethee/presse_papiers.py +44 -0
  160. package/tui/promethee/recherche.py +581 -0
  161. package/tui/promethee/securite.py +204 -0
  162. package/tui/promethee/shell.py +190 -0
  163. package/tui/promethee/textes.py +190 -0
  164. package/tui/promethee/theme.py +92 -0
  165. package/tui/promethee/widgets.py +493 -0
  166. package/tui/requirements.txt +1 -0
@@ -0,0 +1,157 @@
1
+ /** État de la plateforme pour le tableau de bord. Tout vient de mesures réelles : journal, audits, Ollama, moteur. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { countBySeverity, MODULES } from './audits/registry.js';
5
+ import { VERSION } from './config.js';
6
+ import { APPS, CMDLETS } from './engine/catalog.js';
7
+ import { RENDEZVOUS } from './lien.js';
8
+ const DAYS = 14;
9
+ const dayKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
10
+ export function catalogItems(settings) {
11
+ const unlocked = new Set(settings.allowlist.unlocked.map((u) => u.toLowerCase()));
12
+ const items = [];
13
+ for (const [name, rule] of CMDLETS)
14
+ items.push({ name, level: rule.level, category: rule.category, note: rule.note ?? '', ...(rule.level === 'destructive' ? { unlocked: unlocked.has(name) } : {}) });
15
+ for (const [name, rule] of APPS)
16
+ items.push({ name: `${name}.exe`, level: rule.level, category: rule.category, note: rule.note ?? '', ...(rule.level === 'destructive' || rule.unlockable ? { unlocked: unlocked.has(name) } : {}) });
17
+ for (const c of settings.allowlist.custom)
18
+ items.push({ name: c.name, level: c.level, category: 'liste personnelle', note: 'ajoutée par toi, arguments non contrôlés', custom: true });
19
+ return items.sort((a, b) => a.name.localeCompare(b.name));
20
+ }
21
+ export function dayStats(entries, now) {
22
+ const days = new Map();
23
+ for (let i = DAYS - 1; i >= 0; i--) {
24
+ const d = new Date(now);
25
+ d.setDate(d.getDate() - i);
26
+ const date = dayKey(d);
27
+ days.set(date, { date, executed: 0, failed: 0, refused: 0 });
28
+ }
29
+ for (const e of entries) {
30
+ if (e.kind !== 'command' && e.kind !== 'refusal')
31
+ continue;
32
+ const stat = days.get(dayKey(new Date(e.at)));
33
+ if (!stat)
34
+ continue;
35
+ if (e.kind === 'refusal')
36
+ stat.refused++;
37
+ else if (e.outcome === 'ok')
38
+ stat.executed++;
39
+ // Une commande annulée (par toi, ici ou dans la TUI) n'est pas un échec
40
+ else if (e.outcome !== 'cancelled')
41
+ stat.failed++;
42
+ }
43
+ return [...days.values()];
44
+ }
45
+ export class StatusService {
46
+ health = null;
47
+ searchHealthCache = null;
48
+ deps;
49
+ constructor(deps) {
50
+ this.deps = deps;
51
+ }
52
+ /** L'état d'Ollama est gardé trois secondes pour ne pas l'interroger à chaque rafraîchissement. */
53
+ async providerHealth() {
54
+ if (this.health && Date.now() - this.health.at < 3000)
55
+ return this.health.value;
56
+ const value = await this.deps.provider().health();
57
+ this.health = { at: Date.now(), value };
58
+ return value;
59
+ }
60
+ /** SearXNG n'est interrogé que si la recherche est active, et pas plus d'une fois toutes les trois secondes. */
61
+ async searchHealth(settings) {
62
+ const { enabled, url } = settings.search;
63
+ if (!enabled)
64
+ return { enabled, url, available: false, latencyMs: null, error: null };
65
+ const cache = this.searchHealthCache;
66
+ if (cache && cache.url === url && Date.now() - cache.at < 3000)
67
+ return { enabled, url, ...cache.value };
68
+ const value = await this.deps.search().health();
69
+ this.searchHealthCache = { at: Date.now(), url, value };
70
+ return { enabled, url, ...value };
71
+ }
72
+ alerts(entries) {
73
+ const alerts = [];
74
+ const { probe, audits, guardRejections } = this.deps;
75
+ const now = new Date().toISOString();
76
+ if (!probe.available)
77
+ alerts.push({ id: 'engine', at: now, severity: 'high', title: 'Moteur PowerShell indisponible', detail: probe.error ?? 'PowerShell ne répond pas.', source: 'system' });
78
+ const guard = guardRejections();
79
+ if (guard.count > 0 && guard.last)
80
+ alerts.push({ id: 'guard', at: guard.last, severity: 'medium', title: `${guard.count} requête${guard.count > 1 ? 's' : ''} refusée${guard.count > 1 ? 's' : ''} par la garde de l’API`, detail: `Dernier motif : ${guard.reason ?? 'inconnu'}. Une autre page ou un autre programme a tenté d’appeler le serveur local.`, source: 'guard' });
81
+ for (const run of audits.latest()) {
82
+ for (const f of run.findings) {
83
+ if (f.severity !== 'critical' && f.severity !== 'high')
84
+ continue;
85
+ alerts.push({ id: `${run.id}-${f.id}`, at: run.startedAt, severity: f.severity, title: f.title, detail: `${run.target} · ${f.detail}`, source: 'audit' });
86
+ }
87
+ }
88
+ const weekAgo = Date.now() - 7 * 86_400_000;
89
+ for (let i = entries.length - 1, kept = 0; i >= 0 && kept < 5; i--) {
90
+ const e = entries[i];
91
+ if (!e || e.kind !== 'refusal' || new Date(e.at).getTime() < weekAgo)
92
+ continue;
93
+ const codes = Array.isArray(e.data?.codes) ? e.data.codes.map(String) : [];
94
+ if (!codes.some((c) => ['forbidden', 'destructive', 'method-call', 'static-member', 'dynamic-command', 'target-out-of-scope', 'out-of-scope', 'unc-path', 'env-assign', 'drive-variable'].includes(c)))
95
+ continue;
96
+ kept++;
97
+ alerts.push({ id: e.id, at: e.at, severity: 'low', title: 'Commande refusée par la politique', detail: `${e.title}${e.data?.origin === 'ai' ? ' (proposée par le modèle)' : e.data?.origin === 'tui' ? ' (dans la TUI prmt)' : ''}`, source: 'policy' });
98
+ }
99
+ const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
100
+ return alerts.sort((a, b) => order[a.severity] - order[b.severity] || b.at.localeCompare(a.at)).slice(0, 12);
101
+ }
102
+ async snapshot() {
103
+ const { config, journal, audits, brain, engine, probe, settings, targets, startedAt } = this.deps;
104
+ const s = settings();
105
+ const [health, search] = await Promise.all([this.providerHealth(), this.searchHealth(s)]);
106
+ let models = { installed: [], loaded: [], selected: null };
107
+ if (health.available) {
108
+ try {
109
+ models = await brain.models();
110
+ }
111
+ catch {
112
+ /* Ollama a répondu à /api/version mais pas à la suite : on garde les listes vides */
113
+ }
114
+ }
115
+ const entries = journal.all();
116
+ const days = dayStats(entries, new Date());
117
+ const levels = { read: 0, sensitive: 0, destructive: 0, forbidden: 0 };
118
+ for (const rule of [...CMDLETS.values(), ...APPS.values()])
119
+ levels[rule.level]++;
120
+ const findings = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
121
+ const latest = audits.latest();
122
+ for (const run of latest) {
123
+ const counts = countBySeverity(run.findings);
124
+ for (const key of Object.keys(findings))
125
+ findings[key] += counts[key];
126
+ }
127
+ const history = audits.history();
128
+ const ai = entries.filter((e) => e.kind === 'ai' && e.outcome === 'ok');
129
+ // Activité de la TUI : les entrées de son journal (commandes exécutées, refusées, annulées)
130
+ const fromTui = entries.filter((e) => e.data?.origin === 'tui' && (e.kind === 'command' || e.kind === 'refusal'));
131
+ const speeds = ai.map((e) => Number(e.data?.tokensPerSec ?? 0)).filter((v) => v > 0).slice(-20);
132
+ return {
133
+ server: { version: VERSION, startedAt, listen: `${config.host}:${config.port}`, dataDir: config.dataDir, phone: this.deps.phone() },
134
+ tui: { linked: fs.existsSync(path.join(config.tuiDir, RENDEZVOUS)), lastAt: fromTui.at(-1)?.at ?? null, actions: fromTui.length },
135
+ ollama: { available: health.available, version: health.version, latencyMs: health.latencyMs, error: health.error, baseUrl: s.ollama.baseUrl },
136
+ models,
137
+ search,
138
+ fullCyber: s.fullCyber,
139
+ autoRun: s.autoRun,
140
+ engine: { available: probe.available, version: probe.version, codePage: probe.codePage, constrained: s.engine.constrainedLanguage, running: engine.running, timeoutSec: s.engine.timeoutSec, error: probe.error },
141
+ policy: { ...levels, custom: s.allowlist.custom.length, targets: targets().length },
142
+ stats: {
143
+ days,
144
+ executed: days.reduce((n, d) => n + d.executed, 0),
145
+ refused: days.reduce((n, d) => n + d.refused, 0),
146
+ failed: days.reduce((n, d) => n + d.failed, 0),
147
+ audits: history.length,
148
+ aiReplies: ai.length,
149
+ speeds,
150
+ findings,
151
+ },
152
+ recent: journal.query({ limit: 8 }),
153
+ alerts: this.alerts(entries),
154
+ audits: MODULES.map((m) => ({ module: m.info.id, title: m.info.title, last: history.find((h) => h.module === m.info.id) ?? null })),
155
+ };
156
+ }
157
+ }
@@ -0,0 +1,141 @@
1
+ /** Données persistantes : réglages, périmètre autorisé. Fichiers JSON écrits de façon atomique. */
2
+ import { randomBytes } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { badRequest, notFound } from './errors.js';
6
+ import { isLoopback } from './engine/paths.js';
7
+ export function readJson(file, fallback) {
8
+ try {
9
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
10
+ }
11
+ catch {
12
+ return fallback;
13
+ }
14
+ }
15
+ /** Écrit dans un fichier voisin puis renomme : un arrêt brutal ne laisse pas de fichier à moitié écrit. */
16
+ export function writeJson(file, value) {
17
+ fs.mkdirSync(path.dirname(file), { recursive: true });
18
+ const tmp = `${file}.${randomBytes(4).toString('hex')}.tmp`;
19
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf8');
20
+ fs.renameSync(tmp, file);
21
+ }
22
+ /** Date de modification d'un fichier, ou 0 s'il n'existe pas. */
23
+ const stamp = (file) => (file ? (fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0) : 0);
24
+ export class SettingsStore {
25
+ current;
26
+ file;
27
+ defaults;
28
+ seen = 0;
29
+ constructor(file, defaults) {
30
+ this.file = file;
31
+ this.defaults = defaults;
32
+ this.current = this.read();
33
+ }
34
+ read() {
35
+ const saved = this.file ? readJson(this.file, {}) : {};
36
+ this.seen = stamp(this.file);
37
+ return {
38
+ model: saved.model ?? this.defaults.model,
39
+ engine: { ...this.defaults.engine, ...saved.engine },
40
+ allowlist: { ...this.defaults.allowlist, ...saved.allowlist },
41
+ ollama: { ...this.defaults.ollama, ...saved.ollama },
42
+ search: { ...this.defaults.search, ...saved.search },
43
+ fullCyber: saved.fullCyber ?? this.defaults.fullCyber,
44
+ autoRun: saved.autoRun ?? this.defaults.autoRun,
45
+ };
46
+ }
47
+ /** La console et la TUI partagent ce fichier : il est relu dès que l'autre l'a modifié. */
48
+ get() {
49
+ if (this.file && stamp(this.file) !== this.seen)
50
+ this.current = this.read();
51
+ return this.current;
52
+ }
53
+ patch(patch) {
54
+ const base = this.get();
55
+ const next = {
56
+ model: patch.model === undefined ? base.model : patch.model,
57
+ engine: { ...base.engine, ...patch.engine },
58
+ allowlist: { ...base.allowlist, ...patch.allowlist },
59
+ ollama: { ...base.ollama, ...patch.ollama },
60
+ search: { ...base.search, ...patch.search },
61
+ fullCyber: patch.fullCyber ?? base.fullCyber,
62
+ autoRun: patch.autoRun ?? base.autoRun,
63
+ };
64
+ if (patch.engine?.roots) {
65
+ next.engine.roots = patch.engine.roots.map((root) => {
66
+ const resolved = path.win32.resolve(root);
67
+ if (!path.win32.isAbsolute(root) || /^[\\/]{2}/.test(root))
68
+ throw badRequest(`« ${root} » doit être un chemin local absolu.`);
69
+ if (path.win32.parse(resolved).root === resolved)
70
+ throw badRequest('Un lecteur entier ne peut pas être autorisé en écriture.');
71
+ let stat;
72
+ try {
73
+ stat = fs.statSync(resolved);
74
+ }
75
+ catch {
76
+ throw badRequest(`Le dossier « ${resolved} » n’existe pas.`);
77
+ }
78
+ if (!stat.isDirectory())
79
+ throw badRequest(`« ${resolved} » n’est pas un dossier.`);
80
+ return resolved;
81
+ });
82
+ }
83
+ this.current = next;
84
+ if (this.file) {
85
+ writeJson(this.file, next);
86
+ this.seen = stamp(this.file);
87
+ }
88
+ return next;
89
+ }
90
+ }
91
+ export class ScopeStore {
92
+ targets = [];
93
+ file;
94
+ seen = 0;
95
+ constructor(file) {
96
+ this.file = file;
97
+ this.reload();
98
+ }
99
+ reload() {
100
+ this.targets = this.file ? readJson(this.file, []) : this.targets;
101
+ this.seen = stamp(this.file);
102
+ }
103
+ list() {
104
+ if (this.file && stamp(this.file) !== this.seen)
105
+ this.reload();
106
+ return this.targets;
107
+ }
108
+ add(input) {
109
+ this.list();
110
+ if (isLoopback(input.host))
111
+ throw badRequest('La machine locale est toujours autorisée, inutile de la déclarer.');
112
+ if (this.targets.some((t) => t.host === input.host))
113
+ throw badRequest(`« ${input.host} » est déjà dans le périmètre.`);
114
+ if (this.targets.length >= 100)
115
+ throw badRequest('Le périmètre est limité à 100 cibles.');
116
+ const target = {
117
+ id: randomBytes(6).toString('hex'),
118
+ host: input.host,
119
+ note: input.note,
120
+ includeSubdomains: input.includeSubdomains,
121
+ attestedAt: new Date().toISOString(),
122
+ };
123
+ this.targets = [...this.targets, target];
124
+ this.save();
125
+ return target;
126
+ }
127
+ remove(id) {
128
+ const target = this.list().find((t) => t.id === id);
129
+ if (!target)
130
+ throw notFound('Cible introuvable dans le périmètre.');
131
+ this.targets = this.targets.filter((t) => t.id !== id);
132
+ this.save();
133
+ return target;
134
+ }
135
+ save() {
136
+ if (!this.file)
137
+ return;
138
+ writeJson(this.file, this.targets);
139
+ this.seen = stamp(this.file);
140
+ }
141
+ }