@j3r3mcdev/oast-server 1.1.12 → 1.1.14

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 (106) hide show
  1. package/dist/listeners/api/api.routes.js +31 -5
  2. package/package.json +4 -1
  3. package/.env.example +0 -0
  4. package/.github/workflows/ci.yml +0 -29
  5. package/.github/workflows/publish.yml +0 -31
  6. package/image.png +0 -0
  7. package/jest.config.js +0 -14
  8. package/sadmin list shadows +0 -9
  9. package/src/api/controllers/__tests__/tasks.controller.test.ts +0 -74
  10. package/src/api/controllers/events.controller.ts +0 -10
  11. package/src/api/controllers/health.controller.ts +0 -7
  12. package/src/api/controllers/index.ts +0 -0
  13. package/src/api/controllers/tasks.controller.ts +0 -41
  14. package/src/api/dto/__tests__/create-task.dto.test.ts +0 -41
  15. package/src/api/dto/__tests__/filter-tasks.dto.test.ts +0 -35
  16. package/src/api/dto/create-task.dto.ts +0 -33
  17. package/src/api/dto/filter-tasks.dto.ts +0 -33
  18. package/src/api/services/__tests__/events.service.test.ts +0 -41
  19. package/src/api/services/__tests__/tasks.service.test.ts +0 -41
  20. package/src/api/services/events.service.ts +0 -17
  21. package/src/api/services/tasks.service.ts +0 -79
  22. package/src/api/sse/events.stream.ts +0 -90
  23. package/src/bootstrap.ts +0 -89
  24. package/src/config/constants.ts +0 -0
  25. package/src/config/env.ts +0 -0
  26. package/src/core/__tests__/core-router.test.ts +0 -30
  27. package/src/core/__tests__/core-server.test.ts +0 -44
  28. package/src/core/__tests__/event.normalizer.test.ts +0 -56
  29. package/src/core/__tests__/event.router.test.ts +0 -89
  30. package/src/core/__tests__/logger.test.ts +0 -32
  31. package/src/core/__tests__/storage-manager.test.ts +0 -74
  32. package/src/core/event.normalizer.ts +0 -167
  33. package/src/core/event.router.ts +0 -13
  34. package/src/core/http/__tests__/adapter-node.test.ts +0 -52
  35. package/src/core/http/__tests__/body-parser-multipart.test.ts +0 -41
  36. package/src/core/http/__tests__/body-parser-raw.test.ts +0 -28
  37. package/src/core/http/__tests__/body-parser-text.test.ts +0 -28
  38. package/src/core/http/__tests__/compile-path.test.ts +0 -39
  39. package/src/core/http/__tests__/middleware-pipeline.test.ts +0 -51
  40. package/src/core/http/__tests__/request.test.ts +0 -34
  41. package/src/core/http/__tests__/response.test.ts +0 -35
  42. package/src/core/http/__tests__/router-match.test.ts +0 -171
  43. package/src/core/http/adapter-node.ts +0 -51
  44. package/src/core/http/buildRequest.ts +0 -18
  45. package/src/core/http/compile-path.ts +0 -32
  46. package/src/core/http/errors.ts +0 -37
  47. package/src/core/http/http-server.ts +0 -52
  48. package/src/core/http/index.ts +0 -0
  49. package/src/core/http/main.ts +0 -0
  50. package/src/core/http/middleware.ts +0 -160
  51. package/src/core/http/request.ts +0 -55
  52. package/src/core/http/response.ts +0 -93
  53. package/src/core/http/router.ts +0 -138
  54. package/src/core/http/utils.ts +0 -0
  55. package/src/core/id-generator.ts +0 -8
  56. package/src/core/logger.ts +0 -113
  57. package/src/core/router.ts +0 -44
  58. package/src/core/server.ts +0 -85
  59. package/src/core/storage.ts +0 -64
  60. package/src/index.ts +0 -14
  61. package/src/listeners/api/__tests__/api.controller.test.ts +0 -116
  62. package/src/listeners/api/__tests__/api.extractor.test.ts +0 -46
  63. package/src/listeners/api/__tests__/api.listener.test.ts +0 -82
  64. package/src/listeners/api/__tests__/api.routes.test.ts +0 -155
  65. package/src/listeners/api/__tests__/api.sse.test.ts +0 -105
  66. package/src/listeners/api/api.controllers.ts +0 -67
  67. package/src/listeners/api/api.extractor.ts +0 -43
  68. package/src/listeners/api/api.listener.ts +0 -50
  69. package/src/listeners/api/api.routes.ts +0 -76
  70. package/src/listeners/api/api.sse.ts +0 -38
  71. package/src/listeners/dns/__tests__/dns.test.ts +0 -118
  72. package/src/listeners/dns/dns.extractor.ts +0 -14
  73. package/src/listeners/dns/dns.listener.ts +0 -61
  74. package/src/listeners/http/__tests__/http.extractor.test.ts +0 -59
  75. package/src/listeners/http/__tests__/http.listener.test.ts +0 -133
  76. package/src/listeners/http/http.extractor.ts +0 -15
  77. package/src/listeners/http/http.listener.ts +0 -110
  78. package/src/listeners/listener.interface.ts +0 -4
  79. package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +0 -69
  80. package/src/listeners/smtp/__tests__/smtp.listener.test.ts +0 -150
  81. package/src/listeners/smtp/smtp.extractor.ts +0 -18
  82. package/src/listeners/smtp/smtp.listener.ts +0 -78
  83. package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +0 -41
  84. package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +0 -87
  85. package/src/listeners/ssrf/ssrf.extractor.ts +0 -14
  86. package/src/listeners/ssrf/ssrf.listener.ts +0 -37
  87. package/src/listeners/tcp/tcp.extractor.ts +0 -16
  88. package/src/listeners/tcp/tcp.listener.ts +0 -61
  89. package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +0 -35
  90. package/src/listeners/webhook/__tests__/webhook.listener.test.ts +0 -122
  91. package/src/listeners/webhook/webhook.extractor.ts +0 -12
  92. package/src/listeners/webhook/webhook.listener.ts +0 -58
  93. package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +0 -33
  94. package/src/listeners/websocket/__tests__/websocket.listener.test.ts +0 -90
  95. package/src/listeners/websocket/websocket.extractor.ts +0 -11
  96. package/src/listeners/websocket/websocket.listener.ts +0 -40
  97. package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +0 -75
  98. package/src/storage-adapters/adapters/memory.storage.ts +0 -64
  99. package/src/storage-adapters/adapters/redis.storage.ts +0 -0
  100. package/src/storage-adapters/adapters/sqlite.storage.ts +0 -0
  101. package/src/storage-adapters/storage.interface.ts +0 -26
  102. package/src/types/event.types.ts +0 -166
  103. package/src/utils/token.ts +0 -0
  104. package/src-api.txt +0 -0
  105. package/src-architecture.txt +0 -0
  106. package/tsconfig.json +0 -20
