@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.
Files changed (199) hide show
  1. package/.env.example +0 -0
  2. package/.github/workflows/ci.yml +29 -0
  3. package/.github/workflows/publish.yml +31 -0
  4. package/README.md +192 -0
  5. package/dist/api/controllers/__tests__/tasks.controller.test.js +61 -0
  6. package/dist/api/controllers/events.controller.js +13 -0
  7. package/dist/api/controllers/health.controller.js +11 -0
  8. package/dist/api/controllers/index.js +1 -0
  9. package/dist/api/controllers/tasks.controller.js +35 -0
  10. package/dist/api/dto/__tests__/create-task.dto.test.js +33 -0
  11. package/dist/api/dto/__tests__/filter-tasks.dto.test.js +28 -0
  12. package/dist/api/dto/create-task.dto.js +26 -0
  13. package/dist/api/dto/filter-tasks.dto.js +27 -0
  14. package/dist/api/services/__tests__/events.service.test.js +25 -0
  15. package/dist/api/services/__tests__/tasks.service.test.js +25 -0
  16. package/dist/api/services/events.service.js +18 -0
  17. package/dist/api/services/tasks.service.js +52 -0
  18. package/dist/api/sse/events.stream.js +63 -0
  19. package/dist/config/constants.js +1 -0
  20. package/dist/config/env.js +1 -0
  21. package/dist/core/__tests__/core-router.test.js +26 -0
  22. package/dist/core/__tests__/core-server.test.js +39 -0
  23. package/dist/core/__tests__/event.normalizer.test.js +50 -0
  24. package/dist/core/__tests__/event.router.test.js +66 -0
  25. package/dist/core/__tests__/logger.test.js +26 -0
  26. package/dist/core/__tests__/storage-manager.test.js +57 -0
  27. package/dist/core/event.normalizer.js +126 -0
  28. package/dist/core/event.router.js +15 -0
  29. package/dist/core/http/__tests__/adapter-node.test.js +74 -0
  30. package/dist/core/http/__tests__/body-parser-multipart.test.js +35 -0
  31. package/dist/core/http/__tests__/body-parser-raw.test.js +25 -0
  32. package/dist/core/http/__tests__/body-parser-text.test.js +25 -0
  33. package/dist/core/http/__tests__/compile-path.test.js +33 -0
  34. package/dist/core/http/__tests__/middleware-pipeline.test.js +39 -0
  35. package/dist/core/http/__tests__/request.test.js +32 -0
  36. package/dist/core/http/__tests__/response.test.js +26 -0
  37. package/dist/core/http/__tests__/router-match.test.js +117 -0
  38. package/dist/core/http/adapter-node.js +44 -0
  39. package/dist/core/http/buildRequest.js +16 -0
  40. package/dist/core/http/compile-path.js +30 -0
  41. package/dist/core/http/errors.js +35 -0
  42. package/dist/core/http/http-server.js +48 -0
  43. package/dist/core/http/index.js +1 -0
  44. package/dist/core/http/main.js +1 -0
  45. package/dist/core/http/middleware.js +133 -0
  46. package/dist/core/http/request.js +22 -0
  47. package/dist/core/http/response.js +74 -0
  48. package/dist/core/http/router.js +111 -0
  49. package/dist/core/http/utils.js +1 -0
  50. package/dist/core/id-generator.js +14 -0
  51. package/dist/core/logger.js +81 -0
  52. package/dist/core/router.js +30 -0
  53. package/dist/core/server.js +70 -0
  54. package/dist/core/storage.js +46 -0
  55. package/dist/index.js +76 -0
  56. package/dist/listeners/api/__tests__/api.controller.test.js +88 -0
  57. package/dist/listeners/api/__tests__/api.extractor.test.js +39 -0
  58. package/dist/listeners/api/__tests__/api.listener.test.js +66 -0
  59. package/dist/listeners/api/__tests__/api.routes.test.js +105 -0
  60. package/dist/listeners/api/__tests__/api.sse.test.js +78 -0
  61. package/dist/listeners/api/api.controllers.js +39 -0
  62. package/dist/listeners/api/api.extractor.js +41 -0
  63. package/dist/listeners/api/api.listener.js +37 -0
  64. package/dist/listeners/api/api.routes.js +59 -0
  65. package/dist/listeners/api/api.sse.js +35 -0
  66. package/dist/listeners/dns/__tests__/dns.test.js +89 -0
  67. package/dist/listeners/dns/dns.extractor.js +17 -0
  68. package/dist/listeners/dns/dns.listener.js +48 -0
  69. package/dist/listeners/http/__tests__/http.extractor.test.js +52 -0
  70. package/dist/listeners/http/__tests__/http.listener.test.js +106 -0
  71. package/dist/listeners/http/http.extractor.js +18 -0
  72. package/dist/listeners/http/http.listener.js +91 -0
  73. package/dist/listeners/listener.interface.js +2 -0
  74. package/dist/listeners/smtp/__tests__/smtp.extractor.test.js +62 -0
  75. package/dist/listeners/smtp/__tests__/smtp.listener.test.js +129 -0
  76. package/dist/listeners/smtp/smtp.extractor.js +21 -0
  77. package/dist/listeners/smtp/smtp.listener.js +53 -0
  78. package/dist/listeners/ssrf/__tests__/ssrf.extractor.test.js +37 -0
  79. package/dist/listeners/ssrf/__tests__/ssrf.listener.test.js +79 -0
  80. package/dist/listeners/ssrf/ssrf.extractor.js +17 -0
  81. package/dist/listeners/ssrf/ssrf.listener.js +35 -0
  82. package/dist/listeners/tcp/tcp.extractor.js +18 -0
  83. package/dist/listeners/tcp/tcp.listener.js +47 -0
  84. package/dist/listeners/webhook/__tests__/webhook.extractor.test.js +30 -0
  85. package/dist/listeners/webhook/__tests__/webhook.listener.test.js +96 -0
  86. package/dist/listeners/webhook/webhook.extractor.js +15 -0
  87. package/dist/listeners/webhook/webhook.listener.js +51 -0
  88. package/dist/listeners/websocket/__tests__/websocket.extractor.test.js +29 -0
  89. package/dist/listeners/websocket/__tests__/websocket.listener.test.js +73 -0
  90. package/dist/listeners/websocket/websocket.extractor.js +14 -0
  91. package/dist/listeners/websocket/websocket.listener.js +33 -0
  92. package/dist/storage-adapters/adapters/__tests__/memory.storage.test.js +64 -0
  93. package/dist/storage-adapters/adapters/memory.storage.js +48 -0
  94. package/dist/storage-adapters/adapters/redis.storage.js +1 -0
  95. package/dist/storage-adapters/adapters/sqlite.storage.js +1 -0
  96. package/dist/storage-adapters/storage.interface.js +2 -0
  97. package/dist/types/event.types.js +2 -0
  98. package/dist/utils/token.js +1 -0
  99. package/image.png +0 -0
  100. package/jest.config.js +11 -0
  101. package/package.json +45 -0
  102. package/sadmin list shadows +9 -0
  103. package/src/api/controllers/__tests__/tasks.controller.test.ts +74 -0
  104. package/src/api/controllers/events.controller.ts +10 -0
  105. package/src/api/controllers/health.controller.ts +7 -0
  106. package/src/api/controllers/index.ts +0 -0
  107. package/src/api/controllers/tasks.controller.ts +41 -0
  108. package/src/api/dto/__tests__/create-task.dto.test.ts +41 -0
  109. package/src/api/dto/__tests__/filter-tasks.dto.test.ts +35 -0
  110. package/src/api/dto/create-task.dto.ts +33 -0
  111. package/src/api/dto/filter-tasks.dto.ts +33 -0
  112. package/src/api/services/__tests__/events.service.test.ts +41 -0
  113. package/src/api/services/__tests__/tasks.service.test.ts +41 -0
  114. package/src/api/services/events.service.ts +17 -0
  115. package/src/api/services/tasks.service.ts +79 -0
  116. package/src/api/sse/events.stream.ts +90 -0
  117. package/src/config/constants.ts +0 -0
  118. package/src/config/env.ts +0 -0
  119. package/src/core/__tests__/core-router.test.ts +30 -0
  120. package/src/core/__tests__/core-server.test.ts +44 -0
  121. package/src/core/__tests__/event.normalizer.test.ts +56 -0
  122. package/src/core/__tests__/event.router.test.ts +89 -0
  123. package/src/core/__tests__/logger.test.ts +32 -0
  124. package/src/core/__tests__/storage-manager.test.ts +74 -0
  125. package/src/core/event.normalizer.ts +147 -0
  126. package/src/core/event.router.ts +13 -0
  127. package/src/core/http/__tests__/adapter-node.test.ts +52 -0
  128. package/src/core/http/__tests__/body-parser-multipart.test.ts +41 -0
  129. package/src/core/http/__tests__/body-parser-raw.test.ts +28 -0
  130. package/src/core/http/__tests__/body-parser-text.test.ts +28 -0
  131. package/src/core/http/__tests__/compile-path.test.ts +39 -0
  132. package/src/core/http/__tests__/middleware-pipeline.test.ts +51 -0
  133. package/src/core/http/__tests__/request.test.ts +34 -0
  134. package/src/core/http/__tests__/response.test.ts +35 -0
  135. package/src/core/http/__tests__/router-match.test.ts +171 -0
  136. package/src/core/http/adapter-node.ts +51 -0
  137. package/src/core/http/buildRequest.ts +18 -0
  138. package/src/core/http/compile-path.ts +32 -0
  139. package/src/core/http/errors.ts +37 -0
  140. package/src/core/http/http-server.ts +52 -0
  141. package/src/core/http/index.ts +0 -0
  142. package/src/core/http/main.ts +0 -0
  143. package/src/core/http/middleware.ts +160 -0
  144. package/src/core/http/request.ts +55 -0
  145. package/src/core/http/response.ts +93 -0
  146. package/src/core/http/router.ts +138 -0
  147. package/src/core/http/utils.ts +0 -0
  148. package/src/core/id-generator.ts +8 -0
  149. package/src/core/logger.ts +113 -0
  150. package/src/core/router.ts +44 -0
  151. package/src/core/server.ts +85 -0
  152. package/src/core/storage.ts +64 -0
  153. package/src/index.ts +89 -0
  154. package/src/listeners/api/__tests__/api.controller.test.ts +116 -0
  155. package/src/listeners/api/__tests__/api.extractor.test.ts +46 -0
  156. package/src/listeners/api/__tests__/api.listener.test.ts +82 -0
  157. package/src/listeners/api/__tests__/api.routes.test.ts +155 -0
  158. package/src/listeners/api/__tests__/api.sse.test.ts +105 -0
  159. package/src/listeners/api/api.controllers.ts +67 -0
  160. package/src/listeners/api/api.extractor.ts +43 -0
  161. package/src/listeners/api/api.listener.ts +50 -0
  162. package/src/listeners/api/api.routes.ts +76 -0
  163. package/src/listeners/api/api.sse.ts +38 -0
  164. package/src/listeners/dns/__tests__/dns.test.ts +118 -0
  165. package/src/listeners/dns/dns.extractor.ts +14 -0
  166. package/src/listeners/dns/dns.listener.ts +61 -0
  167. package/src/listeners/http/__tests__/http.extractor.test.ts +59 -0
  168. package/src/listeners/http/__tests__/http.listener.test.ts +133 -0
  169. package/src/listeners/http/http.extractor.ts +15 -0
  170. package/src/listeners/http/http.listener.ts +110 -0
  171. package/src/listeners/listener.interface.ts +4 -0
  172. package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +69 -0
  173. package/src/listeners/smtp/__tests__/smtp.listener.test.ts +150 -0
  174. package/src/listeners/smtp/smtp.extractor.ts +18 -0
  175. package/src/listeners/smtp/smtp.listener.ts +60 -0
  176. package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +41 -0
  177. package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +98 -0
  178. package/src/listeners/ssrf/ssrf.extractor.ts +14 -0
  179. package/src/listeners/ssrf/ssrf.listener.ts +37 -0
  180. package/src/listeners/tcp/tcp.extractor.ts +16 -0
  181. package/src/listeners/tcp/tcp.listener.ts +61 -0
  182. package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +35 -0
  183. package/src/listeners/webhook/__tests__/webhook.listener.test.ts +122 -0
  184. package/src/listeners/webhook/webhook.extractor.ts +12 -0
  185. package/src/listeners/webhook/webhook.listener.ts +58 -0
  186. package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +33 -0
  187. package/src/listeners/websocket/__tests__/websocket.listener.test.ts +90 -0
  188. package/src/listeners/websocket/websocket.extractor.ts +11 -0
  189. package/src/listeners/websocket/websocket.listener.ts +40 -0
  190. package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +75 -0
  191. package/src/storage-adapters/adapters/memory.storage.ts +64 -0
  192. package/src/storage-adapters/adapters/redis.storage.ts +0 -0
  193. package/src/storage-adapters/adapters/sqlite.storage.ts +0 -0
  194. package/src/storage-adapters/storage.interface.ts +26 -0
  195. package/src/types/event.types.ts +147 -0
  196. package/src/utils/token.ts +0 -0
  197. package/src-api.txt +0 -0
  198. package/src-architecture.txt +0 -0
  199. package/tsconfig.json +15 -0
