@j3r3mcdev/oast-server 1.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/.env.example +0 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish.yml +31 -0
- package/README.md +192 -0
- package/dist/api/controllers/__tests__/tasks.controller.test.js +61 -0
- package/dist/api/controllers/events.controller.js +13 -0
- package/dist/api/controllers/health.controller.js +11 -0
- package/dist/api/controllers/index.js +1 -0
- package/dist/api/controllers/tasks.controller.js +35 -0
- package/dist/api/dto/__tests__/create-task.dto.test.js +33 -0
- package/dist/api/dto/__tests__/filter-tasks.dto.test.js +28 -0
- package/dist/api/dto/create-task.dto.js +26 -0
- package/dist/api/dto/filter-tasks.dto.js +27 -0
- package/dist/api/services/__tests__/events.service.test.js +25 -0
- package/dist/api/services/__tests__/tasks.service.test.js +25 -0
- package/dist/api/services/events.service.js +18 -0
- package/dist/api/services/tasks.service.js +52 -0
- package/dist/api/sse/events.stream.js +63 -0
- package/dist/config/constants.js +1 -0
- package/dist/config/env.js +1 -0
- package/dist/core/__tests__/core-router.test.js +26 -0
- package/dist/core/__tests__/core-server.test.js +39 -0
- package/dist/core/__tests__/event.normalizer.test.js +50 -0
- package/dist/core/__tests__/event.router.test.js +66 -0
- package/dist/core/__tests__/logger.test.js +26 -0
- package/dist/core/__tests__/storage-manager.test.js +57 -0
- package/dist/core/event.normalizer.js +126 -0
- package/dist/core/event.router.js +15 -0
- package/dist/core/http/__tests__/adapter-node.test.js +74 -0
- package/dist/core/http/__tests__/body-parser-multipart.test.js +35 -0
- package/dist/core/http/__tests__/body-parser-raw.test.js +25 -0
- package/dist/core/http/__tests__/body-parser-text.test.js +25 -0
- package/dist/core/http/__tests__/compile-path.test.js +33 -0
- package/dist/core/http/__tests__/middleware-pipeline.test.js +39 -0
- package/dist/core/http/__tests__/request.test.js +32 -0
- package/dist/core/http/__tests__/response.test.js +26 -0
- package/dist/core/http/__tests__/router-match.test.js +117 -0
- package/dist/core/http/adapter-node.js +44 -0
- package/dist/core/http/buildRequest.js +16 -0
- package/dist/core/http/compile-path.js +30 -0
- package/dist/core/http/errors.js +35 -0
- package/dist/core/http/http-server.js +48 -0
- package/dist/core/http/index.js +1 -0
- package/dist/core/http/main.js +1 -0
- package/dist/core/http/middleware.js +133 -0
- package/dist/core/http/request.js +22 -0
- package/dist/core/http/response.js +74 -0
- package/dist/core/http/router.js +111 -0
- package/dist/core/http/utils.js +1 -0
- package/dist/core/id-generator.js +14 -0
- package/dist/core/logger.js +81 -0
- package/dist/core/router.js +30 -0
- package/dist/core/server.js +70 -0
- package/dist/core/storage.js +46 -0
- package/dist/index.js +76 -0
- package/dist/listeners/api/__tests__/api.controller.test.js +88 -0
- package/dist/listeners/api/__tests__/api.extractor.test.js +39 -0
- package/dist/listeners/api/__tests__/api.listener.test.js +66 -0
- package/dist/listeners/api/__tests__/api.routes.test.js +105 -0
- package/dist/listeners/api/__tests__/api.sse.test.js +78 -0
- package/dist/listeners/api/api.controllers.js +39 -0
- package/dist/listeners/api/api.extractor.js +41 -0
- package/dist/listeners/api/api.listener.js +37 -0
- package/dist/listeners/api/api.routes.js +59 -0
- package/dist/listeners/api/api.sse.js +35 -0
- package/dist/listeners/dns/__tests__/dns.test.js +89 -0
- package/dist/listeners/dns/dns.extractor.js +17 -0
- package/dist/listeners/dns/dns.listener.js +48 -0
- package/dist/listeners/http/__tests__/http.extractor.test.js +52 -0
- package/dist/listeners/http/__tests__/http.listener.test.js +106 -0
- package/dist/listeners/http/http.extractor.js +18 -0
- package/dist/listeners/http/http.listener.js +91 -0
- package/dist/listeners/listener.interface.js +2 -0
- package/dist/listeners/smtp/__tests__/smtp.extractor.test.js +62 -0
- package/dist/listeners/smtp/__tests__/smtp.listener.test.js +129 -0
- package/dist/listeners/smtp/smtp.extractor.js +21 -0
- package/dist/listeners/smtp/smtp.listener.js +53 -0
- package/dist/listeners/ssrf/__tests__/ssrf.extractor.test.js +37 -0
- package/dist/listeners/ssrf/__tests__/ssrf.listener.test.js +79 -0
- package/dist/listeners/ssrf/ssrf.extractor.js +17 -0
- package/dist/listeners/ssrf/ssrf.listener.js +35 -0
- package/dist/listeners/tcp/tcp.extractor.js +18 -0
- package/dist/listeners/tcp/tcp.listener.js +47 -0
- package/dist/listeners/webhook/__tests__/webhook.extractor.test.js +30 -0
- package/dist/listeners/webhook/__tests__/webhook.listener.test.js +96 -0
- package/dist/listeners/webhook/webhook.extractor.js +15 -0
- package/dist/listeners/webhook/webhook.listener.js +51 -0
- package/dist/listeners/websocket/__tests__/websocket.extractor.test.js +29 -0
- package/dist/listeners/websocket/__tests__/websocket.listener.test.js +73 -0
- package/dist/listeners/websocket/websocket.extractor.js +14 -0
- package/dist/listeners/websocket/websocket.listener.js +33 -0
- package/dist/storage-adapters/adapters/__tests__/memory.storage.test.js +64 -0
- package/dist/storage-adapters/adapters/memory.storage.js +48 -0
- package/dist/storage-adapters/adapters/redis.storage.js +1 -0
- package/dist/storage-adapters/adapters/sqlite.storage.js +1 -0
- package/dist/storage-adapters/storage.interface.js +2 -0
- package/dist/types/event.types.js +2 -0
- package/dist/utils/token.js +1 -0
- package/image.png +0 -0
- package/jest.config.js +11 -0
- package/package.json +45 -0
- package/sadmin list shadows +9 -0
- package/src/api/controllers/__tests__/tasks.controller.test.ts +74 -0
- package/src/api/controllers/events.controller.ts +10 -0
- package/src/api/controllers/health.controller.ts +7 -0
- package/src/api/controllers/index.ts +0 -0
- package/src/api/controllers/tasks.controller.ts +41 -0
- package/src/api/dto/__tests__/create-task.dto.test.ts +41 -0
- package/src/api/dto/__tests__/filter-tasks.dto.test.ts +35 -0
- package/src/api/dto/create-task.dto.ts +33 -0
- package/src/api/dto/filter-tasks.dto.ts +33 -0
- package/src/api/services/__tests__/events.service.test.ts +41 -0
- package/src/api/services/__tests__/tasks.service.test.ts +41 -0
- package/src/api/services/events.service.ts +17 -0
- package/src/api/services/tasks.service.ts +79 -0
- package/src/api/sse/events.stream.ts +90 -0
- package/src/config/constants.ts +0 -0
- package/src/config/env.ts +0 -0
- package/src/core/__tests__/core-router.test.ts +30 -0
- package/src/core/__tests__/core-server.test.ts +44 -0
- package/src/core/__tests__/event.normalizer.test.ts +56 -0
- package/src/core/__tests__/event.router.test.ts +89 -0
- package/src/core/__tests__/logger.test.ts +32 -0
- package/src/core/__tests__/storage-manager.test.ts +74 -0
- package/src/core/event.normalizer.ts +147 -0
- package/src/core/event.router.ts +13 -0
- package/src/core/http/__tests__/adapter-node.test.ts +52 -0
- package/src/core/http/__tests__/body-parser-multipart.test.ts +41 -0
- package/src/core/http/__tests__/body-parser-raw.test.ts +28 -0
- package/src/core/http/__tests__/body-parser-text.test.ts +28 -0
- package/src/core/http/__tests__/compile-path.test.ts +39 -0
- package/src/core/http/__tests__/middleware-pipeline.test.ts +51 -0
- package/src/core/http/__tests__/request.test.ts +34 -0
- package/src/core/http/__tests__/response.test.ts +35 -0
- package/src/core/http/__tests__/router-match.test.ts +171 -0
- package/src/core/http/adapter-node.ts +51 -0
- package/src/core/http/buildRequest.ts +18 -0
- package/src/core/http/compile-path.ts +32 -0
- package/src/core/http/errors.ts +37 -0
- package/src/core/http/http-server.ts +52 -0
- package/src/core/http/index.ts +0 -0
- package/src/core/http/main.ts +0 -0
- package/src/core/http/middleware.ts +160 -0
- package/src/core/http/request.ts +55 -0
- package/src/core/http/response.ts +93 -0
- package/src/core/http/router.ts +138 -0
- package/src/core/http/utils.ts +0 -0
- package/src/core/id-generator.ts +8 -0
- package/src/core/logger.ts +113 -0
- package/src/core/router.ts +44 -0
- package/src/core/server.ts +85 -0
- package/src/core/storage.ts +64 -0
- package/src/index.ts +89 -0
- package/src/listeners/api/__tests__/api.controller.test.ts +116 -0
- package/src/listeners/api/__tests__/api.extractor.test.ts +46 -0
- package/src/listeners/api/__tests__/api.listener.test.ts +82 -0
- package/src/listeners/api/__tests__/api.routes.test.ts +155 -0
- package/src/listeners/api/__tests__/api.sse.test.ts +105 -0
- package/src/listeners/api/api.controllers.ts +67 -0
- package/src/listeners/api/api.extractor.ts +43 -0
- package/src/listeners/api/api.listener.ts +50 -0
- package/src/listeners/api/api.routes.ts +76 -0
- package/src/listeners/api/api.sse.ts +38 -0
- package/src/listeners/dns/__tests__/dns.test.ts +118 -0
- package/src/listeners/dns/dns.extractor.ts +14 -0
- package/src/listeners/dns/dns.listener.ts +61 -0
- package/src/listeners/http/__tests__/http.extractor.test.ts +59 -0
- package/src/listeners/http/__tests__/http.listener.test.ts +133 -0
- package/src/listeners/http/http.extractor.ts +15 -0
- package/src/listeners/http/http.listener.ts +110 -0
- package/src/listeners/listener.interface.ts +4 -0
- package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +69 -0
- package/src/listeners/smtp/__tests__/smtp.listener.test.ts +150 -0
- package/src/listeners/smtp/smtp.extractor.ts +18 -0
- package/src/listeners/smtp/smtp.listener.ts +60 -0
- package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +41 -0
- package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +98 -0
- package/src/listeners/ssrf/ssrf.extractor.ts +14 -0
- package/src/listeners/ssrf/ssrf.listener.ts +37 -0
- package/src/listeners/tcp/tcp.extractor.ts +16 -0
- package/src/listeners/tcp/tcp.listener.ts +61 -0
- package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +35 -0
- package/src/listeners/webhook/__tests__/webhook.listener.test.ts +122 -0
- package/src/listeners/webhook/webhook.extractor.ts +12 -0
- package/src/listeners/webhook/webhook.listener.ts +58 -0
- package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +33 -0
- package/src/listeners/websocket/__tests__/websocket.listener.test.ts +90 -0
- package/src/listeners/websocket/websocket.extractor.ts +11 -0
- package/src/listeners/websocket/websocket.listener.ts +40 -0
- package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +75 -0
- package/src/storage-adapters/adapters/memory.storage.ts +64 -0
- package/src/storage-adapters/adapters/redis.storage.ts +0 -0
- package/src/storage-adapters/adapters/sqlite.storage.ts +0 -0
- package/src/storage-adapters/storage.interface.ts +26 -0
- package/src/types/event.types.ts +147 -0
- package/src/utils/token.ts +0 -0
- package/src-api.txt +0 -0
- package/src-architecture.txt +0 -0
- package/tsconfig.json +15 -0
package/.env.example
ADDED
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Node
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 20
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm install
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: npm test
|
|
27
|
+
|
|
28
|
+
- name: Build project
|
|
29
|
+
run: npm run build
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout repository
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 20
|
|
20
|
+
registry-url: "https://registry.npmjs.org"
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Build project
|
|
26
|
+
run: npm run build
|
|
27
|
+
|
|
28
|
+
- name: Publish package
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
31
|
+
run: npm publish --access public
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# OAST Server — Multi‑Protocol Out‑of‑Band Interaction Framework
|
|
2
|
+
|
|
3
|
+
Un serveur OAST modulaire, extensible et agnostique, conçu pour capturer, normaliser et diffuser des interactions _out‑of‑band_ provenant de multiples canaux : HTTP, DNS, SMTP, SSRF, Webhook, WebSocket, TCP, API REST et SSE.
|
|
4
|
+
|
|
5
|
+
Ce framework fournit une architecture listener‑based robuste, unifiée et testée, idéale pour les outils de diagnostic, scanners de vulnérabilités, plateformes d’audit ou systèmes de détection avancés.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Caractéristiques principales
|
|
10
|
+
|
|
11
|
+
- **Architecture modulaire par listeners**
|
|
12
|
+
Chaque protocole est isolé dans son propre module (`http`, `dns`, `smtp`, `ssrf`, `webhook`, `websocket`, `tcp`, `api`).
|
|
13
|
+
|
|
14
|
+
- **Extractors dédiés**
|
|
15
|
+
Chaque listener dispose d’un extractor responsable de la normalisation des données brutes.
|
|
16
|
+
|
|
17
|
+
- **API REST intégrée**
|
|
18
|
+
Permet de récupérer les events, les tasks, les métadonnées et l’état du serveur.
|
|
19
|
+
|
|
20
|
+
- **SSE (Server‑Sent Events)**
|
|
21
|
+
Diffusion en temps réel des interactions capturées.
|
|
22
|
+
|
|
23
|
+
- **Storage agnostique**
|
|
24
|
+
Implémentation par défaut en mémoire, extensible vers Redis, SQLite, PostgreSQL, etc.
|
|
25
|
+
|
|
26
|
+
- **Suite de tests complète**
|
|
27
|
+
118 tests unitaires couvrant extractors, listeners, API, SSE et services.
|
|
28
|
+
|
|
29
|
+
- **Aucune dépendance lourde**
|
|
30
|
+
Pas d’Express, pas de framework web : un cœur minimaliste, rapide et portable.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📦 Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @j3r3mcdev/oast-server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 🚀 Démarrage rapide
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { ApiListener, HttpListener, DnsListener } from "@j3r3mcdev/oast-server";
|
|
44
|
+
|
|
45
|
+
const api = new ApiListener({ port: 8080 });
|
|
46
|
+
const http = new HttpListener({ port: 8000 });
|
|
47
|
+
const dns = new DnsListener({ port: 5353 });
|
|
48
|
+
|
|
49
|
+
api.start();
|
|
50
|
+
http.start();
|
|
51
|
+
dns.start();
|
|
52
|
+
|
|
53
|
+
console.log("OAST server running");
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 📡 Protocoles supportés
|
|
57
|
+
|
|
58
|
+
| Protocole | Listener | Description |
|
|
59
|
+
| ------------- | ------------------- | --------------------------------------- |
|
|
60
|
+
| **HTTP** | `HttpListener` | Capture des requêtes HTTP entrantes |
|
|
61
|
+
| **DNS** | `DnsListener` | Résolution DNS OAST (A, AAAA, TXT…) |
|
|
62
|
+
| **SMTP** | `SmtpListener` | Réception d’emails OAST |
|
|
63
|
+
| **SSRF** | `SsrfListener` | Détection d’interactions SSRF |
|
|
64
|
+
| **Webhook** | `WebhookListener` | Réception d’appels webhook |
|
|
65
|
+
| **WebSocket** | `WebSocketListener` | Connexions WebSocket entrantes |
|
|
66
|
+
| **TCP** | `TcpListener` | Connexions TCP brutes |
|
|
67
|
+
| **API + SSE** | `ApiListener` | API REST + diffusion temps réel via SSE |
|
|
68
|
+
|
|
69
|
+
## 🧩 Architecture
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
Chaque listener contient :
|
|
74
|
+
|
|
75
|
+
- `*.listener.ts` → serveur du protocole
|
|
76
|
+
- `*.extractor.ts` → normalisation des données
|
|
77
|
+
- `__tests__/` → tests unitaires
|
|
78
|
+
|
|
79
|
+
## 🧨 Exemple : Listener HTTP
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { HttpListener } from "@j3r3mcdev/oast-server";
|
|
83
|
+
|
|
84
|
+
const http = new HttpListener({
|
|
85
|
+
port: 8000,
|
|
86
|
+
onEvent: (event) => {
|
|
87
|
+
console.log("HTTP event:", event);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
http.start();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# 🧨 **Exemple : Listener DNS**
|
|
97
|
+
|
|
98
|
+
## 🧨 Exemple : Listener DNS
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { DnsListener } from "@j3r3mcdev/oast-server";
|
|
102
|
+
|
|
103
|
+
const dns = new DnsListener({
|
|
104
|
+
port: 5353,
|
|
105
|
+
onEvent: (event) => {
|
|
106
|
+
console.log("DNS event:", event);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
dns.start();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
# 🧨 **Exemple : SSE (temps réel)**
|
|
116
|
+
|
|
117
|
+
```md
|
|
118
|
+
## 🧨 Exemple : SSE (temps réel)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const stream = new EventSource(
|
|
123
|
+
"http://localhost:8080/stream?channels=http,dns",
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
stream.onmessage = (msg) => {
|
|
127
|
+
const event = JSON.parse(msg.data);
|
|
128
|
+
console.log("Event reçu :", event);
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# 🧨 **Exemple : API REST**
|
|
135
|
+
|
|
136
|
+
## 🧨 Exemple : API REST
|
|
137
|
+
|
|
138
|
+
### Récupérer les events
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
curl http://localhost:8080/events
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Créer une tâche
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
curl -X POST http://localhost:8080/tasks \
|
|
148
|
+
-H "Content-Type: application/json" \
|
|
149
|
+
-d '{"type":"scan","payload":{"url":"http://example.com"}}'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
# 🧱 **Storage** (Markdown PRO)
|
|
155
|
+
|
|
156
|
+
## 🧱 Storage
|
|
157
|
+
|
|
158
|
+
Par défaut : **in‑memory**
|
|
159
|
+
|
|
160
|
+
Vous pouvez implémenter votre propre storage :
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
export interface StorageAdapter {
|
|
164
|
+
save(event: OastEvent): Promise<void>;
|
|
165
|
+
list(filters: any): Promise<OastEvent[]>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class MyStorage implements StorageAdapter {
|
|
169
|
+
private events = [];
|
|
170
|
+
async save(e) {
|
|
171
|
+
this.events.push(e);
|
|
172
|
+
}
|
|
173
|
+
async list() {
|
|
174
|
+
return this.events;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
# 🧪 **Tests** (Markdown PRO)
|
|
182
|
+
|
|
183
|
+
## 🧪 Tests
|
|
184
|
+
|
|
185
|
+
- **37 suites**
|
|
186
|
+
- **118 tests**
|
|
187
|
+
- **100% verts**
|
|
188
|
+
- Tests d’intégration réservés à l’outil d’audit (non inclus dans la lib)
|
|
189
|
+
|
|
190
|
+
## 📘 Licence
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const tasks_controller_1 = require("../tasks.controller");
|
|
5
|
+
(0, globals_1.describe)("TasksController", () => {
|
|
6
|
+
let service;
|
|
7
|
+
let controller;
|
|
8
|
+
(0, globals_1.beforeEach)(() => {
|
|
9
|
+
service = {
|
|
10
|
+
create: globals_1.jest.fn(),
|
|
11
|
+
get: globals_1.jest.fn(),
|
|
12
|
+
list: globals_1.jest.fn(),
|
|
13
|
+
cancel: globals_1.jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
controller = new tasks_controller_1.TasksController(service);
|
|
16
|
+
});
|
|
17
|
+
(0, globals_1.it)("crée une tâche", async () => {
|
|
18
|
+
const fakeTask = {
|
|
19
|
+
id: "123",
|
|
20
|
+
type: "x",
|
|
21
|
+
payload: {},
|
|
22
|
+
priority: "normal",
|
|
23
|
+
metadata: {},
|
|
24
|
+
status: "pending",
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
updatedAt: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
service.create.mockReturnValue(fakeTask);
|
|
29
|
+
const req = { body: { type: "x", payload: {} } };
|
|
30
|
+
const result = await controller.create({ req, res: {}, params: {} });
|
|
31
|
+
(0, globals_1.expect)(service.create).toHaveBeenCalled();
|
|
32
|
+
(0, globals_1.expect)(result).toEqual({
|
|
33
|
+
status: 201,
|
|
34
|
+
body: fakeTask,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
(0, globals_1.it)("retourne 404 si tâche absente", async () => {
|
|
38
|
+
service.get.mockReturnValue(undefined);
|
|
39
|
+
const result = await controller.getOne({
|
|
40
|
+
req: {},
|
|
41
|
+
res: {},
|
|
42
|
+
params: { id: "123" },
|
|
43
|
+
});
|
|
44
|
+
(0, globals_1.expect)(result).toEqual({
|
|
45
|
+
status: 404,
|
|
46
|
+
body: { error: "Task not found" },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
(0, globals_1.it)("annule une tâche", async () => {
|
|
50
|
+
service.cancel.mockReturnValue(true);
|
|
51
|
+
const result = await controller.cancel({
|
|
52
|
+
req: {},
|
|
53
|
+
res: {},
|
|
54
|
+
params: { id: "123" },
|
|
55
|
+
});
|
|
56
|
+
(0, globals_1.expect)(result).toEqual({
|
|
57
|
+
status: 200,
|
|
58
|
+
body: { cancelled: true },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventsController = void 0;
|
|
4
|
+
class EventsController {
|
|
5
|
+
constructor(events) {
|
|
6
|
+
this.events = events;
|
|
7
|
+
this.stream = async ({ res, query }) => {
|
|
8
|
+
const channels = query.channels?.split(",") ?? [];
|
|
9
|
+
return this.events.connect(res, channels);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.EventsController = EventsController;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealthController = void 0;
|
|
4
|
+
class HealthController {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.check = (req, res) => {
|
|
7
|
+
res.json({ status: "ok", timestamp: Date.now() });
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.HealthController = HealthController;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TasksController = void 0;
|
|
4
|
+
const create_task_dto_1 = require("../dto/create-task.dto");
|
|
5
|
+
const filter_tasks_dto_1 = require("../dto/filter-tasks.dto");
|
|
6
|
+
class TasksController {
|
|
7
|
+
constructor(tasks) {
|
|
8
|
+
this.tasks = tasks;
|
|
9
|
+
this.list = async ({ req }) => {
|
|
10
|
+
const dto = filter_tasks_dto_1.FilterTasksDtoValidator.validate(req.query);
|
|
11
|
+
const result = await this.tasks.list(dto);
|
|
12
|
+
return { status: 200, body: result };
|
|
13
|
+
};
|
|
14
|
+
this.create = async ({ req }) => {
|
|
15
|
+
const dto = create_task_dto_1.CreateTaskDtoValidator.validate(req.body);
|
|
16
|
+
const task = await this.tasks.create(dto);
|
|
17
|
+
return { status: 201, body: task };
|
|
18
|
+
};
|
|
19
|
+
this.getOne = async ({ params }) => {
|
|
20
|
+
const task = await this.tasks.get(params.id);
|
|
21
|
+
if (!task) {
|
|
22
|
+
return { status: 404, body: { error: "Task not found" } };
|
|
23
|
+
}
|
|
24
|
+
return { status: 200, body: task };
|
|
25
|
+
};
|
|
26
|
+
this.cancel = async ({ params }) => {
|
|
27
|
+
const ok = await this.tasks.cancel(params.id);
|
|
28
|
+
if (!ok) {
|
|
29
|
+
return { status: 404, body: { error: "Task not found" } };
|
|
30
|
+
}
|
|
31
|
+
return { status: 200, body: { cancelled: true } };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.TasksController = TasksController;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const create_task_dto_1 = require("../create-task.dto");
|
|
5
|
+
(0, globals_1.describe)("CreateTaskDtoValidator", () => {
|
|
6
|
+
(0, globals_1.it)("valide un DTO correct", () => {
|
|
7
|
+
const dto = create_task_dto_1.CreateTaskDtoValidator.validate({
|
|
8
|
+
type: "crawl:api",
|
|
9
|
+
payload: { url: "https://example.com" },
|
|
10
|
+
priority: "high",
|
|
11
|
+
});
|
|
12
|
+
(0, globals_1.expect)(dto).toEqual({
|
|
13
|
+
type: "crawl:api",
|
|
14
|
+
payload: { url: "https://example.com" },
|
|
15
|
+
priority: "high",
|
|
16
|
+
metadata: {},
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
(0, globals_1.it)("applique les valeurs par défaut", () => {
|
|
20
|
+
const dto = create_task_dto_1.CreateTaskDtoValidator.validate({
|
|
21
|
+
type: "ping",
|
|
22
|
+
payload: {},
|
|
23
|
+
});
|
|
24
|
+
(0, globals_1.expect)(dto.priority).toBe("normal");
|
|
25
|
+
(0, globals_1.expect)(dto.metadata).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
(0, globals_1.it)("rejette un type invalide", () => {
|
|
28
|
+
(0, globals_1.expect)(() => create_task_dto_1.CreateTaskDtoValidator.validate({ payload: {} })).toThrow("Invalid type");
|
|
29
|
+
});
|
|
30
|
+
(0, globals_1.it)("rejette un payload invalide", () => {
|
|
31
|
+
(0, globals_1.expect)(() => create_task_dto_1.CreateTaskDtoValidator.validate({ type: "x", payload: null })).toThrow("Invalid payload");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const filter_tasks_dto_1 = require("../filter-tasks.dto");
|
|
5
|
+
(0, globals_1.describe)("FilterTasksDtoValidator", () => {
|
|
6
|
+
(0, globals_1.it)("valide un query correct", () => {
|
|
7
|
+
const dto = filter_tasks_dto_1.FilterTasksDtoValidator.validate({
|
|
8
|
+
status: "running",
|
|
9
|
+
limit: "10",
|
|
10
|
+
type: "crawl:api",
|
|
11
|
+
});
|
|
12
|
+
(0, globals_1.expect)(dto).toEqual({
|
|
13
|
+
status: "running",
|
|
14
|
+
limit: 10,
|
|
15
|
+
type: "crawl:api",
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
(0, globals_1.it)("rejette un status invalide", () => {
|
|
19
|
+
(0, globals_1.expect)(() => filter_tasks_dto_1.FilterTasksDtoValidator.validate({ status: "unknown" })).toThrow("Invalid status");
|
|
20
|
+
});
|
|
21
|
+
(0, globals_1.it)("rejette un limit invalide", () => {
|
|
22
|
+
(0, globals_1.expect)(() => filter_tasks_dto_1.FilterTasksDtoValidator.validate({ limit: "-5" })).toThrow("Invalid limit");
|
|
23
|
+
});
|
|
24
|
+
(0, globals_1.it)("accepte un query vide", () => {
|
|
25
|
+
const dto = filter_tasks_dto_1.FilterTasksDtoValidator.validate({});
|
|
26
|
+
(0, globals_1.expect)(dto).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CreateTaskDtoValidator = void 0;
|
|
4
|
+
class CreateTaskDtoValidator {
|
|
5
|
+
static validate(input) {
|
|
6
|
+
if (!input || typeof input !== "object") {
|
|
7
|
+
throw new Error("Invalid body: expected object");
|
|
8
|
+
}
|
|
9
|
+
if (!input.type || typeof input.type !== "string") {
|
|
10
|
+
throw new Error("Invalid type: expected non-empty string");
|
|
11
|
+
}
|
|
12
|
+
if (!input.payload || typeof input.payload !== "object") {
|
|
13
|
+
throw new Error("Invalid payload: expected object");
|
|
14
|
+
}
|
|
15
|
+
if (input.priority && !["low", "normal", "high"].includes(input.priority)) {
|
|
16
|
+
throw new Error("Invalid priority: must be low, normal or high");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
type: input.type,
|
|
20
|
+
payload: input.payload,
|
|
21
|
+
priority: input.priority ?? "normal",
|
|
22
|
+
metadata: input.metadata ?? {},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.CreateTaskDtoValidator = CreateTaskDtoValidator;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FilterTasksDtoValidator = void 0;
|
|
4
|
+
class FilterTasksDtoValidator {
|
|
5
|
+
static validate(query) {
|
|
6
|
+
const dto = {};
|
|
7
|
+
if (query.status) {
|
|
8
|
+
const allowed = ["pending", "running", "completed", "failed"];
|
|
9
|
+
if (!allowed.includes(query.status)) {
|
|
10
|
+
throw new Error("Invalid status");
|
|
11
|
+
}
|
|
12
|
+
dto.status = query.status;
|
|
13
|
+
}
|
|
14
|
+
if (query.limit) {
|
|
15
|
+
const n = Number(query.limit);
|
|
16
|
+
if (isNaN(n) || n <= 0) {
|
|
17
|
+
throw new Error("Invalid limit");
|
|
18
|
+
}
|
|
19
|
+
dto.limit = n;
|
|
20
|
+
}
|
|
21
|
+
if (query.type) {
|
|
22
|
+
dto.type = String(query.type);
|
|
23
|
+
}
|
|
24
|
+
return dto;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.FilterTasksDtoValidator = FilterTasksDtoValidator;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const tasks_service_1 = require("../tasks.service");
|
|
5
|
+
const events_service_1 = require("../events.service");
|
|
6
|
+
const events_stream_1 = require("../../sse/events.stream");
|
|
7
|
+
(0, globals_1.describe)("TasksService", () => {
|
|
8
|
+
(0, globals_1.it)("émet un événement lors de la création d'une tâche", () => {
|
|
9
|
+
const stream = new events_stream_1.EventStreamManager({ heartbeatInterval: 999999 });
|
|
10
|
+
const events = new events_service_1.EventsService(stream);
|
|
11
|
+
const spy = globals_1.jest.spyOn(stream, "broadcast");
|
|
12
|
+
const service = new tasks_service_1.TasksService(events);
|
|
13
|
+
const task = service.create({ type: "x", payload: {} });
|
|
14
|
+
(0, globals_1.expect)(spy).toHaveBeenCalledWith("tasks", "task.created", globals_1.expect.objectContaining({ id: task.id }));
|
|
15
|
+
});
|
|
16
|
+
(0, globals_1.it)("émet un événement lors de l'annulation d'une tâche", () => {
|
|
17
|
+
const stream = new events_stream_1.EventStreamManager({ heartbeatInterval: 999999 });
|
|
18
|
+
const events = new events_service_1.EventsService(stream);
|
|
19
|
+
const spy = globals_1.jest.spyOn(stream, "broadcast");
|
|
20
|
+
const service = new tasks_service_1.TasksService(events);
|
|
21
|
+
const task = service.create({ type: "x", payload: {} });
|
|
22
|
+
service.cancel(task.id);
|
|
23
|
+
(0, globals_1.expect)(spy).toHaveBeenCalledWith("tasks", "task.cancelled", globals_1.expect.objectContaining({ id: task.id }));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const tasks_service_1 = require("../tasks.service");
|
|
5
|
+
const events_service_1 = require("../events.service");
|
|
6
|
+
const events_stream_1 = require("../../sse/events.stream");
|
|
7
|
+
(0, globals_1.describe)("TasksService", () => {
|
|
8
|
+
(0, globals_1.it)("émet un événement lors de la création d'une tâche", () => {
|
|
9
|
+
const stream = new events_stream_1.EventStreamManager({ heartbeatInterval: 999999 });
|
|
10
|
+
const events = new events_service_1.EventsService(stream);
|
|
11
|
+
const spy = globals_1.jest.spyOn(stream, "broadcast");
|
|
12
|
+
const service = new tasks_service_1.TasksService(events);
|
|
13
|
+
const task = service.create({ type: "x", payload: {} });
|
|
14
|
+
(0, globals_1.expect)(spy).toHaveBeenCalledWith("tasks", "task.created", globals_1.expect.objectContaining({ id: task.id }));
|
|
15
|
+
});
|
|
16
|
+
(0, globals_1.it)("émet un événement lors de l'annulation d'une tâche", () => {
|
|
17
|
+
const stream = new events_stream_1.EventStreamManager({ heartbeatInterval: 999999 });
|
|
18
|
+
const events = new events_service_1.EventsService(stream);
|
|
19
|
+
const spy = globals_1.jest.spyOn(stream, "broadcast");
|
|
20
|
+
const service = new tasks_service_1.TasksService(events);
|
|
21
|
+
const task = service.create({ type: "x", payload: {} });
|
|
22
|
+
service.cancel(task.id);
|
|
23
|
+
(0, globals_1.expect)(spy).toHaveBeenCalledWith("tasks", "task.cancelled", globals_1.expect.objectContaining({ id: task.id }));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventsService = void 0;
|
|
4
|
+
class EventsService {
|
|
5
|
+
constructor(stream) {
|
|
6
|
+
this.stream = stream;
|
|
7
|
+
}
|
|
8
|
+
connect(res, channels = []) {
|
|
9
|
+
return this.stream.addClient(res, channels);
|
|
10
|
+
}
|
|
11
|
+
emit(channel, event, data) {
|
|
12
|
+
this.stream.broadcast(channel, event, data);
|
|
13
|
+
}
|
|
14
|
+
emitAll(event, data) {
|
|
15
|
+
this.stream.broadcastAll(event, data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.EventsService = EventsService;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TasksService = void 0;
|
|
4
|
+
class TasksService {
|
|
5
|
+
constructor(events) {
|
|
6
|
+
this.tasks = new Map();
|
|
7
|
+
this.events = events;
|
|
8
|
+
}
|
|
9
|
+
create(dto) {
|
|
10
|
+
const id = crypto.randomUUID();
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const task = {
|
|
13
|
+
id,
|
|
14
|
+
type: dto.type,
|
|
15
|
+
payload: dto.payload,
|
|
16
|
+
priority: dto.priority ?? "normal",
|
|
17
|
+
metadata: dto.metadata ?? {},
|
|
18
|
+
status: "pending",
|
|
19
|
+
createdAt: now,
|
|
20
|
+
updatedAt: now,
|
|
21
|
+
};
|
|
22
|
+
this.tasks.set(id, task);
|
|
23
|
+
this.events.emit("tasks", "task.created", task);
|
|
24
|
+
return task;
|
|
25
|
+
}
|
|
26
|
+
get(id) {
|
|
27
|
+
return this.tasks.get(id);
|
|
28
|
+
}
|
|
29
|
+
list(filters) {
|
|
30
|
+
let results = Array.from(this.tasks.values());
|
|
31
|
+
if (filters.status) {
|
|
32
|
+
results = results.filter((t) => t.status === filters.status);
|
|
33
|
+
}
|
|
34
|
+
if (filters.type) {
|
|
35
|
+
results = results.filter((t) => t.type === filters.type);
|
|
36
|
+
}
|
|
37
|
+
if (filters.limit) {
|
|
38
|
+
results = results.slice(0, filters.limit);
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
cancel(id) {
|
|
43
|
+
const task = this.tasks.get(id);
|
|
44
|
+
if (!task)
|
|
45
|
+
return false;
|
|
46
|
+
task.status = "failed";
|
|
47
|
+
task.updatedAt = Date.now();
|
|
48
|
+
this.events.emit("tasks", "task.cancelled", task);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.TasksService = TasksService;
|