@@ -1,64 +0,0 @@
1
- import { Storage } from "../storage-adapters/storage.interface";
2
- import { AnyNormalizedEvent } from "../types/event.types";
3
- import { Logger } from "./logger";
4
-
5
- export class StorageManager implements Storage {
6
- private events: AnyNormalizedEvent[] = [];
7
- private logger: Logger;
8
-
9
- constructor(options: { logger?: Logger } = {}) {
10
- this.logger = options.logger ?? new Logger({ context: "StorageManager" });
11
- }
12
-
13
- async save(event: AnyNormalizedEvent): Promise<void> {
14
- this.events.push(event);
15
- }
16
-
17
- async getEvent(id: string): Promise<AnyNormalizedEvent | null> {
18
- return this.events.find((e) => e.id === id) ?? null;
19
- }
20
-
21
- async listEvents(params: {
22
- type?: string;
23
- page?: number;
24
- limit?: number;
25
- }): Promise<AnyNormalizedEvent[]> {
26
- const { type, page = 1, limit = 50 } = params;
27
-
28
- let filtered = this.events;
29
- if (type) filtered = filtered.filter((e) => e.type === type);
30
-
31
- const start = (page - 1) * limit;
32
- return filtered.slice(start, start + limit);
33
- }
34
-
35
- async getAll(): Promise<AnyNormalizedEvent[]> {
36
- return this.events;
37
- }
38
-
39
- async deleteEvent(id: string): Promise<boolean> {
40
- const before = this.events.length;
41
- this.events = this.events.filter((e) => e.id !== id);
42
- return this.events.length !== before;
43
- }
44
-
45
- async clearEvents(): Promise<void> {
46
- this.events = [];
47
- }
48
-
49
- async getStats(): Promise<{
50
- total: number;
51
- byType: Record<string, number>;
52
- }> {
53
- const byType: Record<string, number> = {};
54
-
55
- for (const e of this.events) {
56
- byType[e.type] = (byType[e.type] ?? 0) + 1;
57
- }
58
-
59
- return {
60
- total: this.events.length,
61
- byType,
62
- };
63
- }
64
- }
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- export { ApiListener } from "./listeners/api/api.listener";
2
- export { HttpListener } from "./listeners/http/http.listener";
3
- export { DnsListener } from "./listeners/dns/dns.listener";
4
- export { SmtpListener } from "./listeners/smtp/smtp.listener";
5
- export { TcpListener } from "./listeners/tcp/tcp.listener";
6
-
7
- export { CoreServer } from "./core/server";
8
- export { CoreRouter } from "./core/router";
9
- export { StorageManager } from "./core/storage";
10
- export { Logger } from "./core/logger";
11
-
12
- export { EventNormalizer } from "./core/event.normalizer";
13
-
14
- export * from "./types/event.types";
@@ -1,116 +0,0 @@
1
- import { ApiController } from "../api.controllers";
2
- import { StorageManager } from "../../../core/storage";
3
- import { ServerResponse, IncomingMessage } from "http";
4
- import { Socket } from "net";
5
- import { describe, it, expect, beforeEach, jest } from "@jest/globals";
6
-
7
- // Mock ServerResponse
8
- function mockRes(): ServerResponse {
9
- const socket = new Socket();
10
- const req = new IncomingMessage(socket);
11
- const res = new ServerResponse(req);
12
-
13
- jest.spyOn(res, "writeHead");
14
- jest.spyOn(res, "end");
15
-
16
- return res;
17
- }
18
-
19
- describe("ApiController", () => {
20
- let storage: StorageManager;
21
-
22
- beforeEach(() => {
23
- // On utilise un vrai StorageManager en mémoire
24
- storage = new StorageManager();
25
- });
26
-
27
- it("listEvents renvoie la liste des events (vide par défaut)", async () => {
28
- const res = mockRes();
29
- const url = new URL("http://localhost/events?page=2&type=http");
30
-
31
- await ApiController.listEvents(url, res, storage);
32
-
33
- expect(storage.listEvents).toBeDefined();
34
-
35
- expect(res.writeHead).toHaveBeenCalledWith(200, {
36
- "Content-Type": "application/json",
37
- });
38
-
39
- const body = (res.end as jest.Mock).mock.calls[0][0] as string;
40
- const payload = JSON.parse(body);
41
- expect(payload.success).toBe(true);
42
- expect(payload.events).toEqual([]);
43
- });
44
-
45
- it("getEvent renvoie 404 si event absent", async () => {
46
- const res = mockRes();
47
-
48
- await ApiController.getEvent("unknown", res, storage);
49
-
50
- expect(res.writeHead).toHaveBeenCalledWith(404, {
51
- "Content-Type": "application/json",
52
- });
53
-
54
- const body = (res.end as jest.Mock).mock.calls[0][0] as string;
55
- const payload = JSON.parse(body);
56
- expect(payload.success).toBe(false);
57
- });
58
-
59
- it("getEvent renvoie l'event si présent", async () => {
60
- // On mocke juste getEvent pour ce test
61
- jest.spyOn(storage, "getEvent").mockResolvedValue({ id: "abc" } as any);
62
-
63
- const res = mockRes();
64
-
65
- await ApiController.getEvent("abc", res, storage);
66
-
67
- expect(res.writeHead).toHaveBeenCalledWith(200, {
68
- "Content-Type": "application/json",
69
- });
70
-
71
- const body = (res.end as jest.Mock).mock.calls[0][0] as string;
72
- const payload = JSON.parse(body);
73
- expect(payload.success).toBe(true);
74
- expect(payload.event.id).toBe("abc");
75
- });
76
-
77
- it("deleteAll supprime tous les events", async () => {
78
- const res = mockRes();
79
-
80
- await ApiController.deleteAll(res, storage);
81
-
82
- const body = (res.end as jest.Mock).mock.calls[0][0] as string;
83
- const payload = JSON.parse(body);
84
- expect(payload.success).toBe(true);
85
- });
86
-
87
- it("deleteOne supprime un event", async () => {
88
- const res = mockRes();
89
-
90
- await storage.save({
91
- id: "xyz",
92
- type: "http",
93
- timestamp: Date.now(),
94
- sourceIp: "127.0.0.1",
95
- request: { method: "GET", path: "/", headers: {}, query: {}, body: "" },
96
- });
97
-
98
- await ApiController.deleteOne("xyz", res, storage);
99
-
100
- const raw = String((res.end as jest.Mock).mock.calls[0][0]);
101
- const payload = JSON.parse(raw);
102
-
103
- expect(payload.success).toBe(true);
104
- });
105
-
106
- it("stats renvoie les stats", async () => {
107
- const res = mockRes();
108
-
109
- await ApiController.stats(res, storage);
110
-
111
- const body = (res.end as jest.Mock).mock.calls[0][0] as string;
112
- const payload = JSON.parse(body);
113
- expect(payload.success).toBe(true);
114
- expect(payload.stats).toEqual({ total: 0, byType: {} });
115
- });
116
- });
@@ -1,46 +0,0 @@
1
- import { ApiExtractor } from "../api.extractor";
2
- import { IncomingMessage } from "http";
3
- import { Socket } from "net";
4
- import { describe, it, expect } from "@jest/globals";
5
-
6
- function mockReq(url: string, method = "GET", body?: any): IncomingMessage {
7
- const socket = new Socket();
8
- const req = new IncomingMessage(socket);
9
-
10
- req.url = url;
11
- req.method = method;
12
- req.headers = { host: "localhost" };
13
-
14
- if (body !== undefined) {
15
- const json = JSON.stringify(body);
16
- process.nextTick(() => {
17
- req.emit("data", json);
18
- req.emit("end");
19
- });
20
- } else {
21
- process.nextTick(() => req.emit("end"));
22
- }
23
-
24
- return req;
25
- }
26
-
27
- describe("ApiExtractor", () => {
28
- it("extrait un RawEvent GET", async () => {
29
- const req = mockReq("/events?page=2&type=http");
30
-
31
- const event = await ApiExtractor.extract(req);
32
-
33
- expect(event.method).toBe("GET");
34
- expect(event.path).toBe("/events");
35
- expect(event.query).toEqual({ page: "2", type: "http" });
36
- });
37
-
38
- it("extrait un RawEvent POST avec body JSON", async () => {
39
- const req = mockReq("/events", "POST", { hello: "world" });
40
-
41
- const event = await ApiExtractor.extract(req);
42
-
43
- expect(event.method).toBe("POST");
44
- expect(event.body).toEqual({ hello: "world" });
45
- });
46
- });
@@ -1,82 +0,0 @@
1
- import http from "http";
2
- import { ApiListener } from "../api.listener";
3
- import { StorageManager } from "../../../core/storage";
4
- import { Logger } from "../../../core/logger";
5
- import { describe, test, beforeEach, afterEach, expect } from "@jest/globals";
6
- import { ApiController } from "../api.controllers";
7
-
8
- describe("ApiListener", () => {
9
- let storage: StorageManager;
10
- let api: ApiListener;
11
-
12
- beforeEach(async () => {
13
- storage = new StorageManager();
14
-
15
- api = new ApiListener(storage, {
16
- port: 9999,
17
- logger: new Logger({ context: "ApiTest" }),
18
- });
19
-
20
- await api.start();
21
- });
22
-
23
- afterEach(async () => {
24
- await api.stop();
25
- });
26
-
27
- test("GET /events returns empty list", async () => {
28
- const res = await fetch("http://localhost:9999/events");
29
- const json = await res.json();
30
-
31
- expect(json.success).toBe(true);
32
- expect(json.events).toEqual([]);
33
- });
34
-
35
- test("GET /stats returns correct structure", async () => {
36
- const res = await fetch("http://localhost:9999/stats");
37
- const json = await res.json();
38
-
39
- expect(json.success).toBe(true);
40
- expect(json.stats.total).toBe(0);
41
- expect(json.stats.byType).toEqual({});
42
- });
43
-
44
- test("GET /events/:id returns 404 when not found", async () => {
45
- const res = await fetch("http://localhost:9999/events/unknown");
46
- const json = await res.json();
47
-
48
- expect(res.status).toBe(404);
49
- expect(json.success).toBe(false);
50
- });
51
-
52
- test("DELETE /events clears all events", async () => {
53
- // On ajoute un event dans le storage
54
- await storage.save({
55
- id: "1",
56
- type: "http",
57
- timestamp: Date.now(),
58
- sourceIp: "127.0.0.1",
59
- request: {
60
- method: "GET",
61
- path: "/",
62
- headers: {},
63
- query: {},
64
- body: "",
65
- },
66
- });
67
-
68
- let events = await storage.listEvents({});
69
- expect(events.length).toBe(1);
70
-
71
- // On appelle l'API réelle
72
- const res = await fetch("http://localhost:9999/events", {
73
- method: "DELETE",
74
- });
75
-
76
- const json = await res.json();
77
- expect(json.success).toBe(true);
78
-
79
- events = await storage.listEvents({});
80
- expect(events.length).toBe(0);
81
- });
82
- });
@@ -1,155 +0,0 @@
1
- import { handleApiRequest } from "../api.routes";
2
- import { StorageManager } from "../../../core/storage";
3
- import { ApiSse } from "../api.sse";
4
- import { ApiController } from "../api.controllers";
5
- import { Logger } from "../../../core/logger";
6
- import { IncomingMessage, ServerResponse } from "http";
7
- import { Socket } from "net";
8
- import { describe, it, expect, beforeEach, jest } from "@jest/globals";
9
-
10
- // Helpers pour mocker req/res
11
- function mockReq(method: string, url: string): IncomingMessage {
12
- const socket = new Socket();
13
- const req = new IncomingMessage(socket);
14
- req.method = method;
15
- req.url = url;
16
- req.headers = { host: "localhost" };
17
- return req;
18
- }
19
-
20
- function mockRes(): ServerResponse {
21
- const socket = new Socket();
22
- const req = new IncomingMessage(socket);
23
- const res = new ServerResponse(req);
24
-
25
- jest.spyOn(res, "writeHead");
26
- jest.spyOn(res, "end");
27
-
28
- return res;
29
- }
30
-
31
- describe("handleApiRequest", () => {
32
- let storage: StorageManager;
33
- let sse: ApiSse;
34
- let logger: Logger;
35
-
36
- beforeEach(() => {
37
- storage = new StorageManager();
38
- sse = new ApiSse(new Logger({ context: "SSETest" }));
39
- logger = new Logger({ context: "ApiTest" });
40
-
41
- jest.spyOn(ApiController, "listEvents").mockResolvedValue(undefined);
42
- jest.spyOn(ApiController, "getEvent").mockResolvedValue(undefined);
43
- jest.spyOn(ApiController, "deleteAll").mockResolvedValue(undefined);
44
- jest.spyOn(ApiController, "deleteOne").mockResolvedValue(undefined);
45
- jest.spyOn(ApiController, "stats").mockResolvedValue(undefined);
46
-
47
- jest.spyOn(sse, "handle").mockReturnValue(undefined as any);
48
- });
49
-
50
- it("route GET /events → ApiController.listEvents", async () => {
51
- const req = mockReq("GET", "/events");
52
- const res = mockRes();
53
-
54
- await expect(
55
- handleApiRequest(req, res, storage, sse, logger),
56
- ).resolves.toBeUndefined();
57
-
58
- expect(ApiController.listEvents).toHaveBeenCalled();
59
- });
60
-
61
- it("route GET /events/:id → ApiController.getEvent", async () => {
62
- const req = mockReq("GET", "/events/123");
63
- const res = mockRes();
64
-
65
- await expect(
66
- handleApiRequest(req, res, storage, sse, logger),
67
- ).resolves.toBeUndefined();
68
-
69
- expect(ApiController.getEvent).toHaveBeenCalledWith("123", res, storage);
70
- });
71
-
72
- it("route DELETE /events → ApiController.deleteAll", async () => {
73
- const req = mockReq("DELETE", "/events");
74
- const res = mockRes();
75
-
76
- await expect(
77
- handleApiRequest(req, res, storage, sse, logger),
78
- ).resolves.toBeUndefined();
79
-
80
- expect(ApiController.deleteAll).toHaveBeenCalled();
81
- });
82
-
83
- it("route DELETE /events/:id → ApiController.deleteOne", async () => {
84
- const req = mockReq("DELETE", "/events/abc");
85
- const res = mockRes();
86
-
87
- await expect(
88
- handleApiRequest(req, res, storage, sse, logger),
89
- ).resolves.toBeUndefined();
90
-
91
- expect(ApiController.deleteOne).toHaveBeenCalledWith("abc", res, storage);
92
- });
93
-
94
- it("route GET /stats → ApiController.stats", async () => {
95
- const req = mockReq("GET", "/stats");
96
- const res = mockRes();
97
-
98
- await expect(
99
- handleApiRequest(req, res, storage, sse, logger),
100
- ).resolves.toBeUndefined();
101
-
102
- expect(ApiController.stats).toHaveBeenCalled();
103
- });
104
-
105
- it("route GET /events/stream → SSE", async () => {
106
- const req = mockReq("GET", "/events/stream");
107
- const res = mockRes();
108
-
109
- await expect(
110
- handleApiRequest(req, res, storage, sse, logger),
111
- ).resolves.toBeUndefined();
112
-
113
- expect(sse.handle).toHaveBeenCalledWith(res);
114
- });
115
-
116
- it("404 → renvoie Not found", async () => {
117
- const req = mockReq("GET", "/unknown");
118
- const res = mockRes();
119
-
120
- await expect(
121
- handleApiRequest(req, res, storage, sse, logger),
122
- ).resolves.toBeUndefined();
123
-
124
- expect(res.writeHead).toHaveBeenCalledWith(404, {
125
- "Content-Type": "application/json",
126
- });
127
-
128
- const raw = String((res.end as jest.Mock).mock.calls[0][0]);
129
- const body = JSON.parse(raw);
130
- expect(body.success).toBe(false);
131
- });
132
-
133
- it("500 → renvoie Internal Server Error", async () => {
134
- const req = mockReq("GET", "/events");
135
-
136
- jest
137
- .spyOn(ApiController, "listEvents")
138
- .mockRejectedValue(new Error("Boom"));
139
-
140
- const res = mockRes();
141
-
142
- await expect(
143
- handleApiRequest(req, res, storage, sse, logger),
144
- ).resolves.toBeUndefined();
145
-
146
- expect(res.writeHead).toHaveBeenCalledWith(500, {
147
- "Content-Type": "application/json",
148
- });
149
-
150
- const raw = String((res.end as jest.Mock).mock.calls[0][0]);
151
- const body = JSON.parse(raw);
152
- expect(body.success).toBe(false);
153
- expect(body.error).toBe("Boom");
154
- });
155
- });
@@ -1,105 +0,0 @@
1
- import { ApiSse } from "../api.sse";
2
- import { Logger } from "../../../core/logger";
3
- import { ServerResponse, IncomingMessage } from "http";
4
- import { Socket } from "net";
5
- import { describe, it, expect, jest } from "@jest/globals";
6
-
7
- function mockRes(): ServerResponse {
8
- const socket = new Socket();
9
- const req = new IncomingMessage(socket);
10
- const res = new ServerResponse(req);
11
- return res;
12
- }
13
-
14
- describe("ApiSse", () => {
15
- it("connecte un client SSE", () => {
16
- const sse = new ApiSse(new Logger({ context: "Test" }));
17
- const res = mockRes();
18
-
19
- // On espionne writeHead et write
20
- const writeHeadSpy = jest.spyOn(res, "writeHead");
21
- const writeSpy = jest.spyOn(res, "write");
22
-
23
- sse.handle(res);
24
-
25
- expect(writeHeadSpy).toHaveBeenCalledWith(200, {
26
- "Content-Type": "text/event-stream",
27
- "Cache-Control": "no-cache",
28
- Connection: "keep-alive",
29
- });
30
-
31
- expect(writeSpy).toHaveBeenCalled();
32
- });
33
-
34
- it("broadcast un event à tous les clients", () => {
35
- const sse = new ApiSse(new Logger({ context: "Test" }));
36
-
37
- const res1 = mockRes();
38
- const res2 = mockRes();
39
-
40
- const spy1 = jest.spyOn(res1, "write");
41
- const spy2 = jest.spyOn(res2, "write");
42
-
43
- sse.handle(res1);
44
- sse.handle(res2);
45
-
46
- sse.broadcast({ id: "123", type: "http" });
47
-
48
- expect(spy1).toHaveBeenCalled();
49
- expect(spy2).toHaveBeenCalled();
50
-
51
- const payload = spy1.mock.calls[spy1.mock.calls.length - 1][0];
52
- expect(payload).toContain("event: event");
53
- expect(payload).toContain('"id":"123"');
54
- });
55
-
56
- it("supprime un client à la fermeture", () => {
57
- const sse = new ApiSse(new Logger({ context: "Test" }));
58
- const res = mockRes();
59
-
60
- sse.handle(res);
61
-
62
- expect((sse as any).clients.size).toBe(1);
63
-
64
- res.emit("close");
65
-
66
- expect((sse as any).clients.size).toBe(0);
67
- });
68
-
69
- it("envoie un event SSE avec le bon format", () => {
70
- const sse = new ApiSse(new Logger({ context: "Test" }));
71
- const res = mockRes();
72
-
73
- const spy = jest.spyOn(res, "write");
74
-
75
- sse.handle(res);
76
- sse.broadcast({ id: "abc", type: "dns" });
77
-
78
- const payload = spy.mock.calls[spy.mock.calls.length - 1][0];
79
-
80
- expect(payload).toContain("event: event");
81
- expect(payload).toContain("data:");
82
- expect(payload).toContain('"id":"abc"');
83
- expect(payload.endsWith("\n\n")).toBe(true);
84
- });
85
-
86
- it("broadcast ne plante pas s'il n'y a aucun client", () => {
87
- const sse = new ApiSse(new Logger({ context: "Test" }));
88
-
89
- expect(() => {
90
- sse.broadcast({ id: "x", type: "http" });
91
- }).not.toThrow();
92
- });
93
-
94
- it("ne duplique pas un client déjà enregistré", () => {
95
- const sse = new ApiSse(new Logger({ context: "Test" }));
96
- const res = mockRes();
97
-
98
- sse.handle(res);
99
-
100
- // simulate duplicate registration attempt
101
- (sse as any).clients.add(res);
102
-
103
- expect((sse as any).clients.size).toBe(1);
104
- });
105
- });
@@ -1,67 +0,0 @@
1
- import { ServerResponse } from "http";
2
- import { StorageManager } from "../../core/storage";
3
-
4
- export class ApiController {
5
- static async listEvents(
6
- url: URL,
7
- res: ServerResponse,
8
- storage: StorageManager,
9
- ): Promise<void> {
10
- const type = url.searchParams.get("type") ?? undefined;
11
- const page = Number(url.searchParams.get("page") ?? 1);
12
- const limit = Number(url.searchParams.get("limit") ?? 50);
13
-
14
- const events = await storage.listEvents({ type, page, limit });
15
-
16
- res.writeHead(200, { "Content-Type": "application/json" });
17
- res.end(JSON.stringify({ success: true, events }));
18
- }
19
-
20
- static async getEvent(
21
- id: string,
22
- res: ServerResponse,
23
- storage: StorageManager,
24
- ): Promise<void> {
25
- const event = await storage.getEvent(id);
26
-
27
- if (!event) {
28
- res.writeHead(404, { "Content-Type": "application/json" });
29
- res.end(JSON.stringify({ success: false, error: "Not found" }));
30
- return;
31
- }
32
-
33
- res.writeHead(200, { "Content-Type": "application/json" });
34
- res.end(JSON.stringify({ success: true, event }));
35
- }
36
-
37
- static async deleteAll(
38
- res: ServerResponse,
39
- storage: StorageManager,
40
- ): Promise<void> {
41
- await storage.clearEvents();
42
-
43
- res.writeHead(200, { "Content-Type": "application/json" });
44
- res.end(JSON.stringify({ success: true }));
45
- }
46
-
47
- static async deleteOne(
48
- id: string,
49
- res: ServerResponse,
50
- storage: StorageManager,
51
- ): Promise<void> {
52
- const ok = await storage.deleteEvent(id);
53
-
54
- res.writeHead(200, { "Content-Type": "application/json" });
55
- res.end(JSON.stringify({ success: ok }));
56
- }
57
-
58
- static async stats(
59
- res: ServerResponse,
60
- storage: StorageManager,
61
- ): Promise<void> {
62
- const stats = await storage.getStats();
63
-
64
- res.writeHead(200, { "Content-Type": "application/json" });
65
- res.end(JSON.stringify({ success: true, stats }));
66
- }
67
- }
@@ -1,43 +0,0 @@
1
- import { IncomingMessage } from "http";
2
- import { RawEvent } from "../../types/event.types";
3
-
4
- export class ApiExtractor {
5
- static async extract(req: IncomingMessage): Promise<RawEvent> {
6
- const ip = req.socket.remoteAddress ?? "";
7
- const method = req.method ?? "";
8
- const url = new URL(req.url ?? "", `http://${req.headers.host}`);
9
- const path = url.pathname;
10
-
11
- const query: Record<string, string> = {};
12
- for (const [key, value] of url.searchParams.entries()) {
13
- query[key] = value;
14
- }
15
-
16
- // Lecture du body (si POST/PUT/PATCH)
17
- let body: any = null;
18
-
19
- if (method !== "GET" && method !== "HEAD") {
20
- body = await new Promise((resolve) => {
21
- let data = "";
22
- req.on("data", (chunk) => (data += chunk));
23
- req.on("end", () => {
24
- try {
25
- resolve(JSON.parse(data));
26
- } catch {
27
- resolve(data);
28
- }
29
- });
30
- });
31
- }
32
-
33
- return {
34
- ip,
35
- method,
36
- path,
37
- headers: req.headers,
38
- query,
39
- body,
40
- raw: req,
41
- };
42
- }
43
- }