odac 1.1.0 → 1.3.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 (117) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +42 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/release.yml +68 -1
  6. package/.github/workflows/test-coverage.yml +6 -5
  7. package/.husky/pre-commit +10 -0
  8. package/.husky/pre-push +13 -0
  9. package/.releaserc.js +1 -1
  10. package/CHANGELOG.md +99 -0
  11. package/README.md +16 -0
  12. package/bin/odac.js +196 -38
  13. package/client/odac.js +238 -171
  14. package/docs/backend/01-overview/03-development-server.md +38 -45
  15. package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
  16. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  17. package/docs/backend/03-config/01-database-connection.md +2 -2
  18. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  19. package/docs/backend/03-config/03-request-timeout.md +1 -1
  20. package/docs/backend/03-config/04-environment-variables.md +4 -4
  21. package/docs/backend/03-config/05-early-hints.md +2 -2
  22. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  23. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  24. package/docs/backend/04-routing/09-websocket.md +14 -1
  25. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  26. package/docs/backend/05-controllers/03-controller-classes.md +40 -20
  27. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  28. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  29. package/docs/backend/08-database/01-getting-started.md +2 -2
  30. package/docs/backend/10-authentication/03-register.md +1 -1
  31. package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
  32. package/docs/backend/10-authentication/05-session-management.md +15 -1
  33. package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
  34. package/docs/backend/10-authentication/07-magic-links.md +1 -1
  35. package/docs/index.json +5 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +18 -14
  38. package/src/Auth.js +58 -23
  39. package/src/Config.js +7 -7
  40. package/src/Database.js +1 -1
  41. package/src/Env.js +3 -1
  42. package/src/Ipc.js +7 -0
  43. package/src/Lang.js +9 -2
  44. package/src/Mail.js +19 -9
  45. package/src/Odac.js +56 -44
  46. package/src/Request.js +6 -2
  47. package/src/Route/Cron.js +58 -17
  48. package/src/Route/Internal.js +1 -1
  49. package/src/Route.js +375 -125
  50. package/src/Server.js +40 -3
  51. package/src/Storage.js +4 -0
  52. package/src/Token.js +6 -4
  53. package/src/Validator.js +24 -15
  54. package/src/Var.js +22 -6
  55. package/src/View/EarlyHints.js +43 -33
  56. package/src/View/Form.js +17 -11
  57. package/src/View.js +68 -12
  58. package/src/WebSocket.js +45 -12
  59. package/template/package.json +3 -1
  60. package/template/view/content/home.html +3 -3
  61. package/template/view/head/main.html +2 -2
  62. package/test/Client.test.js +168 -0
  63. package/test/Config.test.js +112 -0
  64. package/test/Lang.test.js +92 -0
  65. package/test/Odac.test.js +88 -0
  66. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  67. package/test/{framework/Route.test.js → Route.test.js} +105 -1
  68. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  69. package/test/WebSocket.test.js +238 -0
  70. package/test/scripts/check-coverage.js +4 -4
  71. package/test/cli/Cli.test.js +0 -36
  72. package/test/core/Commands.test.js +0 -538
  73. package/test/core/Config.test.js +0 -1432
  74. package/test/core/Lang.test.js +0 -250
  75. package/test/core/Odac.test.js +0 -234
  76. package/test/core/Process.test.js +0 -156
  77. package/test/framework/WebSocket.test.js +0 -100
  78. package/test/server/Api.test.js +0 -647
  79. package/test/server/DNS.test.js +0 -2050
  80. package/test/server/DNS.test.js.bak +0 -2084
  81. package/test/server/Hub.test.js +0 -497
  82. package/test/server/Log.test.js +0 -73
  83. package/test/server/Mail.account.test_.js +0 -460
  84. package/test/server/Mail.init.test_.js +0 -411
  85. package/test/server/Mail.test_.js +0 -1340
  86. package/test/server/SSL.test_.js +0 -1491
  87. package/test/server/Server.test.js +0 -765
  88. package/test/server/Service.test_.js +0 -1127
  89. package/test/server/Subdomain.test.js +0 -440
  90. package/test/server/Web/Firewall.test.js +0 -175
  91. package/test/server/Web/Proxy.test.js +0 -397
  92. package/test/server/Web.test.js +0 -1494
  93. package/test/server/__mocks__/acme-client.js +0 -17
  94. package/test/server/__mocks__/bcrypt.js +0 -50
  95. package/test/server/__mocks__/child_process.js +0 -389
  96. package/test/server/__mocks__/crypto.js +0 -432
  97. package/test/server/__mocks__/fs.js +0 -450
  98. package/test/server/__mocks__/globalOdac.js +0 -227
  99. package/test/server/__mocks__/http.js +0 -575
  100. package/test/server/__mocks__/https.js +0 -272
  101. package/test/server/__mocks__/index.js +0 -249
  102. package/test/server/__mocks__/mail/server.js +0 -100
  103. package/test/server/__mocks__/mail/smtp.js +0 -31
  104. package/test/server/__mocks__/mailparser.js +0 -81
  105. package/test/server/__mocks__/net.js +0 -369
  106. package/test/server/__mocks__/node-forge.js +0 -328
  107. package/test/server/__mocks__/os.js +0 -320
  108. package/test/server/__mocks__/path.js +0 -291
  109. package/test/server/__mocks__/selfsigned.js +0 -8
  110. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  111. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  112. package/test/server/__mocks__/smtp-server.js +0 -106
  113. package/test/server/__mocks__/sqlite3.js +0 -394
  114. package/test/server/__mocks__/testFactories.js +0 -299
  115. package/test/server/__mocks__/testHelpers.js +0 -363
  116. package/test/server/__mocks__/tls.js +0 -229
  117. /package/template/{config.json → odac.json} +0 -0
