@imenam/simple-scraper 1.0.23 → 1.0.27
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/README.md +438 -398
- package/dist/browser.js +32 -10
- package/dist/cli/setup-mcp.js +2 -2
- package/dist/cookie-sync.js +67 -0
- package/dist/cookies.js +28 -40
- package/dist/data-dir.js +24 -0
- package/dist/gui.js +75 -85
- package/dist/index.js +36 -38
- package/dist/keep-alive.js +51 -0
- package/dist/login.js +139 -0
- package/dist/relay.js +11 -7
- package/dist/rpc.js +159 -0
- package/dist/sessions.js +23 -11
- package/dist/site-check.js +88 -0
- package/dist/site-store.js +210 -0
- package/package.json +2 -1
- package/public/dist/assets/index-CmU4YRqF.js +11 -0
- package/public/dist/assets/index-olIqry-_.css +1 -0
- package/public/dist/index.html +13 -13
- package/public/dist/assets/index-DAjr-PsH.js +0 -12
- package/public/dist/assets/index-IkSxq6_f.css +0 -1
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import puppeteer from 'puppeteer';
|
|
2
|
+
import { getStore } from './site-store.js';
|
|
2
3
|
let browser = null;
|
|
4
|
+
let launching = null;
|
|
3
5
|
export function getDefaultTimeout() {
|
|
4
6
|
const val = process.env.PUPPETEER_TIMEOUT;
|
|
5
7
|
return val ? parseInt(val, 10) : 30_000;
|
|
@@ -27,18 +29,36 @@ export function getDefaultViewport() {
|
|
|
27
29
|
deviceScaleFactor: envNumber('PUPPETEER_DEVICE_SCALE_FACTOR', 1),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
32
|
+
/** Lance le navigateur partagé et y charge les cookies de tous les sites. */
|
|
33
|
+
async function launchBrowser() {
|
|
34
|
+
const cookies = getStore()?.allCookies() ?? [];
|
|
34
35
|
const headless = process.env.PUPPETEER_HEADLESS !== 'false';
|
|
35
|
-
|
|
36
|
+
const launched = await puppeteer.launch({
|
|
36
37
|
headless,
|
|
37
38
|
defaultViewport: getDefaultViewport(),
|
|
38
39
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
if (cookies.length)
|
|
42
|
+
await launched.setCookie(...cookies);
|
|
43
|
+
console.error(`[Browser] Launched Puppeteer instance (${cookies.length} site cookie(s) loaded)`);
|
|
44
|
+
return launched;
|
|
45
|
+
}
|
|
46
|
+
export async function getBrowser() {
|
|
47
|
+
if (browser && browser.connected) {
|
|
48
|
+
return browser;
|
|
49
|
+
}
|
|
50
|
+
if (!launching) {
|
|
51
|
+
launching = launchBrowser()
|
|
52
|
+
.then(b => (browser = b))
|
|
53
|
+
.finally(() => {
|
|
54
|
+
launching = null;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return launching;
|
|
58
|
+
}
|
|
59
|
+
/** Navigateur partagé s'il est déjà lancé, sans le démarrer. */
|
|
60
|
+
export function getRunningBrowser() {
|
|
61
|
+
return browser && browser.connected ? browser : null;
|
|
42
62
|
}
|
|
43
63
|
export async function closeBrowser() {
|
|
44
64
|
if (browser) {
|
|
@@ -54,11 +74,13 @@ export async function closeBrowser() {
|
|
|
54
74
|
}
|
|
55
75
|
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
56
76
|
process.on(sig, async () => {
|
|
57
|
-
// Sessions must be closed before the browser to avoid dangling page handles
|
|
58
|
-
//
|
|
59
|
-
//
|
|
77
|
+
// Sessions must be closed before the browser to avoid dangling page handles,
|
|
78
|
+
// and site cookies saved before it disappears. Both modules are imported
|
|
79
|
+
// dynamically: each of them imports this one.
|
|
60
80
|
const { closeAllSessions } = await import('./sessions.js');
|
|
81
|
+
const { syncJarsFromBrowser } = await import('./cookie-sync.js');
|
|
61
82
|
await closeAllSessions();
|
|
83
|
+
await syncJarsFromBrowser().catch(err => console.error(`[Cookies] Synchronisation échouée : ${err}`));
|
|
62
84
|
await closeBrowser();
|
|
63
85
|
process.exit(0);
|
|
64
86
|
});
|
package/dist/cli/setup-mcp.js
CHANGED
|
@@ -33,7 +33,7 @@ function buildServerEntry(codeRoot) {
|
|
|
33
33
|
PROXY_APP_PATH: `/simple-scraper-${folder}`,
|
|
34
34
|
PUPPETEER_VIEWPORT_WIDTH: '1920',
|
|
35
35
|
PUPPETEER_VIEWPORT_HEIGHT: '1080',
|
|
36
|
-
|
|
36
|
+
SCRAPER_DATA_DIR: '',
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
}
|
|
@@ -44,7 +44,7 @@ const GENERATED_VARS = [
|
|
|
44
44
|
['PROXY_APP_PATH', 'chemin de montage de la GUI derrière le proxy'],
|
|
45
45
|
['PUPPETEER_VIEWPORT_WIDTH', 'largeur du viewport en pixels CSS'],
|
|
46
46
|
['PUPPETEER_VIEWPORT_HEIGHT', 'hauteur du viewport en pixels CSS'],
|
|
47
|
-
['
|
|
47
|
+
['SCRAPER_DATA_DIR', 'dossier des sites : réglages, cookies, historique (alias : COOKIES_DIR)'],
|
|
48
48
|
];
|
|
49
49
|
const OPTIONAL_VARS = [
|
|
50
50
|
['PUPPETEER_DEVICE_SCALE_FACTOR', 'pixels physiques par pixel CSS (2 = screenshots retina)'],
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getRunningBrowser } from './browser.js';
|
|
2
|
+
import { cookieInScope, getStore, requireStore, toCookieData } from './site-store.js';
|
|
3
|
+
const PERIODIC_SYNC_MS = 5 * 60 * 1000;
|
|
4
|
+
let queue = Promise.resolve();
|
|
5
|
+
/** Exécute les opérations sur les cookies une par une, dans l'ordre d'appel. */
|
|
6
|
+
function runExclusive(fn) {
|
|
7
|
+
const next = queue.then(fn);
|
|
8
|
+
queue = next.catch(() => undefined);
|
|
9
|
+
return next;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Recopie dans les cookies de chaque site ceux que le navigateur partagé détient
|
|
13
|
+
* pour ses domaines : les cookies renouvelés par le site sont gardés, ceux qu'il
|
|
14
|
+
* a supprimés disparaissent.
|
|
15
|
+
*/
|
|
16
|
+
export function syncJarsFromBrowser() {
|
|
17
|
+
return runExclusive(async () => {
|
|
18
|
+
const browser = getRunningBrowser();
|
|
19
|
+
const store = getStore();
|
|
20
|
+
if (!browser || !store)
|
|
21
|
+
return;
|
|
22
|
+
const all = await browser.cookies();
|
|
23
|
+
for (const site of store.list()) {
|
|
24
|
+
const scope = store.cookieScope(site.id);
|
|
25
|
+
store.writeJar(site.id, all.filter(c => cookieInScope(c.domain, scope)));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Lance la synchronisation en arrière-plan ; une erreur est journalisée. */
|
|
30
|
+
export function scheduleJarSync() {
|
|
31
|
+
syncJarsFromBrowser().catch(err => console.error(`[Cookies] Synchronisation échouée : ${err instanceof Error ? err.message : String(err)}`));
|
|
32
|
+
}
|
|
33
|
+
export function startPeriodicJarSync() {
|
|
34
|
+
setInterval(scheduleJarSync, PERIODIC_SYNC_MS).unref();
|
|
35
|
+
}
|
|
36
|
+
/** Retire du navigateur partagé les cookies couverts par les domaines donnés. */
|
|
37
|
+
async function deleteBrowserCookies(scope) {
|
|
38
|
+
const browser = getRunningBrowser();
|
|
39
|
+
if (!browser)
|
|
40
|
+
return;
|
|
41
|
+
const existing = (await browser.cookies()).filter(c => cookieInScope(c.domain, scope));
|
|
42
|
+
if (existing.length)
|
|
43
|
+
await browser.deleteCookie(...existing);
|
|
44
|
+
}
|
|
45
|
+
/** Remplace les cookies du site, dans son dossier comme dans le navigateur partagé. */
|
|
46
|
+
export function replaceSiteCookies(siteId, cookies) {
|
|
47
|
+
return runExclusive(async () => {
|
|
48
|
+
const store = requireStore();
|
|
49
|
+
const previousScope = store.cookieScope(siteId);
|
|
50
|
+
const prepared = cookies.map(toCookieData);
|
|
51
|
+
store.writeJar(siteId, prepared);
|
|
52
|
+
await deleteBrowserCookies(Array.from(new Set([...previousScope, ...store.cookieScope(siteId)])));
|
|
53
|
+
const browser = getRunningBrowser();
|
|
54
|
+
if (browser && prepared.length)
|
|
55
|
+
await browser.setCookie(...prepared);
|
|
56
|
+
return prepared.length;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Efface les cookies du site, dans son dossier comme dans le navigateur partagé. */
|
|
60
|
+
export function clearSiteCookies(siteId) {
|
|
61
|
+
return runExclusive(async () => {
|
|
62
|
+
const store = requireStore();
|
|
63
|
+
const scope = store.cookieScope(siteId);
|
|
64
|
+
store.writeJar(siteId, []);
|
|
65
|
+
await deleteBrowserCookies(scope);
|
|
66
|
+
});
|
|
67
|
+
}
|
package/dist/cookies.js
CHANGED
|
@@ -1,49 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const HTTP_ONLY_PREFIX = '#HttpOnly_';
|
|
2
|
+
/**
|
|
3
|
+
* Lit un export de cookies au format Netscape (une ligne par cookie, champs
|
|
4
|
+
* séparés par des tabulations). Le préfixe `#HttpOnly_` marque un cookie
|
|
5
|
+
* http-only ; une date d'expiration à 0 désigne un cookie de session.
|
|
6
|
+
*/
|
|
3
7
|
export function parseNetscapeCookies(content) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
const cookies = [];
|
|
9
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
10
|
+
let line = rawLine.trim();
|
|
11
|
+
let httpOnly = false;
|
|
12
|
+
if (line.startsWith(HTTP_ONLY_PREFIX)) {
|
|
13
|
+
httpOnly = true;
|
|
14
|
+
line = line.slice(HTTP_ONLY_PREFIX.length);
|
|
15
|
+
}
|
|
16
|
+
else if (!line || line.startsWith('#')) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
8
19
|
const parts = line.split('\t');
|
|
9
20
|
if (parts.length < 7)
|
|
10
|
-
|
|
11
|
-
const [domain, , cookiePath, secure, , name,
|
|
12
|
-
|
|
21
|
+
continue;
|
|
22
|
+
const [domain, , cookiePath, secure, expires, name, ...valueParts] = parts;
|
|
23
|
+
const cookie = {
|
|
13
24
|
domain: domain.trim(),
|
|
14
|
-
path: cookiePath.trim(),
|
|
25
|
+
path: cookiePath.trim() || '/',
|
|
15
26
|
secure: secure.trim().toUpperCase() === 'TRUE',
|
|
27
|
+
httpOnly,
|
|
16
28
|
name: name.trim(),
|
|
17
|
-
value:
|
|
29
|
+
value: valueParts.join('\t').trim(),
|
|
18
30
|
};
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* Loads and merges all cookie files from the COOKIES_DIR directory.
|
|
24
|
-
* The directory is created if it does not exist yet.
|
|
25
|
-
* Silently returns an empty array if COOKIES_DIR is not set.
|
|
26
|
-
*/
|
|
27
|
-
export async function loadAllCookies() {
|
|
28
|
-
const cookiesDir = process.env.COOKIES_DIR;
|
|
29
|
-
if (!cookiesDir)
|
|
30
|
-
return [];
|
|
31
|
-
let files;
|
|
32
|
-
try {
|
|
33
|
-
await fs.mkdir(cookiesDir, { recursive: true });
|
|
34
|
-
files = await fs.readdir(cookiesDir);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return [];
|
|
31
|
+
const expiresSec = Number(expires);
|
|
32
|
+
if (Number.isFinite(expiresSec) && expiresSec > 0)
|
|
33
|
+
cookie.expires = expiresSec;
|
|
34
|
+
cookies.push(cookie);
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const content = await fs.readFile(path.join(cookiesDir, file), 'utf-8');
|
|
42
|
-
return parseNetscapeCookies(content);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}));
|
|
48
|
-
return results.flat();
|
|
36
|
+
return cookies;
|
|
49
37
|
}
|
package/dist/data-dir.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Dossier de données des sites (réglages, cookies, historique).
|
|
5
|
+
*
|
|
6
|
+
* SCRAPER_DATA_DIR et COOKIES_DIR sont deux noms du même réglage : l'un ou
|
|
7
|
+
* l'autre suffit. Définis tous les deux, ils doivent désigner le même dossier.
|
|
8
|
+
* Retourne null quand aucun des deux n'est renseigné : les sites sont alors
|
|
9
|
+
* inactifs. Le dossier est créé s'il n'existe pas.
|
|
10
|
+
*/
|
|
11
|
+
export function getDataDir() {
|
|
12
|
+
const dataDir = process.env.SCRAPER_DATA_DIR?.trim() ?? '';
|
|
13
|
+
const cookiesDir = process.env.COOKIES_DIR?.trim() ?? '';
|
|
14
|
+
if (dataDir && cookiesDir && path.resolve(dataDir) !== path.resolve(cookiesDir)) {
|
|
15
|
+
throw new Error(`SCRAPER_DATA_DIR (${dataDir}) et COOKIES_DIR (${cookiesDir}) désignent deux dossiers différents. ` +
|
|
16
|
+
'Ces deux variables nomment le même réglage : définir l\'une des deux, ou leur donner la même valeur.');
|
|
17
|
+
}
|
|
18
|
+
const dir = dataDir || cookiesDir;
|
|
19
|
+
if (!dir)
|
|
20
|
+
return null;
|
|
21
|
+
const resolved = path.resolve(dir);
|
|
22
|
+
fs.mkdirSync(resolved, { recursive: true });
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
package/dist/gui.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import bodyParser from 'body-parser';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import fs from 'fs';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
5
|
import { setupLogging, ProxyClient, IpcHub, GUI_LISTEN_HOST, expressGuiGuard } from '@imenam/mcp-gui-interface';
|
|
7
6
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -13,6 +12,7 @@ async function callMaster(type, data, timeoutMs = 5000) {
|
|
|
13
12
|
return resp.data;
|
|
14
13
|
}
|
|
15
14
|
const SESSION_ID_RE = /^sess_[a-z0-9]{6}$/;
|
|
15
|
+
const SITE_ID_RE = /^site_[a-z0-9]{6}$/;
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const rootDir = path.resolve(__dirname, '..');
|
|
18
18
|
setupLogging({ processLabel: 'GUI', logDir: process.env.MCP_LOG_DIR });
|
|
@@ -61,17 +61,9 @@ process.on('unhandledRejection', (reason) => {
|
|
|
61
61
|
console.error(`[GUI] Unhandled Rejection: ${reason}`);
|
|
62
62
|
process.exit(1);
|
|
63
63
|
});
|
|
64
|
-
/** Retourne COOKIES_DIR en le créant au besoin (null si non configuré). */
|
|
65
|
-
const getCookiesDir = () => {
|
|
66
|
-
const cookiesDir = process.env.COOKIES_DIR ?? null;
|
|
67
|
-
if (!cookiesDir)
|
|
68
|
-
return null;
|
|
69
|
-
fs.mkdirSync(cookiesDir, { recursive: true });
|
|
70
|
-
return cookiesDir;
|
|
71
|
-
};
|
|
72
64
|
const app = express();
|
|
73
65
|
app.use(expressGuiGuard());
|
|
74
|
-
app.use(bodyParser.json());
|
|
66
|
+
app.use(bodyParser.json({ limit: '5mb' }));
|
|
75
67
|
app.use(express.static(path.join(rootDir, 'public/dist')));
|
|
76
68
|
app.get(/^(?!\/api\/|\/proxy\/)/, (_req, res) => {
|
|
77
69
|
res.sendFile(path.join(rootDir, 'public/dist/index.html'));
|
|
@@ -79,103 +71,101 @@ app.get(/^(?!\/api\/|\/proxy\/)/, (_req, res) => {
|
|
|
79
71
|
app.get('/proxy/health', (_req, res) => {
|
|
80
72
|
res.status(200).json({ status: 'healthy' });
|
|
81
73
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const cookiesDir = getCookiesDir();
|
|
85
|
-
if (!cookiesDir) {
|
|
86
|
-
res.status(400).json({ success: false, error: 'COOKIES_DIR not configured' });
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
74
|
+
/** Transmet la requête au master et renvoie sa réponse en JSON, ou l'erreur en 500. */
|
|
75
|
+
async function forward(res, type, data, timeoutMs) {
|
|
89
76
|
try {
|
|
90
|
-
const
|
|
91
|
-
res.json({ success: true,
|
|
77
|
+
const result = await callMaster(type, data, timeoutMs);
|
|
78
|
+
res.json({ success: true, data: result ?? null });
|
|
92
79
|
}
|
|
93
80
|
catch (error) {
|
|
94
81
|
const message = error instanceof Error ? error.message : String(error);
|
|
95
82
|
res.status(500).json({ success: false, error: message });
|
|
96
83
|
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const cookiesDir = getCookiesDir();
|
|
101
|
-
if (!cookiesDir) {
|
|
102
|
-
res.status(400).json({ success: false, error: 'COOKIES_DIR not configured' });
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const { filename, content } = req.body;
|
|
106
|
-
if (!filename || typeof content !== 'string') {
|
|
107
|
-
res.status(400).json({ success: false, error: 'filename and content are required' });
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const safeName = path.basename(filename);
|
|
84
|
+
}
|
|
85
|
+
/** Renvoie la capture JPEG produite par le master. */
|
|
86
|
+
async function sendJpeg(res, type, data) {
|
|
111
87
|
try {
|
|
112
|
-
|
|
113
|
-
res.
|
|
88
|
+
const shot = await callMaster(type, data);
|
|
89
|
+
res.type('image/jpeg').send(Buffer.from(shot.base64, 'base64'));
|
|
114
90
|
}
|
|
115
91
|
catch (error) {
|
|
116
92
|
const message = error instanceof Error ? error.message : String(error);
|
|
117
93
|
res.status(500).json({ success: false, error: message });
|
|
118
94
|
}
|
|
95
|
+
}
|
|
96
|
+
/** Refuse en 400 un identifiant mal formé ; retourne false dans ce cas. */
|
|
97
|
+
function checkId(res, value, re, label) {
|
|
98
|
+
if (re.test(value))
|
|
99
|
+
return true;
|
|
100
|
+
res.status(400).json({ success: false, error: `Invalid ${label} ID` });
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
// ─── Statut général ───────────────────────────────────────────────────────────
|
|
104
|
+
app.get('/api/status', (_req, res) => forward(res, 'STATUS'));
|
|
105
|
+
// ─── Sites ────────────────────────────────────────────────────────────────────
|
|
106
|
+
app.get('/api/sites', (_req, res) => forward(res, 'SITES_LIST'));
|
|
107
|
+
app.post('/api/sites', (req, res) => forward(res, 'SITE_CREATE', { config: req.body }));
|
|
108
|
+
app.get('/api/sites/:id', (req, res) => {
|
|
109
|
+
if (checkId(res, req.params.id, SITE_ID_RE, 'site'))
|
|
110
|
+
void forward(res, 'SITE_GET', { siteId: req.params.id });
|
|
119
111
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!cookiesDir) {
|
|
124
|
-
res.status(400).json({ success: false, error: 'COOKIES_DIR not configured' });
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const safeName = path.basename(req.params.filename);
|
|
128
|
-
try {
|
|
129
|
-
const content = fs.readFileSync(path.join(cookiesDir, safeName), 'utf-8');
|
|
130
|
-
res.json({ success: true, content });
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
-
res.status(500).json({ success: false, error: message });
|
|
112
|
+
app.put('/api/sites/:id', (req, res) => {
|
|
113
|
+
if (checkId(res, req.params.id, SITE_ID_RE, 'site')) {
|
|
114
|
+
void forward(res, 'SITE_UPDATE', { siteId: req.params.id, config: req.body });
|
|
135
115
|
}
|
|
136
116
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!cookiesDir) {
|
|
141
|
-
res.status(400).json({ success: false, error: 'COOKIES_DIR not configured' });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const safeName = path.basename(req.params.filename);
|
|
145
|
-
try {
|
|
146
|
-
fs.unlinkSync(path.join(cookiesDir, safeName));
|
|
147
|
-
res.json({ success: true });
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
-
res.status(500).json({ success: false, error: message });
|
|
152
|
-
}
|
|
117
|
+
app.delete('/api/sites/:id', (req, res) => {
|
|
118
|
+
if (checkId(res, req.params.id, SITE_ID_RE, 'site'))
|
|
119
|
+
void forward(res, 'SITE_DELETE', { siteId: req.params.id });
|
|
153
120
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
121
|
+
app.post('/api/sites/:id/check', (req, res) => {
|
|
122
|
+
if (checkId(res, req.params.id, SITE_ID_RE, 'site'))
|
|
123
|
+
void forward(res, 'SITE_CHECK', { siteId: req.params.id });
|
|
124
|
+
});
|
|
125
|
+
app.post('/api/sites/:id/cookies', (req, res) => {
|
|
126
|
+
if (checkId(res, req.params.id, SITE_ID_RE, 'site')) {
|
|
127
|
+
void forward(res, 'SITE_IMPORT_COOKIES', { siteId: req.params.id, content: req.body.content }, 15_000);
|
|
159
128
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
129
|
+
});
|
|
130
|
+
app.delete('/api/sites/:id/cookies', (req, res) => {
|
|
131
|
+
if (checkId(res, req.params.id, SITE_ID_RE, 'site')) {
|
|
132
|
+
void forward(res, 'SITE_CLEAR_COOKIES', { siteId: req.params.id }, 15_000);
|
|
163
133
|
}
|
|
164
134
|
});
|
|
165
|
-
// ───
|
|
166
|
-
app.get('/api/
|
|
167
|
-
const
|
|
168
|
-
if (!
|
|
169
|
-
res.status(400).json({ success: false, error: 'Invalid session ID' });
|
|
135
|
+
// ─── Historique ───────────────────────────────────────────────────────────────
|
|
136
|
+
app.get('/api/events', (req, res) => {
|
|
137
|
+
const siteId = typeof req.query.site === 'string' ? req.query.site : undefined;
|
|
138
|
+
if (siteId && !checkId(res, siteId, SITE_ID_RE, 'site'))
|
|
170
139
|
return;
|
|
140
|
+
const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : undefined;
|
|
141
|
+
void forward(res, 'EVENTS_LIST', { siteId, limit });
|
|
142
|
+
});
|
|
143
|
+
// ─── Connexion assistée ───────────────────────────────────────────────────────
|
|
144
|
+
app.post('/api/login', (req, res) => {
|
|
145
|
+
const siteId = String(req.body.siteId ?? '');
|
|
146
|
+
if (checkId(res, siteId, SITE_ID_RE, 'site'))
|
|
147
|
+
void forward(res, 'LOGIN_START', { siteId }, 30_000);
|
|
148
|
+
});
|
|
149
|
+
app.get('/api/login', (_req, res) => forward(res, 'LOGIN_STATUS'));
|
|
150
|
+
app.get('/api/login/screenshot', (_req, res) => sendJpeg(res, 'LOGIN_SCREENSHOT'));
|
|
151
|
+
app.post('/api/login/confirm', (_req, res) => forward(res, 'LOGIN_CONFIRM', undefined, 15_000));
|
|
152
|
+
app.post('/api/login/cancel', (_req, res) => forward(res, 'LOGIN_CANCEL', undefined, 15_000));
|
|
153
|
+
// ─── Sessions ─────────────────────────────────────────────────────────────────
|
|
154
|
+
app.get('/api/sessions', (_req, res) => forward(res, 'SESSION_LIST'));
|
|
155
|
+
app.delete('/api/sessions', (_req, res) => forward(res, 'SESSION_CLOSE_ALL', undefined, 15_000));
|
|
156
|
+
app.get('/api/sessions/:id/screenshot', (req, res) => {
|
|
157
|
+
if (checkId(res, req.params.id, SESSION_ID_RE, 'session')) {
|
|
158
|
+
void sendJpeg(res, 'SESSION_SCREENSHOT', { sessionId: req.params.id });
|
|
171
159
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
160
|
+
});
|
|
161
|
+
app.get('/api/sessions/:id/logs', (req, res) => {
|
|
162
|
+
if (checkId(res, req.params.id, SESSION_ID_RE, 'session')) {
|
|
163
|
+
void forward(res, 'SESSION_LOGS', { sessionId: req.params.id });
|
|
175
164
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
165
|
+
});
|
|
166
|
+
app.delete('/api/sessions/:id', (req, res) => {
|
|
167
|
+
if (checkId(res, req.params.id, SESSION_ID_RE, 'session')) {
|
|
168
|
+
void forward(res, 'SESSION_CLOSE', { sessionId: req.params.id });
|
|
179
169
|
}
|
|
180
170
|
});
|
|
181
171
|
async function startServer() {
|