@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.
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/bin/prmt.mjs +174 -0
- package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
- package/dist/assets/Audits-yUBm0EpW.js +1 -0
- package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
- package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
- package/dist/assets/Carte-CPuiR1Ha.css +1 -0
- package/dist/assets/Carte-xycu1mQn.js +1 -0
- package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
- package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
- package/dist/assets/Field-C7c2mCbb.js +2 -0
- package/dist/assets/Journal-BLIIlh80.css +1 -0
- package/dist/assets/Journal-BaelFjwV.js +1 -0
- package/dist/assets/Landing-B2jxV3zs.css +1 -0
- package/dist/assets/Landing-Dr612Zdu.js +26 -0
- package/dist/assets/Register-DA_Aq5f8.js +1 -0
- package/dist/assets/Reports-8wfDNSxH.js +1 -0
- package/dist/assets/Reports-B8GXN95N.css +1 -0
- package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
- package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
- package/dist/assets/SettingsPage-BElIP46c.css +1 -0
- package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
- package/dist/assets/Shell-BAyHVljp.css +1 -0
- package/dist/assets/Shell-CQRlx9-V.js +2 -0
- package/dist/assets/Table-DUmGel5e.js +1 -0
- package/dist/assets/Tabs-CfhbELZk.js +1 -0
- package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
- package/dist/assets/Terminal-CEfj38NV.css +1 -0
- package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
- package/dist/assets/auditViews-BMza2Sdz.js +1 -0
- package/dist/assets/auditViews-BePWj26h.css +1 -0
- package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/assets/charts-BcijSJ4J.js +1 -0
- package/dist/assets/circle-stop-C33BAcKW.js +1 -0
- package/dist/assets/command-ChA49Xk7.js +1 -0
- package/dist/assets/command-Ck4pdQCv.css +1 -0
- package/dist/assets/download-CeuKOenD.js +1 -0
- package/dist/assets/eye-CXXHBBpr.js +1 -0
- package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/assets/habillage-B5mMBadn.js +1 -0
- package/dist/assets/index-BH1etemR.css +1 -0
- package/dist/assets/index-COSiM-R9.js +14 -0
- package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
- package/dist/assets/layers-BWBAp2CZ.png +0 -0
- package/dist/assets/markdown-DF87LQh1.css +1 -0
- package/dist/assets/markdown-o5sMhPHN.js +4 -0
- package/dist/assets/marker-icon-hN30_KVU.png +0 -0
- package/dist/assets/play-Daw4K_73.js +1 -0
- package/dist/assets/plus-D_QHm8ud.js +1 -0
- package/dist/assets/trash-DLpHM-s8.js +1 -0
- package/dist/assets/web-6j1-aY67.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +18 -0
- package/docs/ANALYSE.md +87 -0
- package/docs/SECURITE.md +188 -0
- package/lib/scripts/lier.js +15 -0
- package/lib/scripts/searxng.js +91 -0
- package/lib/server/app.js +444 -0
- package/lib/server/audits/modules/local.js +429 -0
- package/lib/server/audits/modules/network.js +331 -0
- package/lib/server/audits/modules/web.js +302 -0
- package/lib/server/audits/net.js +141 -0
- package/lib/server/audits/registry.js +202 -0
- package/lib/server/audits/types.js +58 -0
- package/lib/server/brain/brain.js +624 -0
- package/lib/server/brain/conversations.js +82 -0
- package/lib/server/brain/ollama.js +197 -0
- package/lib/server/brain/prompt.js +188 -0
- package/lib/server/brain/provider.js +1 -0
- package/lib/server/cli/bridge.js +292 -0
- package/lib/server/cli/pont.js +62 -0
- package/lib/server/config.js +68 -0
- package/lib/server/engine/analyzer.js +205 -0
- package/lib/server/engine/catalog.js +229 -0
- package/lib/server/engine/decoder.js +91 -0
- package/lib/server/engine/engine.js +194 -0
- package/lib/server/engine/paths.js +129 -0
- package/lib/server/engine/policy.js +397 -0
- package/lib/server/engine/relax.js +10 -0
- package/lib/server/engine/runner.js +193 -0
- package/lib/server/errors.js +23 -0
- package/lib/server/http/guard.js +75 -0
- package/lib/server/http/router.js +154 -0
- package/lib/server/http/static.js +76 -0
- package/lib/server/index.js +40 -0
- package/lib/server/journal.js +139 -0
- package/lib/server/lien.js +33 -0
- package/lib/server/savoir/base.js +284 -0
- package/lib/server/savoir/lecture.js +406 -0
- package/lib/server/savoir/memoire.js +115 -0
- package/lib/server/search/categories.js +179 -0
- package/lib/server/search/fiche.js +326 -0
- package/lib/server/search/lieux.js +134 -0
- package/lib/server/search/overpass.js +216 -0
- package/lib/server/search/searxng.js +223 -0
- package/lib/server/search/tiles.js +103 -0
- package/lib/server/status.js +157 -0
- package/lib/server/stores.js +141 -0
- package/lib/server/telephone/acces.js +457 -0
- package/lib/server/telephone/certificat.js +79 -0
- package/lib/server/telephone/qr.js +308 -0
- package/lib/shared/analysis.js +40 -0
- package/lib/shared/schemas.js +105 -0
- package/lib/shared/types.js +10 -0
- package/package.json +81 -4
- package/searxng/settings.yml +35 -0
- package/tui/Modelfile +23 -0
- package/tui/README.md +103 -0
- package/tui/prmt_tui.py +65 -0
- package/tui/promethee/__init__.py +1 -0
- package/tui/promethee/__main__.py +4 -0
- package/tui/promethee/accueil.py +66 -0
- package/tui/promethee/agents.py +220 -0
- package/tui/promethee/app/__init__.py +4 -0
- package/tui/promethee/app/application.py +299 -0
- package/tui/promethee/app/clavier.py +99 -0
- package/tui/promethee/app/commandes_slash.py +164 -0
- package/tui/promethee/app/contexte.py +188 -0
- package/tui/promethee/app/definitions.py +518 -0
- package/tui/promethee/app/ecran.py +177 -0
- package/tui/promethee/app/envoi.py +141 -0
- package/tui/promethee/app/historique.py +373 -0
- package/tui/promethee/app/modele.py +108 -0
- package/tui/promethee/app/notes.py +63 -0
- package/tui/promethee/app/outils.py +589 -0
- package/tui/promethee/app/plateforme.py +409 -0
- package/tui/promethee/app/saisie.py +100 -0
- package/tui/promethee/app/taches.py +92 -0
- package/tui/promethee/app/tour.py +539 -0
- package/tui/promethee/app/validation.py +210 -0
- package/tui/promethee/appels.py +35 -0
- package/tui/promethee/commandes.py +154 -0
- package/tui/promethee/completion.py +140 -0
- package/tui/promethee/config.py +12 -0
- package/tui/promethee/consignes.py +278 -0
- package/tui/promethee/conversation.py +94 -0
- package/tui/promethee/etat.py +253 -0
- package/tui/promethee/fichiers.py +354 -0
- package/tui/promethee/journal.py +116 -0
- package/tui/promethee/logo.py +132 -0
- package/tui/promethee/memoire.py +50 -0
- package/tui/promethee/ollama.py +52 -0
- package/tui/promethee/plan.py +91 -0
- package/tui/promethee/politique.py +429 -0
- package/tui/promethee/preferences.py +65 -0
- package/tui/promethee/presse_papiers.py +44 -0
- package/tui/promethee/recherche.py +581 -0
- package/tui/promethee/securite.py +204 -0
- package/tui/promethee/shell.py +190 -0
- package/tui/promethee/textes.py +190 -0
- package/tui/promethee/theme.py +92 -0
- package/tui/promethee/widgets.py +493 -0
- package/tui/requirements.txt +1 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/** Assemblage du serveur local : services, garde, routes de l'API, fichiers de l'interface. */
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { auditInput, cancelInput, cwdInput, decisionInput, newConversationInput, pairInput, phoneToggleInput, previewInput, runInput, scopeInput, sendInput, settingsPatch } from '../shared/schemas.js';
|
|
8
|
+
import { PC_ONLY_SETTINGS } from '../shared/types.js';
|
|
9
|
+
import { Audits } from './audits/registry.js';
|
|
10
|
+
import { Brain } from './brain/brain.js';
|
|
11
|
+
import { ConversationStore, listTuiSessions } from './brain/conversations.js';
|
|
12
|
+
import { OllamaProvider } from './brain/ollama.js';
|
|
13
|
+
import { VERSION, defaultSettings } from './config.js';
|
|
14
|
+
import { PowerShellAnalyzer } from './engine/analyzer.js';
|
|
15
|
+
import { Engine } from './engine/engine.js';
|
|
16
|
+
import { PowerShellRunner, engineEnv, probePowerShell } from './engine/runner.js';
|
|
17
|
+
import { AppError, forbidden, notFound } from './errors.js';
|
|
18
|
+
import { PAIR_PATH, checkRemoteRequest, checkRequest } from './http/guard.js';
|
|
19
|
+
import { Router, pipeEvents, sendJson } from './http/router.js';
|
|
20
|
+
import { SECURITY_HEADERS, serveStatic } from './http/static.js';
|
|
21
|
+
import { Journal } from './journal.js';
|
|
22
|
+
import { Fiches, parseCardRequest } from './search/fiche.js';
|
|
23
|
+
import { PlacesService, parsePlacesRequest } from './search/lieux.js';
|
|
24
|
+
import { Overpass } from './search/overpass.js';
|
|
25
|
+
import { Searxng } from './search/searxng.js';
|
|
26
|
+
import { TileCache, parseTile } from './search/tiles.js';
|
|
27
|
+
import { TUI_JOURNAL } from './lien.js';
|
|
28
|
+
import { StatusService, catalogItems } from './status.js';
|
|
29
|
+
import { ScopeStore, SettingsStore } from './stores.js';
|
|
30
|
+
import { Savoir } from './savoir/base.js';
|
|
31
|
+
import { ReadError, fetchPage, readPage } from './savoir/lecture.js';
|
|
32
|
+
import { PhoneAccess, cookieValue, setCookie } from './telephone/acces.js';
|
|
33
|
+
const JOURNAL_KINDS = ['command', 'refusal', 'audit', 'ai', 'search', 'scope', 'settings', 'security', 'system'];
|
|
34
|
+
const JOURNAL_OUTCOMES = ['ok', 'error', 'refused', 'cancelled', 'info'];
|
|
35
|
+
const pick = (value, allowed) => allowed.includes(value ?? '') ? value : undefined;
|
|
36
|
+
export async function createApp(options) {
|
|
37
|
+
const { config } = options;
|
|
38
|
+
const persist = options.persist !== false;
|
|
39
|
+
const data = (name) => (persist ? path.join(config.dataDir, name) : null);
|
|
40
|
+
if (persist)
|
|
41
|
+
fs.mkdirSync(config.dataDir, { recursive: true });
|
|
42
|
+
const env = engineEnv(process.env);
|
|
43
|
+
/**
|
|
44
|
+
* Requête arrivée par l'écoute du téléphone : son appareil (null pendant l'appairage). Suit la requête à travers les appels
|
|
45
|
+
* asynchrones, jusqu'aux entrées du journal qu'elle provoque, marquées « via : telephone ».
|
|
46
|
+
*/
|
|
47
|
+
const fromPhone = new AsyncLocalStorage();
|
|
48
|
+
const phoneDevice = () => fromPhone.getStore()?.device ?? null;
|
|
49
|
+
// Le journal de la TUI est lu, jamais écrit : le tableau de bord montre l'activité des deux interfaces.
|
|
50
|
+
const journal = new Journal(data('journal.jsonl'), {
|
|
51
|
+
extra: persist ? [path.join(config.tuiDir, TUI_JOURNAL)] : [],
|
|
52
|
+
tag: () => {
|
|
53
|
+
const store = fromPhone.getStore();
|
|
54
|
+
return store ? { via: 'telephone', ...(store.device ? { appareil: store.device.name } : {}) } : null;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const settings = new SettingsStore(data('reglages.json'), defaultSettings(config));
|
|
58
|
+
const scope = new ScopeStore(data('perimetre.json'));
|
|
59
|
+
const probe = options.probe ?? (await probePowerShell(env));
|
|
60
|
+
const analyzer = options.analyzer ?? new PowerShellAnalyzer({ env, cwd: persist ? config.dataDir : config.home });
|
|
61
|
+
const runner = options.runner ?? new PowerShellRunner({ env, oemTable: probe.oemTable });
|
|
62
|
+
const engine = new Engine({ analyzer, runner, journal, settings: () => settings.get(), targets: () => scope.list(), probe, home: config.home, systemRoot: config.systemRoot });
|
|
63
|
+
let cached = null;
|
|
64
|
+
const provider = () => {
|
|
65
|
+
if (options.provider)
|
|
66
|
+
return options.provider;
|
|
67
|
+
const url = settings.get().ollama.baseUrl;
|
|
68
|
+
if (!cached || cached.url !== url)
|
|
69
|
+
cached = { url, provider: new OllamaProvider(url) };
|
|
70
|
+
return cached.provider;
|
|
71
|
+
};
|
|
72
|
+
let searxng = null;
|
|
73
|
+
// Sans persistance (tests) et sans remplaçant, aucune recherche ne sort : même le SearXNG local n'est pas appelé
|
|
74
|
+
const offlineSearch = {
|
|
75
|
+
web: () => Promise.reject(new Error('recherche coupée pendant les tests')),
|
|
76
|
+
place: () => Promise.reject(new Error('recherche coupée pendant les tests')),
|
|
77
|
+
images: () => Promise.reject(new Error('recherche coupée pendant les tests')),
|
|
78
|
+
health: () => Promise.resolve({ available: false, latencyMs: null, error: 'Recherche coupée pendant les tests.' }),
|
|
79
|
+
};
|
|
80
|
+
const search = () => {
|
|
81
|
+
if (options.search)
|
|
82
|
+
return options.search;
|
|
83
|
+
if (!persist)
|
|
84
|
+
return offlineSearch;
|
|
85
|
+
const url = settings.get().search.url;
|
|
86
|
+
if (!searxng || searxng.url !== url)
|
|
87
|
+
searxng = { url, backend: new Searxng(() => url) };
|
|
88
|
+
return searxng.backend;
|
|
89
|
+
};
|
|
90
|
+
const offline = () => Promise.reject(new Error('réseau coupé pendant les tests'));
|
|
91
|
+
const tiles = new TileCache(data('tuiles'), options.tileFetch ?? (persist ? fetch : offline));
|
|
92
|
+
const placesBackend = options.places ?? new Overpass(persist ? fetch : offline);
|
|
93
|
+
const places = new PlacesService(() => placesBackend, data('lieux'));
|
|
94
|
+
const fiches = new Fiches({
|
|
95
|
+
search,
|
|
96
|
+
places: () => placesBackend,
|
|
97
|
+
fetcher: options.cardFetch ?? (persist ? fetch : offline),
|
|
98
|
+
onLookup: (request, card) => journal.add({
|
|
99
|
+
kind: 'search',
|
|
100
|
+
outcome: card.note && !card.description && !card.image ? 'error' : 'ok',
|
|
101
|
+
title: `Fiche du lieu : ${request.name.slice(0, 100)}`,
|
|
102
|
+
detail: card.description ? `description : ${card.descriptionSource?.label ?? '?'}${card.image ? ', image' : ''}` : (card.note ?? 'rien trouvé'),
|
|
103
|
+
data: { query: `${request.name} ${request.zone}`.trim(), osm: request.osm, origin: 'console' },
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
const conversations = new ConversationStore(data('conversations'));
|
|
107
|
+
// Le savoir local : sans persistance, une base en mémoire ; les pages ne sortent sur le réseau qu'avec un vrai serveur
|
|
108
|
+
const savoir = new Savoir(data('savoir.db'));
|
|
109
|
+
const pageFetch = options.pageFetch ?? (persist ? fetchPage : () => Promise.reject(new ReadError('Lecture coupée pendant les tests.')));
|
|
110
|
+
const brain = new Brain({
|
|
111
|
+
provider,
|
|
112
|
+
engine,
|
|
113
|
+
journal,
|
|
114
|
+
settings: () => settings.get(),
|
|
115
|
+
store: conversations,
|
|
116
|
+
search,
|
|
117
|
+
prefetchPlaces: (category, where) => places.prefetch(category, where),
|
|
118
|
+
savoir,
|
|
119
|
+
readPage: (url, signal) => readPage(url, signal, pageFetch),
|
|
120
|
+
});
|
|
121
|
+
const audits = new Audits({ engine, journal, settings: () => settings.get(), targets: () => scope.list(), home: config.home, dir: data('audits') });
|
|
122
|
+
const token = options.token ?? randomBytes(24).toString('hex');
|
|
123
|
+
const phone = new PhoneAccess({
|
|
124
|
+
dir: data('telephone'),
|
|
125
|
+
port: config.phonePort,
|
|
126
|
+
...(config.phoneAddresses ? { addresses: () => config.phoneAddresses ?? [] } : {}),
|
|
127
|
+
journal,
|
|
128
|
+
serve: (req, res) => serveRemote(req, res),
|
|
129
|
+
});
|
|
130
|
+
const startedAt = new Date().toISOString();
|
|
131
|
+
const rejections = { count: 0, last: null, reason: null, logged: 0 };
|
|
132
|
+
const status = new StatusService({
|
|
133
|
+
config,
|
|
134
|
+
startedAt,
|
|
135
|
+
journal,
|
|
136
|
+
audits,
|
|
137
|
+
brain,
|
|
138
|
+
engine,
|
|
139
|
+
probe,
|
|
140
|
+
provider,
|
|
141
|
+
search,
|
|
142
|
+
settings: () => settings.get(),
|
|
143
|
+
targets: () => scope.list(),
|
|
144
|
+
guardRejections: () => ({ count: rejections.count, last: rejections.last, reason: rejections.reason }),
|
|
145
|
+
phone: () => phone.summary(),
|
|
146
|
+
});
|
|
147
|
+
const router = new Router();
|
|
148
|
+
// ------------------------------------------------------------ Session et état
|
|
149
|
+
router.add('GET', '/api/session', (ctx) => {
|
|
150
|
+
const device = phoneDevice();
|
|
151
|
+
if (!device)
|
|
152
|
+
return ctx.json({ token, version: VERSION, dev: config.dev });
|
|
153
|
+
// Chaque visite repousse l'échéance du cookie de 30 jours.
|
|
154
|
+
const value = cookieValue(ctx.req.headers.cookie);
|
|
155
|
+
if (value)
|
|
156
|
+
ctx.res.setHeader('Set-Cookie', setCookie(value));
|
|
157
|
+
ctx.json({ token, version: VERSION, dev: config.dev, remote: true, device: device.name });
|
|
158
|
+
});
|
|
159
|
+
router.add('GET', '/api/status', async (ctx) => ctx.json(await status.snapshot()));
|
|
160
|
+
// ------------------------------------------------------------ Réglages, liste blanche, périmètre
|
|
161
|
+
router.add('GET', '/api/settings', (ctx) => ctx.json(settings.get()));
|
|
162
|
+
router.add('PUT', '/api/settings', async (ctx) => {
|
|
163
|
+
const patch = await ctx.body(settingsPatch);
|
|
164
|
+
if (fromPhone.getStore()) {
|
|
165
|
+
const groups = patch;
|
|
166
|
+
const touched = PC_ONLY_SETTINGS.filter((key) => {
|
|
167
|
+
if (!key.includes('.'))
|
|
168
|
+
return groups[key] !== undefined;
|
|
169
|
+
const [group = '', field = ''] = key.split('.');
|
|
170
|
+
const g = groups[group];
|
|
171
|
+
return typeof g === 'object' && g !== null && g[field] !== undefined;
|
|
172
|
+
});
|
|
173
|
+
if (touched.length > 0)
|
|
174
|
+
throw new AppError('pc_only', 403, 'Se règle sur le PC : depuis le téléphone, la politique ne peut pas être élargie (liste blanche, dossiers autorisés, langage contraint, confirmation des lectures, mode full cyber, adresses d’Ollama et de SearXNG).');
|
|
175
|
+
}
|
|
176
|
+
const next = settings.patch(patch);
|
|
177
|
+
journal.add({ kind: 'settings', outcome: 'info', title: `Réglages modifiés : ${Object.keys(patch).join(', ') || 'aucun champ'}`, data: { patch } });
|
|
178
|
+
ctx.json(next);
|
|
179
|
+
});
|
|
180
|
+
router.add('GET', '/api/policy', (ctx) => ctx.json({ items: catalogItems(settings.get()) }));
|
|
181
|
+
router.add('GET', '/api/scope', (ctx) => ctx.json({ targets: scope.list() }));
|
|
182
|
+
router.add('POST', '/api/scope', async (ctx) => {
|
|
183
|
+
const input = await ctx.body(scopeInput);
|
|
184
|
+
const target = scope.add(input);
|
|
185
|
+
journal.add({ kind: 'scope', outcome: 'info', title: `Cible ajoutée au périmètre : ${target.host}`, detail: 'Attestation : la cible t’appartient ou tu as une autorisation écrite.', data: { host: target.host, includeSubdomains: target.includeSubdomains, note: target.note } });
|
|
186
|
+
ctx.json(target, 201);
|
|
187
|
+
});
|
|
188
|
+
router.add('DELETE', '/api/scope/:id', (ctx) => {
|
|
189
|
+
const removed = scope.remove(ctx.params.id ?? '');
|
|
190
|
+
journal.add({ kind: 'scope', outcome: 'info', title: `Cible retirée du périmètre : ${removed.host}`, data: { host: removed.host } });
|
|
191
|
+
ctx.json({ ok: true });
|
|
192
|
+
});
|
|
193
|
+
// ------------------------------------------------------------ Téléphone
|
|
194
|
+
const pcOnly = () => {
|
|
195
|
+
if (fromPhone.getStore())
|
|
196
|
+
throw new AppError('pc_only', 403, 'L’accès téléphone se gère sur le PC, dans Réglages, Téléphone.');
|
|
197
|
+
};
|
|
198
|
+
router.add('GET', '/api/telephone', (ctx) => {
|
|
199
|
+
pcOnly();
|
|
200
|
+
ctx.json(phone.state());
|
|
201
|
+
});
|
|
202
|
+
router.add('PUT', '/api/telephone', async (ctx) => {
|
|
203
|
+
pcOnly();
|
|
204
|
+
const { active } = await ctx.body(phoneToggleInput);
|
|
205
|
+
ctx.json(active ? await phone.open() : await phone.close());
|
|
206
|
+
});
|
|
207
|
+
router.add('POST', '/api/telephone/code', (ctx) => {
|
|
208
|
+
pcOnly();
|
|
209
|
+
ctx.json(phone.newCode());
|
|
210
|
+
});
|
|
211
|
+
router.add('DELETE', '/api/telephone/appareils/:id', (ctx) => {
|
|
212
|
+
pcOnly();
|
|
213
|
+
ctx.json(phone.removeDevice(ctx.params.id ?? ''));
|
|
214
|
+
});
|
|
215
|
+
router.add('POST', PAIR_PATH, async (ctx) => {
|
|
216
|
+
// Sur le PC, il n'y a rien à appairer : la route n'existe que par l'écoute du téléphone.
|
|
217
|
+
if (!fromPhone.getStore())
|
|
218
|
+
throw notFound('Adresse inconnue de l’API.');
|
|
219
|
+
const { code } = await ctx.body(pairInput);
|
|
220
|
+
const { device, cookie } = phone.pair(code, String(ctx.req.headers['user-agent'] ?? ''));
|
|
221
|
+
ctx.res.setHeader('Set-Cookie', setCookie(cookie));
|
|
222
|
+
ctx.json({ name: device.name });
|
|
223
|
+
});
|
|
224
|
+
// ------------------------------------------------------------ Moteur d'exécution
|
|
225
|
+
router.add('POST', '/api/engine/preview', async (ctx) => {
|
|
226
|
+
const input = await ctx.body(previewInput);
|
|
227
|
+
ctx.json(await engine.preview(input.command, input.cwd, 'terminal'));
|
|
228
|
+
});
|
|
229
|
+
router.add('POST', '/api/engine/run', async (ctx) => {
|
|
230
|
+
const input = await ctx.body(runInput);
|
|
231
|
+
const execution = engine.start(input.ticket, { acknowledge: input.acknowledge, confirmation: input.confirmation });
|
|
232
|
+
await pipeEvents(ctx, execution.events);
|
|
233
|
+
});
|
|
234
|
+
router.add('POST', '/api/engine/cancel', async (ctx) => {
|
|
235
|
+
const input = await ctx.body(cancelInput);
|
|
236
|
+
ctx.json({ cancelled: engine.cancel(input.execId) });
|
|
237
|
+
});
|
|
238
|
+
router.add('POST', '/api/engine/cwd', async (ctx) => {
|
|
239
|
+
const input = await ctx.body(cwdInput);
|
|
240
|
+
ctx.json({ cwd: engine.resolveCwd(input.from, input.to) });
|
|
241
|
+
});
|
|
242
|
+
router.add('GET', '/api/engine/cwd', (ctx) => ctx.json({ cwd: engine.available ? engine.resolveCwd(engine.defaultCwd(), '.') : engine.defaultCwd() }));
|
|
243
|
+
// ------------------------------------------------------------ Journal
|
|
244
|
+
router.add('GET', '/api/journal', (ctx) => {
|
|
245
|
+
const q = ctx.url.searchParams;
|
|
246
|
+
ctx.json({
|
|
247
|
+
entries: journal.query({
|
|
248
|
+
kind: pick(q.get('kind'), JOURNAL_KINDS),
|
|
249
|
+
outcome: pick(q.get('outcome'), JOURNAL_OUTCOMES),
|
|
250
|
+
...(q.get('q') ? { q: q.get('q') ?? '' } : {}),
|
|
251
|
+
limit: Number(q.get('limit')) || 100,
|
|
252
|
+
...(q.get('before') ? { before: q.get('before') ?? '' } : {}),
|
|
253
|
+
}),
|
|
254
|
+
total: journal.size,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
router.add('GET', '/api/journal/export', (ctx) => ctx.text(journal.exportText(), 'application/x-ndjson', 'promethee-journal.jsonl'));
|
|
258
|
+
// ------------------------------------------------------------ Cerveau
|
|
259
|
+
router.add('GET', '/api/models', async (ctx) => ctx.json(await brain.models()));
|
|
260
|
+
router.add('GET', '/api/brain/conversations', (ctx) => ctx.json({ conversations: conversations.list() }));
|
|
261
|
+
router.add('POST', '/api/brain/conversations', async (ctx) => {
|
|
262
|
+
const input = await ctx.body(newConversationInput);
|
|
263
|
+
ctx.json(conversations.create(input.model ?? settings.get().model ?? ''), 201);
|
|
264
|
+
});
|
|
265
|
+
router.add('GET', '/api/brain/conversations/:id', (ctx) => ctx.json(conversations.get(ctx.params.id ?? '')));
|
|
266
|
+
router.add('DELETE', '/api/brain/conversations/:id', (ctx) => {
|
|
267
|
+
brain.stop(ctx.params.id ?? '');
|
|
268
|
+
conversations.remove(ctx.params.id ?? '');
|
|
269
|
+
ctx.json({ ok: true });
|
|
270
|
+
});
|
|
271
|
+
router.add('POST', '/api/brain/conversations/:id/send', async (ctx) => {
|
|
272
|
+
const input = await ctx.body(sendInput);
|
|
273
|
+
await pipeEvents(ctx, await brain.send(ctx.params.id ?? '', input.text, input.attachments ?? []));
|
|
274
|
+
});
|
|
275
|
+
router.add('POST', '/api/brain/conversations/:id/decision', async (ctx) => {
|
|
276
|
+
const input = await ctx.body(decisionInput);
|
|
277
|
+
await pipeEvents(ctx, await brain.decide(ctx.params.id ?? '', input.proposalId, input.decision, input.acknowledge));
|
|
278
|
+
});
|
|
279
|
+
// ------------------------------------------------------------ Carte des recherches
|
|
280
|
+
const mapAllowed = () => {
|
|
281
|
+
if (!settings.get().search.enabled)
|
|
282
|
+
throw forbidden('La recherche web est coupée dans les réglages, la carte aussi.', 'search_disabled');
|
|
283
|
+
};
|
|
284
|
+
/** Abandonné si la page ferme la connexion avant la réponse. */
|
|
285
|
+
const untilClosed = (res) => {
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
res.on('close', () => {
|
|
288
|
+
if (!res.writableFinished)
|
|
289
|
+
controller.abort();
|
|
290
|
+
});
|
|
291
|
+
return controller.signal;
|
|
292
|
+
};
|
|
293
|
+
router.add('GET', '/api/map/places', async (ctx) => {
|
|
294
|
+
mapAllowed();
|
|
295
|
+
const { category, where } = parsePlacesRequest(ctx.url.searchParams.get('category'), ctx.url.searchParams.get('where'));
|
|
296
|
+
ctx.json(await places.get(category, where));
|
|
297
|
+
});
|
|
298
|
+
router.add('GET', '/api/map/card', async (ctx) => {
|
|
299
|
+
mapAllowed();
|
|
300
|
+
ctx.json({ card: await fiches.card(parseCardRequest(ctx.url.searchParams), untilClosed(ctx.res)) });
|
|
301
|
+
});
|
|
302
|
+
router.add('GET', '/api/map/image/:id', async (ctx) => {
|
|
303
|
+
mapAllowed();
|
|
304
|
+
const { bytes, type } = await fiches.image(ctx.params.id ?? '', untilClosed(ctx.res));
|
|
305
|
+
ctx.res.writeHead(200, { 'Content-Type': type, 'Content-Length': bytes.length, 'Cache-Control': 'private, max-age=86400', 'X-Content-Type-Options': 'nosniff' });
|
|
306
|
+
ctx.res.end(bytes);
|
|
307
|
+
});
|
|
308
|
+
router.add('GET', '/api/map/tile/:z/:x/:y', async (ctx) => {
|
|
309
|
+
mapAllowed();
|
|
310
|
+
const { z, x, y } = parseTile(ctx.params.z ?? '', ctx.params.x ?? '', ctx.params.y ?? '');
|
|
311
|
+
const png = await tiles.get(z, x, y);
|
|
312
|
+
ctx.res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': png.length, 'Cache-Control': 'private, max-age=86400' });
|
|
313
|
+
ctx.res.end(png);
|
|
314
|
+
});
|
|
315
|
+
// ------------------------------------------------------------ Savoir local
|
|
316
|
+
router.add('GET', '/api/savoir', (ctx) => {
|
|
317
|
+
const limit = Number(ctx.url.searchParams.get('limit') ?? 100);
|
|
318
|
+
ctx.json(savoir.list({ q: (ctx.url.searchParams.get('q') ?? '').slice(0, 200), limit: Number.isFinite(limit) ? limit : 100 }));
|
|
319
|
+
});
|
|
320
|
+
router.add('GET', '/api/savoir/:id', (ctx) => {
|
|
321
|
+
const found = savoir.get(ctx.params.id ?? '');
|
|
322
|
+
if (!found)
|
|
323
|
+
throw notFound('Cette page n’est plus dans le savoir.');
|
|
324
|
+
ctx.json(found);
|
|
325
|
+
});
|
|
326
|
+
router.add('DELETE', '/api/savoir/:id', (ctx) => {
|
|
327
|
+
const page = savoir.forget(ctx.params.id ?? '');
|
|
328
|
+
if (!page)
|
|
329
|
+
throw notFound('Cette page n’est plus dans le savoir.');
|
|
330
|
+
journal.add({ kind: 'search', outcome: 'info', title: `Savoir : page oubliée, ${page.title.slice(0, 100)}`, detail: page.url, data: { url: page.url, origin: 'console' } });
|
|
331
|
+
ctx.json({ ok: true });
|
|
332
|
+
});
|
|
333
|
+
router.add('DELETE', '/api/savoir', (ctx) => {
|
|
334
|
+
const removed = savoir.forgetAll();
|
|
335
|
+
journal.add({ kind: 'search', outcome: 'info', title: 'Savoir vidé', detail: `${removed} page${removed > 1 ? 's' : ''} oubliée${removed > 1 ? 's' : ''}.`, data: { removed, origin: 'console' } });
|
|
336
|
+
ctx.json({ removed });
|
|
337
|
+
});
|
|
338
|
+
router.add('POST', '/api/brain/conversations/:id/stop', (ctx) => ctx.json({ stopped: brain.stop(ctx.params.id ?? '') }));
|
|
339
|
+
router.add('GET', '/api/tui/sessions', (ctx) => ctx.json({ sessions: listTuiSessions(config.tuiDir) }));
|
|
340
|
+
// ------------------------------------------------------------ Audits
|
|
341
|
+
router.add('GET', '/api/audits/modules', (ctx) => ctx.json({ modules: audits.modules() }));
|
|
342
|
+
router.add('POST', '/api/audits/run', async (ctx) => {
|
|
343
|
+
const input = await ctx.body(auditInput);
|
|
344
|
+
const events = audits.run(input.module, input.input);
|
|
345
|
+
// Le premier pas vérifie l'entrée et le périmètre : une erreur ici part en réponse HTTP normale.
|
|
346
|
+
const first = await events.next();
|
|
347
|
+
await pipeEvents(ctx, (async function* () {
|
|
348
|
+
if (!first.done)
|
|
349
|
+
yield first.value;
|
|
350
|
+
yield* events;
|
|
351
|
+
})());
|
|
352
|
+
});
|
|
353
|
+
router.add('GET', '/api/audits/history', (ctx) => ctx.json({ runs: audits.history({ module: ctx.url.searchParams.get('module') ?? undefined }) }));
|
|
354
|
+
router.add('GET', '/api/audits/findings', (ctx) => ctx.json({ runs: audits.latest() }));
|
|
355
|
+
router.add('GET', '/api/audits/:id', (ctx) => ctx.json(audits.get(ctx.params.id ?? '')));
|
|
356
|
+
router.add('GET', '/api/audits/:id/report', (ctx) => ctx.text(audits.report(ctx.params.id ?? ''), 'text/markdown', `promethee-audit-${ctx.params.id}.md`));
|
|
357
|
+
router.add('DELETE', '/api/audits/:id', (ctx) => {
|
|
358
|
+
audits.remove(ctx.params.id ?? '');
|
|
359
|
+
ctx.json({ ok: true });
|
|
360
|
+
});
|
|
361
|
+
const parseUrl = (req, res) => {
|
|
362
|
+
try {
|
|
363
|
+
return new URL(req.url ?? '/', 'http://localhost');
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
sendJson(res, { error: { code: 'bad_request', message: 'Adresse invalide.' } }, 400);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const reject = (req, res, url, verdict) => {
|
|
371
|
+
// Un téléphone pas encore appairé demande la session : c'est le chemin normal de l'appairage, pas une attaque.
|
|
372
|
+
if (verdict.code !== 'not_paired') {
|
|
373
|
+
rejections.count++;
|
|
374
|
+
rejections.last = new Date().toISOString();
|
|
375
|
+
rejections.reason = verdict.message;
|
|
376
|
+
// Une entrée de journal toutes les 30 secondes au plus, pour qu'une rafale ne le remplisse pas.
|
|
377
|
+
if (Date.now() - rejections.logged > 30_000) {
|
|
378
|
+
rejections.logged = Date.now();
|
|
379
|
+
journal.add({ kind: 'security', outcome: 'refused', title: `Requête refusée par la garde : ${verdict.message}`, detail: `${req.method ?? '?'} ${url.pathname}`, data: { code: verdict.code, host: req.headers.host ?? null, origin: req.headers.origin ?? null } });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
sendJson(res, { error: { code: verdict.code, message: verdict.message } }, verdict.status);
|
|
383
|
+
};
|
|
384
|
+
const dispatch = async (req, res, url) => {
|
|
385
|
+
if (url.pathname.startsWith('/api/')) {
|
|
386
|
+
for (const [name, value] of Object.entries(SECURITY_HEADERS))
|
|
387
|
+
if (name !== 'Content-Security-Policy')
|
|
388
|
+
res.setHeader(name, value);
|
|
389
|
+
const handled = await router.handle(req, res, url);
|
|
390
|
+
if (!handled)
|
|
391
|
+
sendJson(res, { error: { code: 'not_found', message: 'Adresse inconnue de l’API.' } }, 404);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
serveStatic(res, config.distDir, url.pathname, req.method ?? 'GET');
|
|
395
|
+
};
|
|
396
|
+
const server = http.createServer((req, res) => {
|
|
397
|
+
// Le port réellement écouté fait foi (utile quand le système en choisit un libre).
|
|
398
|
+
const address = server.address();
|
|
399
|
+
const guard = { port: address && typeof address === 'object' ? address.port : config.port, dev: config.dev, token };
|
|
400
|
+
void (async () => {
|
|
401
|
+
const url = parseUrl(req, res);
|
|
402
|
+
if (!url)
|
|
403
|
+
return;
|
|
404
|
+
const verdict = checkRequest(req, url.pathname, guard);
|
|
405
|
+
if (!verdict.ok)
|
|
406
|
+
return reject(req, res, url, verdict);
|
|
407
|
+
await dispatch(req, res, url);
|
|
408
|
+
})();
|
|
409
|
+
});
|
|
410
|
+
/** Requête arrivée par l'écoute du téléphone (server/telephone/acces.ts). */
|
|
411
|
+
const serveRemote = (req, res) => {
|
|
412
|
+
void (async () => {
|
|
413
|
+
const url = parseUrl(req, res);
|
|
414
|
+
if (!url)
|
|
415
|
+
return;
|
|
416
|
+
const verdict = checkRemoteRequest(req, url.pathname, { hosts: phone.hosts(), token, device: (cookie) => phone.device(cookie, req.socket) });
|
|
417
|
+
if (!verdict.ok)
|
|
418
|
+
return reject(req, res, url, verdict);
|
|
419
|
+
await fromPhone.run({ device: verdict.device }, () => dispatch(req, res, url));
|
|
420
|
+
})();
|
|
421
|
+
};
|
|
422
|
+
journal.add({ kind: 'system', outcome: 'info', title: `Serveur Prométhée ${VERSION} démarré`, detail: probe.available ? `PowerShell ${probe.version ?? '?'} · page de codes ${probe.codePage ?? '?'}` : `Moteur indisponible : ${probe.error ?? ''}` });
|
|
423
|
+
return {
|
|
424
|
+
server,
|
|
425
|
+
token,
|
|
426
|
+
engine,
|
|
427
|
+
journal,
|
|
428
|
+
settings,
|
|
429
|
+
scope,
|
|
430
|
+
audits,
|
|
431
|
+
brain,
|
|
432
|
+
phone,
|
|
433
|
+
close: async () => {
|
|
434
|
+
await phone.dispose();
|
|
435
|
+
await new Promise((resolve) => {
|
|
436
|
+
engine.cancelAll();
|
|
437
|
+
analyzer.dispose();
|
|
438
|
+
server.close(() => resolve());
|
|
439
|
+
server.closeAllConnections();
|
|
440
|
+
});
|
|
441
|
+
savoir.close();
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|