@@ -1,397 +0,0 @@
1
- /**
2
- * Unit tests for Web/Proxy.js module
3
- * Tests custom HTTP proxy implementation for both HTTP/1 and HTTP/2
4
- */
5
-
6
- jest.mock('http')
7
-
8
- const http = require('http')
9
- const WebProxy = require('../../../server/src/Web/Proxy.js')
10
-
11
- describe('WebProxy', () => {
12
- let proxy
13
- let mockLog
14
- let mockReq
15
- let mockRes
16
- let mockWebsite
17
-
18
- beforeEach(() => {
19
- jest.clearAllMocks()
20
-
21
- mockLog = jest.fn()
22
- proxy = new WebProxy(mockLog)
23
-
24
- mockReq = {
25
- url: '/test',
26
- method: 'GET',
27
- headers: {
28
- host: 'example.com',
29
- 'user-agent': 'test-agent'
30
- },
31
- socket: {
32
- remoteAddress: '192.168.1.100'
33
- },
34
- setTimeout: jest.fn(),
35
- on: jest.fn(),
36
- pipe: jest.fn()
37
- }
38
-
39
- mockRes = {
40
- writeHead: jest.fn(),
41
- end: jest.fn(),
42
- setTimeout: jest.fn(),
43
- on: jest.fn(),
44
- headersSent: false,
45
- writeEarlyHints: jest.fn()
46
- }
47
-
48
- mockWebsite = {
49
- port: 3000,
50
- domain: 'example.com'
51
- }
52
- })
53
-
54
- describe('http1 proxy', () => {
55
- test('should create HTTP request with correct options', () => {
56
- const mockProxyReq = {
57
- on: jest.fn(),
58
- pipe: jest.fn()
59
- }
60
-
61
- http.request.mockReturnValue(mockProxyReq)
62
-
63
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
64
-
65
- expect(http.request).toHaveBeenCalledWith(
66
- expect.objectContaining({
67
- hostname: '127.0.0.1',
68
- port: 3000,
69
- path: '/test',
70
- method: 'GET',
71
- timeout: 0
72
- }),
73
- expect.any(Function)
74
- )
75
- })
76
-
77
- test('should add custom headers to proxy request', () => {
78
- const mockProxyReq = {
79
- on: jest.fn(),
80
- pipe: jest.fn()
81
- }
82
-
83
- http.request.mockReturnValue(mockProxyReq)
84
-
85
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
86
-
87
- const options = http.request.mock.calls[0][0]
88
- expect(options.headers).toMatchObject({
89
- host: 'example.com',
90
- 'user-agent': 'test-agent',
91
- 'x-odac-connection-remoteaddress': '192.168.1.100',
92
- 'x-odac-connection-ssl': 'true'
93
- })
94
- })
95
-
96
- test('should handle proxy connection errors', () => {
97
- const mockProxyReq = {
98
- on: jest.fn(),
99
- pipe: jest.fn()
100
- }
101
-
102
- http.request.mockReturnValue(mockProxyReq)
103
-
104
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
105
-
106
- const errorHandler = mockProxyReq.on.mock.calls.find(call => call[0] === 'error')[1]
107
- const error = new Error('Connection refused')
108
- error.code = 'ECONNREFUSED'
109
-
110
- errorHandler(error)
111
-
112
- expect(mockLog).toHaveBeenCalledWith('Proxy error for example.com: Connection refused')
113
- expect(mockRes.writeHead).toHaveBeenCalledWith(502)
114
- expect(mockRes.end).toHaveBeenCalledWith('Bad Gateway')
115
- })
116
-
117
- test('should not send error response if headers already sent', () => {
118
- const mockProxyReq = {
119
- on: jest.fn(),
120
- pipe: jest.fn()
121
- }
122
-
123
- http.request.mockReturnValue(mockProxyReq)
124
- mockRes.headersSent = true
125
-
126
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
127
-
128
- const errorHandler = mockProxyReq.on.mock.calls.find(call => call[0] === 'error')[1]
129
- const error = new Error('Connection timeout')
130
-
131
- errorHandler(error)
132
-
133
- expect(mockLog).toHaveBeenCalledWith('Proxy error for example.com: Connection timeout')
134
- expect(mockRes.writeHead).not.toHaveBeenCalled()
135
- expect(mockRes.end).not.toHaveBeenCalledWith('Bad Gateway')
136
- })
137
-
138
- test('should ignore ECONNRESET errors', () => {
139
- const mockProxyReq = {
140
- on: jest.fn(),
141
- pipe: jest.fn()
142
- }
143
-
144
- http.request.mockReturnValue(mockProxyReq)
145
-
146
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
147
-
148
- const errorHandler = mockProxyReq.on.mock.calls.find(call => call[0] === 'error')[1]
149
- const error = new Error('Connection reset')
150
- error.code = 'ECONNRESET'
151
-
152
- errorHandler(error)
153
-
154
- expect(mockLog).not.toHaveBeenCalled()
155
- expect(mockRes.writeHead).not.toHaveBeenCalled()
156
- })
157
-
158
- test('should filter forbidden response headers', done => {
159
- const mockProxyRes = {
160
- statusCode: 200,
161
- headers: {
162
- 'content-type': 'application/json',
163
- connection: 'keep-alive',
164
- 'keep-alive': 'timeout=5',
165
- 'transfer-encoding': 'chunked',
166
- upgrade: 'websocket',
167
- 'proxy-connection': 'keep-alive',
168
- 'x-custom-header': 'value'
169
- },
170
- pipe: jest.fn()
171
- }
172
-
173
- const mockProxyReq = {
174
- on: jest.fn(),
175
- pipe: jest.fn()
176
- }
177
-
178
- http.request.mockImplementation((options, callback) => {
179
- setTimeout(() => callback(mockProxyRes), 0)
180
- return mockProxyReq
181
- })
182
-
183
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
184
-
185
- setTimeout(() => {
186
- expect(mockRes.writeHead).toHaveBeenCalledWith(
187
- 200,
188
- expect.objectContaining({
189
- 'content-type': 'application/json',
190
- 'x-custom-header': 'value'
191
- })
192
- )
193
-
194
- const headers = mockRes.writeHead.mock.calls[0][1]
195
- expect(headers).not.toHaveProperty('connection')
196
- expect(headers).not.toHaveProperty('keep-alive')
197
- expect(headers).not.toHaveProperty('transfer-encoding')
198
- expect(headers).not.toHaveProperty('upgrade')
199
- expect(headers).not.toHaveProperty('proxy-connection')
200
- done()
201
- }, 10)
202
- })
203
-
204
- test('should handle Server-Sent Events connections', done => {
205
- const mockProxyRes = {
206
- statusCode: 200,
207
- headers: {
208
- 'content-type': 'text/event-stream'
209
- },
210
- pipe: jest.fn()
211
- }
212
-
213
- const mockProxyReq = {
214
- on: jest.fn(),
215
- pipe: jest.fn(),
216
- destroy: jest.fn()
217
- }
218
-
219
- http.request.mockImplementation((options, callback) => {
220
- setTimeout(() => callback(mockProxyRes), 0)
221
- return mockProxyReq
222
- })
223
-
224
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
225
-
226
- setTimeout(() => {
227
- expect(mockReq.setTimeout).toHaveBeenCalledWith(0)
228
- expect(mockRes.setTimeout).toHaveBeenCalledWith(0)
229
- expect(mockReq.on).toHaveBeenCalledWith('close', expect.any(Function))
230
- expect(mockReq.on).toHaveBeenCalledWith('aborted', expect.any(Function))
231
- expect(mockRes.on).toHaveBeenCalledWith('close', expect.any(Function))
232
- done()
233
- }, 10)
234
- })
235
-
236
- test('should handle early hints', done => {
237
- const mockProxyRes = {
238
- statusCode: 200,
239
- headers: {
240
- 'content-type': 'text/html',
241
- 'x-odac-early-hints': JSON.stringify(['</style.css>; rel=preload; as=style'])
242
- },
243
- pipe: jest.fn()
244
- }
245
-
246
- const mockProxyReq = {
247
- on: jest.fn(),
248
- pipe: jest.fn()
249
- }
250
-
251
- http.request.mockImplementation((options, callback) => {
252
- setTimeout(() => callback(mockProxyRes), 0)
253
- return mockProxyReq
254
- })
255
-
256
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
257
-
258
- setTimeout(() => {
259
- expect(mockRes.writeEarlyHints).toHaveBeenCalledWith({
260
- link: ['</style.css>; rel=preload; as=style']
261
- })
262
-
263
- const headers = mockRes.writeHead.mock.calls[0][1]
264
- expect(headers).not.toHaveProperty('x-odac-early-hints')
265
- done()
266
- }, 10)
267
- })
268
-
269
- test('should ignore invalid early hints JSON', done => {
270
- const mockProxyRes = {
271
- statusCode: 200,
272
- headers: {
273
- 'content-type': 'text/html',
274
- 'x-odac-early-hints': 'invalid-json'
275
- },
276
- pipe: jest.fn()
277
- }
278
-
279
- const mockProxyReq = {
280
- on: jest.fn(),
281
- pipe: jest.fn()
282
- }
283
-
284
- http.request.mockImplementation((options, callback) => {
285
- setTimeout(() => callback(mockProxyRes), 0)
286
- return mockProxyReq
287
- })
288
-
289
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
290
-
291
- setTimeout(() => {
292
- expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
293
- expect(mockRes.writeHead).toHaveBeenCalled()
294
- done()
295
- }, 10)
296
- })
297
-
298
- test('should pipe request to proxy', () => {
299
- const mockProxyReq = {
300
- on: jest.fn(),
301
- pipe: jest.fn()
302
- }
303
-
304
- http.request.mockReturnValue(mockProxyReq)
305
-
306
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
307
-
308
- expect(mockReq.pipe).toHaveBeenCalledWith(mockProxyReq)
309
- })
310
-
311
- test('should pipe proxy response to client', done => {
312
- const mockProxyRes = {
313
- statusCode: 200,
314
- headers: {'content-type': 'text/plain'},
315
- pipe: jest.fn()
316
- }
317
-
318
- const mockProxyReq = {
319
- on: jest.fn(),
320
- pipe: jest.fn()
321
- }
322
-
323
- http.request.mockImplementation((options, callback) => {
324
- setTimeout(() => callback(mockProxyRes), 0)
325
- return mockProxyReq
326
- })
327
-
328
- proxy.http1(mockReq, mockRes, mockWebsite, 'example.com')
329
-
330
- setTimeout(() => {
331
- expect(mockProxyRes.pipe).toHaveBeenCalledWith(mockRes)
332
- done()
333
- }, 10)
334
- })
335
- })
336
-
337
- describe('http2 proxy', () => {
338
- test('should filter HTTP/2 pseudo-headers', () => {
339
- mockReq.headers[':method'] = 'GET'
340
- mockReq.headers[':path'] = '/test'
341
- mockReq.headers[':scheme'] = 'https'
342
- mockReq.headers[':authority'] = 'example.com'
343
-
344
- const mockProxyReq = {
345
- on: jest.fn(),
346
- pipe: jest.fn()
347
- }
348
-
349
- http.request.mockReturnValue(mockProxyReq)
350
-
351
- proxy.http2(mockReq, mockRes, mockWebsite, 'example.com')
352
-
353
- const options = http.request.mock.calls[0][0]
354
- expect(options.headers).not.toHaveProperty(':method')
355
- expect(options.headers).not.toHaveProperty(':path')
356
- expect(options.headers).not.toHaveProperty(':scheme')
357
- expect(options.headers).not.toHaveProperty(':authority')
358
- expect(options.headers).toHaveProperty('user-agent')
359
- })
360
-
361
- test('should handle HTTP/2 proxy errors', () => {
362
- const mockProxyReq = {
363
- on: jest.fn(),
364
- pipe: jest.fn()
365
- }
366
-
367
- http.request.mockReturnValue(mockProxyReq)
368
-
369
- proxy.http2(mockReq, mockRes, mockWebsite, 'example.com')
370
-
371
- const errorHandler = mockProxyReq.on.mock.calls.find(call => call[0] === 'error')[1]
372
- const error = new Error('Connection failed')
373
-
374
- errorHandler(error)
375
-
376
- expect(mockLog).toHaveBeenCalledWith('Proxy error for example.com: Connection failed')
377
- expect(mockRes.writeHead).toHaveBeenCalledWith(502)
378
- expect(mockRes.end).toHaveBeenCalledWith('Bad Gateway')
379
- })
380
-
381
- test('should handle missing remote address', () => {
382
- mockReq.socket = {}
383
-
384
- const mockProxyReq = {
385
- on: jest.fn(),
386
- pipe: jest.fn()
387
- }
388
-
389
- http.request.mockReturnValue(mockProxyReq)
390
-
391
- proxy.http2(mockReq, mockRes, mockWebsite, 'example.com')
392
-
393
- const options = http.request.mock.calls[0][0]
394
- expect(options.headers['x-odac-connection-remoteaddress']).toBe('')
395
- })
396
- })
397
- })