@@ -0,0 +1,89 @@
1
+ import {
2
+ AnyNormalizedEvent,
3
+ NormalizedHttpEvent,
4
+ } from "../../types/event.types";
5
+ import {
6
+ StorageListParams,
7
+ Storage,
8
+ } from "../../storage-adapters/storage.interface";
9
+ import { EventRouter } from "../event.router";
10
+ import { EventNormalizer } from "../event.normalizer";
11
+ import { describe, it, expect, jest } from "@jest/globals";
12
+
13
+ class MockStorage implements Storage {
14
+ save = jest.fn<(event: AnyNormalizedEvent) => Promise<void>>();
15
+ getEvent = jest.fn<(id: string) => Promise<AnyNormalizedEvent | null>>();
16
+ listEvents =
17
+ jest.fn<(params: StorageListParams) => Promise<AnyNormalizedEvent[]>>();
18
+ deleteEvent = jest.fn<(id: string) => Promise<boolean>>();
19
+ clearEvents = jest.fn<() => Promise<void>>();
20
+ getStats =
21
+ jest.fn<() => Promise<{ total: number; byType: Record<string, number> }>>();
22
+ getAll = jest.fn<() => Promise<AnyNormalizedEvent[]>>();
23
+ }
24
+
25
+ describe("EventRouter", () => {
26
+ it("enregistre un événement HTTP normalisé", async () => {
27
+ const mockStorage = new MockStorage();
28
+
29
+ const fakeEvent: NormalizedHttpEvent = {
30
+ id: "123",
31
+ type: "http",
32
+ timestamp: 111,
33
+ sourceIp: "1.1.1.1",
34
+ request: {
35
+ method: "GET",
36
+ path: "/",
37
+ headers: {},
38
+ query: {},
39
+ body: {},
40
+ },
41
+ };
42
+
43
+ jest.spyOn(EventNormalizer, "normalizeHttp").mockReturnValue(fakeEvent);
44
+
45
+ const router = new EventRouter(mockStorage);
46
+
47
+ const req = {
48
+ ip: "1.1.1.1",
49
+ method: "GET",
50
+ path: "/",
51
+ headers: {},
52
+ query: {},
53
+ body: {},
54
+ raw: {},
55
+ };
56
+
57
+ const result = await router.handleHttp(req);
58
+
59
+ expect(EventNormalizer.normalizeHttp).toHaveBeenCalledWith(req);
60
+ expect(mockStorage.save).toHaveBeenCalledWith(fakeEvent);
61
+ expect(result).toEqual(fakeEvent);
62
+ });
63
+
64
+ it("propagate une erreur si normalizeHttp throw", async () => {
65
+ const mockStorage = new MockStorage();
66
+
67
+ jest.spyOn(EventNormalizer, "normalizeHttp").mockImplementation(() => {
68
+ throw new Error("Boom");
69
+ });
70
+
71
+ const router = new EventRouter(mockStorage);
72
+
73
+ await expect(router.handleHttp({} as any)).rejects.toThrow("Boom");
74
+ expect(mockStorage.save).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("propagate une erreur si storage.save throw", async () => {
78
+ const mockStorage = new MockStorage();
79
+
80
+ const fakeEvent = { id: "x", type: "http" } as any;
81
+
82
+ jest.spyOn(EventNormalizer, "normalizeHttp").mockReturnValue(fakeEvent);
83
+ mockStorage.save.mockRejectedValue(new Error("DB error"));
84
+
85
+ const router = new EventRouter(mockStorage);
86
+
87
+ await expect(router.handleHttp({} as any)).rejects.toThrow("DB error");
88
+ });
89
+ });
@@ -0,0 +1,32 @@
1
+ import { Logger, LogEntry } from "../../core/logger";
2
+ import { describe, it, expect, jest } from "@jest/globals";
3
+
4
+ describe("Logger", () => {
5
+ it("appelle les hooks", () => {
6
+ const hook = jest.fn<(entry: LogEntry) => void>();
7
+ const logger = new Logger({ hooks: [hook], enabled: true });
8
+
9
+ logger.info("hello");
10
+
11
+ expect(hook).toHaveBeenCalled();
12
+ expect(hook.mock.calls[0][0].message).toBe("hello");
13
+ });
14
+
15
+ it("respecte le niveau de logs", () => {
16
+ const hook = jest.fn<(entry: LogEntry) => void>();
17
+ const logger = new Logger({ level: "warn", hooks: [hook] });
18
+
19
+ logger.info("should not log");
20
+ logger.error("should log");
21
+
22
+ expect(hook).toHaveBeenCalledTimes(1);
23
+ expect(hook.mock.calls[0][0].level).toBe("error");
24
+ });
25
+
26
+ it("crée un logger enfant avec contexte", () => {
27
+ const logger = new Logger({ context: "root" });
28
+ const child = logger.withContext("child");
29
+
30
+ expect(child).not.toBe(logger);
31
+ });
32
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import { StorageManager } from "../storage"; // si ton fichier s'appelle storage-manager.ts
3
+ import { NormalizedHttpEvent } from "../../types/event.types";
4
+
5
+ function makeHttpEvent(id: string): NormalizedHttpEvent {
6
+ return {
7
+ id,
8
+ type: "http",
9
+ timestamp: Date.now(),
10
+ sourceIp: "127.0.0.1",
11
+ request: {
12
+ method: "GET",
13
+ path: "/",
14
+ headers: {},
15
+ query: {},
16
+ body: "",
17
+ },
18
+ };
19
+ }
20
+
21
+ describe("StorageManager", () => {
22
+ test("save + getEvent", async () => {
23
+ const storage = new StorageManager();
24
+
25
+ const event = makeHttpEvent("1");
26
+ await storage.save(event);
27
+
28
+ const found = await storage.getEvent("1");
29
+ expect(found).not.toBeNull();
30
+ expect(found?.id).toBe("1");
31
+ });
32
+
33
+ test("listEvents returns events", async () => {
34
+ const storage = new StorageManager();
35
+
36
+ await storage.save(makeHttpEvent("1"));
37
+
38
+ const list = await storage.listEvents({});
39
+ expect(list.length).toBe(1);
40
+ });
41
+
42
+ test("deleteEvent removes event", async () => {
43
+ const storage = new StorageManager();
44
+
45
+ await storage.save(makeHttpEvent("1"));
46
+
47
+ let list = await storage.listEvents({});
48
+ expect(list.length).toBe(1);
49
+
50
+ await storage.deleteEvent("1");
51
+
52
+ list = await storage.listEvents({});
53
+ expect(list.length).toBe(0);
54
+ });
55
+
56
+ test("clearEvents empties storage", async () => {
57
+ const storage = new StorageManager();
58
+
59
+ await storage.save(makeHttpEvent("1"));
60
+ await storage.clearEvents();
61
+
62
+ const list = await storage.listEvents({});
63
+ expect(list.length).toBe(0);
64
+ });
65
+
66
+ test("getStats returns correct structure", async () => {
67
+ const storage = new StorageManager();
68
+
69
+ const stats = await storage.getStats();
70
+
71
+ expect(stats.total).toBe(0);
72
+ expect(stats.byType).toEqual({});
73
+ });
74
+ });
@@ -0,0 +1,147 @@
1
+ import {
2
+ RawEvent,
3
+ RawDnsEvent,
4
+ RawSmtpEvent,
5
+ RawTcpEvent,
6
+ RawSsrfEvent,
7
+ NormalizedHttpEvent,
8
+ NormalizedDnsEvent,
9
+ NormalizedSmtpEvent,
10
+ NormalizedTcpEvent,
11
+ NormalizedSsrfEvent,
12
+ NormalizedWebhookEvent,
13
+ RawWebhookEvent,
14
+ NormalizedWebSocketEvent,
15
+ RawWebSocketEvent,
16
+ } from "../types/event.types";
17
+
18
+ import { IdGenerator } from "./id-generator";
19
+
20
+ export class EventNormalizer {
21
+ //
22
+ // -------------------------
23
+ // DNS
24
+ // -------------------------
25
+ //
26
+ static normalizeDns(event: RawDnsEvent): NormalizedDnsEvent {
27
+ return {
28
+ id: IdGenerator.generate(),
29
+ type: "dns",
30
+ timestamp: Date.now(),
31
+ ip: event.ip ?? "",
32
+ query: event.query ?? "",
33
+ recordType: event.recordType ?? "",
34
+ raw: event.raw,
35
+ };
36
+ }
37
+
38
+ //
39
+ // -------------------------
40
+ // HTTP
41
+ // -------------------------
42
+ //
43
+ static normalizeHttp(event: RawEvent): NormalizedHttpEvent {
44
+ const sourceIp = event.ip && event.ip.trim() !== "" ? event.ip : "unknown";
45
+
46
+ return {
47
+ id: IdGenerator.generate(),
48
+ type: "http",
49
+ timestamp: Date.now(),
50
+ sourceIp,
51
+ request: {
52
+ method: event.method,
53
+ path: event.path,
54
+ headers: event.headers,
55
+ query: event.query,
56
+ body: event.body,
57
+ },
58
+ };
59
+ }
60
+
61
+ //
62
+ // -------------------------
63
+ // SMTP
64
+ // -------------------------
65
+ //
66
+ static normalizeSmtp(event: RawSmtpEvent): NormalizedSmtpEvent {
67
+ return {
68
+ id: IdGenerator.generate(),
69
+ type: "smtp",
70
+ timestamp: Date.now(),
71
+ ip: event.ip ?? "",
72
+ from: event.from ?? "",
73
+ to: event.to ?? [],
74
+ subject: event.subject ?? "",
75
+ body: event.body ?? "",
76
+ raw: event.raw,
77
+ };
78
+ }
79
+
80
+ //
81
+ // -------------------------
82
+ // TCP
83
+ // -------------------------
84
+ //
85
+ static normalizeTcp(event: RawTcpEvent): NormalizedTcpEvent {
86
+ return {
87
+ id: IdGenerator.generate(),
88
+ type: "tcp",
89
+ timestamp: Date.now(),
90
+ ip: event.ip ?? "",
91
+ port: event.port ?? 0,
92
+ data: event.data ?? "",
93
+ raw: event.raw,
94
+ };
95
+ }
96
+
97
+ //
98
+ // -------------------------
99
+ // SSRF
100
+ // -------------------------
101
+ //
102
+ static normalizeSsrf(raw: RawSsrfEvent): NormalizedSsrfEvent {
103
+ return {
104
+ id: IdGenerator.generate(),
105
+ type: "ssrf",
106
+ timestamp: Date.now(),
107
+ sourceIp: raw.ip,
108
+ request: {
109
+ method: raw.method,
110
+ path: raw.path,
111
+ headers: raw.headers,
112
+ query: raw.query,
113
+ },
114
+ };
115
+ }
116
+
117
+ //
118
+ // -------------------------
119
+ // webhook
120
+ // -------------------------
121
+ //
122
+ static normalizeWebhook(raw: RawWebhookEvent): NormalizedWebhookEvent {
123
+ return {
124
+ id: IdGenerator.generate(),
125
+ type: "webhook",
126
+ timestamp: Date.now(),
127
+ sourceIp: raw.ip,
128
+ headers: raw.headers,
129
+ body: raw.body,
130
+ };
131
+ }
132
+
133
+ //
134
+ // -------------------------
135
+ // websocket
136
+ // -------------------------
137
+ //
138
+ static normalizeWebSocket(raw: RawWebSocketEvent): NormalizedWebSocketEvent {
139
+ return {
140
+ id: IdGenerator.generate(),
141
+ type: "websocket",
142
+ timestamp: Date.now(),
143
+ sourceIp: raw.ip,
144
+ message: raw.message,
145
+ };
146
+ }
147
+ }
@@ -0,0 +1,13 @@
1
+ import { EventNormalizer } from "./event.normalizer";
2
+ import { Storage } from "../storage-adapters/storage.interface";
3
+ import { RawEvent, NormalizedHttpEvent } from "../types/event.types";
4
+
5
+ export class EventRouter {
6
+ constructor(private storage: Storage) {}
7
+
8
+ async handleHttp(event: RawEvent): Promise<NormalizedHttpEvent> {
9
+ const normalized = EventNormalizer.normalizeHttp(event);
10
+ await this.storage.save(normalized);
11
+ return normalized;
12
+ }
13
+ }
@@ -0,0 +1,52 @@
1
+ import { NodeAdapter } from "../adapter-node";
2
+ import { MiddlewarePipeline } from "../middleware";
3
+ import { Router } from "../router";
4
+ import { Request } from "../request";
5
+ import { Response } from "../response";
6
+ import { describe, it, expect, jest } from "@jest/globals";
7
+ import * as Build from "../buildRequest";
8
+
9
+ describe("NodeAdapter.handle", () => {
10
+ it("appelle le handler quand la route match", async () => {
11
+ const router = new Router();
12
+ const pipeline = new MiddlewarePipeline();
13
+
14
+ // mock buildRequest
15
+ const buildRequestMock = jest
16
+ .spyOn(Build, "buildRequest")
17
+ .mockImplementation(
18
+ (raw: any, params?: Record<string, string>) =>
19
+ new Request({
20
+ method: "GET",
21
+ path: "/test",
22
+ headers: {},
23
+ params: params ?? {},
24
+ }),
25
+ );
26
+
27
+ const handler = jest.fn();
28
+
29
+ router.match = jest.fn(() => ({
30
+ handler,
31
+ params: { id: "42" },
32
+ middlewares: [],
33
+ })) as any;
34
+
35
+ pipeline.run = jest.fn(async () => {}) as any;
36
+
37
+ const res = {
38
+ writableEnded: false,
39
+ setHeader: jest.fn(),
40
+ end: jest.fn(),
41
+ statusCode: 200,
42
+ };
43
+
44
+ const adapter = new NodeAdapter(router, pipeline);
45
+
46
+ await adapter.handle({ url: "/test", method: "GET", headers: {} }, res);
47
+
48
+ expect(handler).toHaveBeenCalled();
49
+ expect(pipeline.run).toHaveBeenCalled();
50
+ expect(buildRequestMock).toHaveBeenCalledTimes(2);
51
+ });
52
+ });
@@ -0,0 +1,41 @@
1
+ import { bodyParserMultipart } from "../middleware";
2
+ import { Request } from "../request";
3
+ import { Response } from "../response";
4
+ import { describe, it, expect } from "@jest/globals";
5
+
6
+ describe("bodyParserMultipart", () => {
7
+ it("parse un champ texte et un fichier", async () => {
8
+ const boundary = "----1234";
9
+
10
+ const raw = {
11
+ headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
12
+ [Symbol.asyncIterator]: async function* () {
13
+ yield Buffer.from(
14
+ `--${boundary}\r\n` +
15
+ `Content-Disposition: form-data; name="username"\r\n\r\n` +
16
+ `jeremy\r\n` +
17
+ `--${boundary}\r\n` +
18
+ `Content-Disposition: form-data; name="file"; filename="test.txt"\r\n` +
19
+ `Content-Type: text/plain\r\n\r\n` +
20
+ `Hello world\r\n` +
21
+ `--${boundary}--`,
22
+ );
23
+ },
24
+ };
25
+
26
+ const req = new Request({
27
+ method: "POST",
28
+ path: "/upload",
29
+ headers: raw.headers,
30
+ raw,
31
+ });
32
+
33
+ const res = new Response({});
34
+
35
+ await bodyParserMultipart(req, res);
36
+
37
+ expect(req.form?.username).toBe("jeremy");
38
+ expect(req.files?.[0].filename).toBe("test.txt");
39
+ expect(req.files?.[0].data.toString()).toBe("Hello world");
40
+ });
41
+ });
@@ -0,0 +1,28 @@
1
+ import { bodyParserRaw } from "../middleware";
2
+ import { Request } from "../request";
3
+ import { Response } from "../response";
4
+ import { describe, it, expect } from "@jest/globals";
5
+
6
+ describe("bodyParserRaw", () => {
7
+ it("lit le body brut", async () => {
8
+ const raw = {
9
+ headers: { "content-type": "application/octet-stream" },
10
+ [Symbol.asyncIterator]: async function* () {
11
+ yield Buffer.from([1, 2, 3, 4]);
12
+ },
13
+ };
14
+
15
+ const req = new Request({
16
+ method: "POST",
17
+ path: "/",
18
+ headers: raw.headers,
19
+ raw,
20
+ });
21
+
22
+ const res = new Response({});
23
+
24
+ await bodyParserRaw(req, res);
25
+
26
+ expect(req.bodyRaw).toEqual(Buffer.from([1, 2, 3, 4]));
27
+ });
28
+ });
@@ -0,0 +1,28 @@
1
+ import { bodyParserText } from "../middleware";
2
+ import { Request } from "../request";
3
+ import { Response } from "../response";
4
+ import { describe, it, expect } from "@jest/globals";
5
+
6
+ describe("bodyParserText", () => {
7
+ it("lit le body text", async () => {
8
+ const raw = {
9
+ headers: { "content-type": "text/plain" },
10
+ [Symbol.asyncIterator]: async function* () {
11
+ yield Buffer.from("Hello world");
12
+ },
13
+ };
14
+
15
+ const req = new Request({
16
+ method: "POST",
17
+ path: "/",
18
+ headers: raw.headers,
19
+ raw,
20
+ });
21
+
22
+ const res = new Response({});
23
+
24
+ await bodyParserText(req, res);
25
+
26
+ expect(req.bodyText).toBe("Hello world");
27
+ });
28
+ });
@@ -0,0 +1,39 @@
1
+ import { compilePath } from "../compile-path";
2
+ import { describe, it, expect } from "@jest/globals";
3
+
4
+ describe("compilePath", () => {
5
+ it("compile un chemin simple avec segment statique", () => {
6
+ const result = compilePath("/tasks");
7
+
8
+ expect(result).toEqual([{ type: "static", value: "tasks" }]);
9
+ });
10
+
11
+ it("compile un chemin avec paramètre", () => {
12
+ const result = compilePath("/tasks/:id");
13
+
14
+ expect(result).toEqual([
15
+ { type: "static", value: "tasks" },
16
+ { type: "param", name: "id" },
17
+ ]);
18
+ });
19
+
20
+ it("compile un chemin avec paramètre", () => {
21
+ const result = compilePath("/tasks/:id");
22
+
23
+ expect(result).toEqual([
24
+ { type: "static", value: "tasks" },
25
+ { type: "param", name: "id" },
26
+ ]);
27
+ });
28
+
29
+ it("compile un chemin complexe avec static, param et wildcard", () => {
30
+ const result = compilePath("/a/:b/*/c");
31
+
32
+ expect(result).toEqual([
33
+ { type: "static", value: "a" },
34
+ { type: "param", name: "b" },
35
+ { type: "wildcard" },
36
+ { type: "static", value: "c" },
37
+ ]);
38
+ });
39
+ });
@@ -0,0 +1,51 @@
1
+ import { MiddlewarePipeline } from "../../http/middleware";
2
+ import { Request } from "../../http/request";
3
+ import { Response } from "../../http/response";
4
+ import { describe, it, expect, jest } from "@jest/globals";
5
+
6
+ describe("MiddlewarePipeline", () => {
7
+ it("exécute les middlewares dans l'ordre", async () => {
8
+ const pipeline = new MiddlewarePipeline();
9
+
10
+ const calls: string[] = [];
11
+
12
+ pipeline.use(async (req, res, next) => {
13
+ calls.push("mw1");
14
+ await next();
15
+ });
16
+
17
+ pipeline.use(async (req, res, next) => {
18
+ calls.push("mw2");
19
+ await next();
20
+ });
21
+
22
+ const req = new Request({ method: "GET", path: "/", headers: {} });
23
+ const res = new Response({ setHeader: jest.fn(), end: jest.fn() });
24
+
25
+ await pipeline.run(req, res);
26
+
27
+ expect(calls).toEqual(["mw1", "mw2"]);
28
+ });
29
+
30
+ it("arrête la chaîne si next() n'est pas appelé", async () => {
31
+ const pipeline = new MiddlewarePipeline();
32
+
33
+ const calls: string[] = [];
34
+
35
+ pipeline.use(async () => {
36
+ calls.push("mw1");
37
+ // pas de next()
38
+ });
39
+
40
+ pipeline.use(async () => {
41
+ calls.push("mw2");
42
+ });
43
+
44
+ const req = new Request({ method: "GET", path: "/", headers: {} });
45
+ const res = new Response({ setHeader: jest.fn(), end: jest.fn() });
46
+
47
+ await pipeline.run(req, res);
48
+
49
+ expect(calls).toEqual(["mw1"]);
50
+ });
51
+ });
@@ -0,0 +1,34 @@
1
+ import { Request } from "../request";
2
+ import { describe, it, expect } from "@jest/globals";
3
+
4
+ describe("Request", () => {
5
+ it("crée une requête immuable avec les bonnes propriétés", () => {
6
+ const req = new Request({
7
+ method: "GET",
8
+ path: "/test",
9
+ headers: { "content-type": "application/json" },
10
+ query: { a: "1" },
11
+ params: { id: "123" },
12
+ body: { x: 1 },
13
+ ip: "127.0.0.1",
14
+ });
15
+
16
+ expect(req.method).toBe("GET");
17
+ expect(req.path).toBe("/test");
18
+ expect(req.headers["content-type"]).toBe("application/json");
19
+ expect(req.query.a).toBe("1");
20
+ expect(req.params.id).toBe("123");
21
+ expect(req.body.x).toBe(1);
22
+ expect(req.ip).toBe("127.0.0.1");
23
+ });
24
+
25
+ it("retourne un header via header()", () => {
26
+ const req = new Request({
27
+ method: "GET",
28
+ path: "/",
29
+ headers: { "x-test": "ok" },
30
+ });
31
+
32
+ expect(req.header("x-test")).toBe("ok");
33
+ });
34
+ });
@@ -0,0 +1,35 @@
1
+ import { Response } from "../../http/response";
2
+ import { describe, it, expect, jest } from "@jest/globals";
3
+
4
+ describe("Response", () => {
5
+ it("enregistre le status et les headers", () => {
6
+ const raw = {
7
+ setHeader: jest.fn(),
8
+ end: jest.fn(),
9
+ };
10
+
11
+ const res = new Response(raw);
12
+
13
+ res.status(201).header("x-test", "ok").send("hello");
14
+
15
+ expect(raw.setHeader).toHaveBeenCalledWith("x-test", "ok");
16
+ expect(raw.end).toHaveBeenCalledWith("hello");
17
+ });
18
+
19
+ it("envoie du JSON", () => {
20
+ const raw = {
21
+ setHeader: jest.fn(),
22
+ end: jest.fn(),
23
+ };
24
+
25
+ const res = new Response(raw);
26
+
27
+ res.json({ a: 1 });
28
+
29
+ expect(raw.setHeader).toHaveBeenCalledWith(
30
+ "content-type",
31
+ "application/json",
32
+ );
33
+ expect(raw.end).toHaveBeenCalledWith(JSON.stringify({ a: 1 }));
34
+ });
35
+ });