odac 0.9.0 → 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 (208) hide show
  1. package/.github/workflows/auto-pr-description.yml +0 -2
  2. package/.github/workflows/codeql.yml +46 -0
  3. package/.github/workflows/release.yml +13 -6
  4. package/.github/workflows/test-coverage.yml +10 -9
  5. package/.releaserc.js +9 -6
  6. package/CHANGELOG.md +62 -150
  7. package/CODE_OF_CONDUCT.md +1 -1
  8. package/CONTRIBUTING.md +8 -8
  9. package/LICENSE +21 -661
  10. package/README.md +12 -12
  11. package/SECURITY.md +4 -4
  12. package/bin/odac.js +101 -0
  13. package/{framework/web/candy.js → client/odac.js} +310 -44
  14. package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md} +4 -2
  15. package/docs/backend/01-overview/02-super-handy-helper-functions.md +29 -1
  16. package/docs/backend/01-overview/03-development-server.md +11 -11
  17. package/docs/backend/02-structure/01-typical-project-layout.md +4 -4
  18. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  19. package/docs/backend/03-config/01-database-connection.md +1 -1
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +4 -4
  21. package/docs/backend/03-config/04-environment-variables.md +20 -20
  22. package/docs/backend/03-config/05-early-hints.md +4 -4
  23. package/docs/backend/04-routing/01-basic-page-routes.md +4 -4
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +5 -5
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +3 -3
  26. package/docs/backend/04-routing/04-authentication-aware-routes.md +5 -5
  27. package/docs/backend/04-routing/05-advanced-routing.md +3 -3
  28. package/docs/backend/04-routing/06-error-pages.md +17 -17
  29. package/docs/backend/04-routing/07-cron-jobs.md +13 -13
  30. package/docs/backend/04-routing/08-middleware.md +214 -0
  31. package/docs/backend/04-routing/09-websocket-auth-middleware.md +292 -0
  32. package/docs/backend/04-routing/09-websocket-examples.md +381 -0
  33. package/docs/backend/04-routing/09-websocket-quick-reference.md +211 -0
  34. package/docs/backend/04-routing/09-websocket.md +298 -0
  35. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +3 -3
  36. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +41 -0
  37. package/docs/backend/05-controllers/03-controller-classes.md +19 -19
  38. package/docs/backend/05-forms/01-custom-forms.md +114 -114
  39. package/docs/backend/05-forms/02-automatic-database-insert.md +82 -82
  40. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +26 -26
  41. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +10 -10
  42. package/docs/backend/07-views/01-the-view-directory.md +1 -1
  43. package/docs/backend/07-views/02-rendering-a-view.md +22 -22
  44. package/docs/backend/07-views/03-template-syntax.md +52 -52
  45. package/docs/backend/07-views/03-variables.md +84 -84
  46. package/docs/backend/07-views/04-request-data.md +57 -57
  47. package/docs/backend/07-views/05-conditionals.md +78 -78
  48. package/docs/backend/07-views/06-loops.md +114 -114
  49. package/docs/backend/07-views/07-translations.md +66 -66
  50. package/docs/backend/07-views/08-backend-javascript.md +103 -103
  51. package/docs/backend/07-views/09-comments.md +71 -71
  52. package/docs/backend/08-database/01-database-connection.md +8 -8
  53. package/docs/backend/08-database/02-using-mysql.md +49 -49
  54. package/docs/backend/09-validation/01-the-validator-service.md +38 -38
  55. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +15 -15
  56. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +10 -10
  57. package/docs/backend/10-authentication/03-register.md +12 -12
  58. package/docs/backend/10-authentication/{04-candy-register-forms.md → 04-odac-register-forms.md} +141 -141
  59. package/docs/backend/10-authentication/05-session-management.md +10 -10
  60. package/docs/backend/10-authentication/{06-candy-login-forms.md → 06-odac-login-forms.md} +125 -125
  61. package/docs/backend/11-mail/01-the-mail-service.md +5 -5
  62. package/docs/backend/12-streaming/01-streaming-overview.md +96 -54
  63. package/docs/backend/13-utilities/{01-candy-var.md → 01-odac-var.md} +109 -109
  64. package/docs/frontend/01-overview/01-introduction.md +30 -30
  65. package/docs/frontend/02-ajax-navigation/01-quick-start.md +45 -45
  66. package/docs/frontend/02-ajax-navigation/02-configuration.md +14 -14
  67. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +36 -36
  68. package/docs/frontend/03-forms/01-form-handling.md +32 -32
  69. package/docs/frontend/04-api-requests/01-get-post.md +33 -33
  70. package/docs/frontend/05-streaming/01-client-streaming.md +15 -15
  71. package/docs/frontend/06-websocket/00-overview.md +76 -0
  72. package/docs/frontend/06-websocket/01-websocket-client.md +139 -0
  73. package/docs/frontend/06-websocket/02-shared-websocket.md +149 -0
  74. package/docs/index.json +49 -11
  75. package/eslint.config.mjs +6 -6
  76. package/{framework/index.js → index.js} +1 -1
  77. package/package.json +14 -39
  78. package/{framework/src → src}/Auth.js +59 -59
  79. package/{framework/src → src}/Config.js +3 -3
  80. package/{framework/src → src}/Lang.js +7 -7
  81. package/{framework/src → src}/Mail.js +5 -5
  82. package/{framework/src → src}/Mysql.js +42 -42
  83. package/src/Odac.js +112 -0
  84. package/{framework/src → src}/Request.js +38 -36
  85. package/{framework/src → src}/Route/Internal.js +116 -116
  86. package/src/Route/Middleware.js +75 -0
  87. package/src/Route.js +621 -0
  88. package/src/Server.js +22 -0
  89. package/{framework/src → src}/Stream.js +11 -3
  90. package/{framework/src → src}/Validator.js +21 -21
  91. package/{framework/src → src}/Var.js +5 -5
  92. package/{framework/src → src}/View/EarlyHints.js +1 -1
  93. package/{framework/src → src}/View/Form.js +69 -69
  94. package/{framework/src → src}/View.js +78 -81
  95. package/src/WebSocket.js +403 -0
  96. package/template/config.json +5 -0
  97. package/{web → template}/controller/page/about.js +6 -6
  98. package/{web → template}/controller/page/index.js +9 -9
  99. package/{web → template}/package.json +4 -5
  100. package/{web → template}/public/assets/css/style.css +4 -4
  101. package/{web → template}/public/assets/js/app.js +6 -6
  102. package/{web → template}/route/www.js +6 -6
  103. package/{web → template}/skeleton/main.html +1 -1
  104. package/{web → template}/view/content/about.html +5 -5
  105. package/{web → template}/view/content/home.html +12 -12
  106. package/template/view/footer/main.html +11 -0
  107. package/{web → template}/view/head/main.html +1 -1
  108. package/{web → template}/view/header/main.html +2 -2
  109. package/test/core/Candy.test.js +58 -58
  110. package/test/core/Commands.test.js +7 -7
  111. package/test/core/Config.test.js +82 -85
  112. package/test/core/Lang.test.js +2 -2
  113. package/test/core/Process.test.js +6 -6
  114. package/test/framework/Route.test.js +56 -37
  115. package/test/framework/View/EarlyHints.test.js +2 -2
  116. package/test/framework/WebSocket.test.js +100 -0
  117. package/test/framework/middleware.test.js +85 -0
  118. package/test/server/Api.test.js +31 -31
  119. package/test/server/DNS.test.js +11 -11
  120. package/test/server/Hub.test.js +497 -0
  121. package/test/server/Mail.account.test_.js +3 -3
  122. package/test/server/Mail.init.test_.js +10 -10
  123. package/test/server/Mail.test_.js +20 -20
  124. package/test/server/SSL.test_.js +54 -54
  125. package/test/server/Server.test.js +39 -39
  126. package/test/server/Service.test_.js +7 -7
  127. package/test/server/Subdomain.test.js +7 -7
  128. package/test/server/Web/Firewall.test.js +87 -87
  129. package/test/server/Web/Proxy.test.js +397 -0
  130. package/test/server/{Web.test_.js → Web.test.js} +137 -205
  131. package/test/server/__mocks__/fs.js +2 -2
  132. package/test/server/__mocks__/{globalCandy.js → globalOdac.js} +5 -5
  133. package/test/server/__mocks__/index.js +6 -6
  134. package/test/server/__mocks__/testFactories.js +1 -1
  135. package/test/server/__mocks__/testHelpers.js +7 -7
  136. package/.husky/pre-commit +0 -2
  137. package/.kiro/steering/code-style.md +0 -56
  138. package/.kiro/steering/product.md +0 -20
  139. package/.kiro/steering/structure.md +0 -77
  140. package/.kiro/steering/tech.md +0 -87
  141. package/AGENTS.md +0 -84
  142. package/bin/candy +0 -10
  143. package/bin/candypack +0 -10
  144. package/cli/index.js +0 -3
  145. package/cli/src/Cli.js +0 -348
  146. package/cli/src/Connector.js +0 -93
  147. package/cli/src/Monitor.js +0 -416
  148. package/core/Candy.js +0 -87
  149. package/core/Commands.js +0 -239
  150. package/core/Config.js +0 -1094
  151. package/core/Lang.js +0 -52
  152. package/core/Log.js +0 -43
  153. package/core/Process.js +0 -26
  154. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +0 -20
  155. package/docs/server/01-installation/01-quick-install.md +0 -19
  156. package/docs/server/01-installation/02-manual-installation-via-npm.md +0 -9
  157. package/docs/server/02-get-started/01-core-concepts.md +0 -7
  158. package/docs/server/02-get-started/02-basic-commands.md +0 -57
  159. package/docs/server/02-get-started/03-cli-reference.md +0 -276
  160. package/docs/server/02-get-started/04-cli-quick-reference.md +0 -102
  161. package/docs/server/03-service/01-start-a-new-service.md +0 -57
  162. package/docs/server/03-service/02-delete-a-service.md +0 -48
  163. package/docs/server/04-web/01-create-a-website.md +0 -36
  164. package/docs/server/04-web/02-list-websites.md +0 -9
  165. package/docs/server/04-web/03-delete-a-website.md +0 -29
  166. package/docs/server/05-subdomain/01-create-a-subdomain.md +0 -32
  167. package/docs/server/05-subdomain/02-list-subdomains.md +0 -33
  168. package/docs/server/05-subdomain/03-delete-a-subdomain.md +0 -41
  169. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +0 -34
  170. package/docs/server/07-mail/01-create-a-mail-account.md +0 -23
  171. package/docs/server/07-mail/02-delete-a-mail-account.md +0 -20
  172. package/docs/server/07-mail/03-list-mail-accounts.md +0 -20
  173. package/docs/server/07-mail/04-change-account-password.md +0 -23
  174. package/framework/src/Candy.js +0 -81
  175. package/framework/src/Route.js +0 -455
  176. package/framework/src/Server.js +0 -15
  177. package/locale/de-DE.json +0 -80
  178. package/locale/en-US.json +0 -79
  179. package/locale/es-ES.json +0 -80
  180. package/locale/fr-FR.json +0 -80
  181. package/locale/pt-BR.json +0 -80
  182. package/locale/ru-RU.json +0 -80
  183. package/locale/tr-TR.json +0 -85
  184. package/locale/zh-CN.json +0 -80
  185. package/server/index.js +0 -5
  186. package/server/src/Api.js +0 -88
  187. package/server/src/DNS.js +0 -940
  188. package/server/src/Hub.js +0 -535
  189. package/server/src/Mail.js +0 -571
  190. package/server/src/SSL.js +0 -180
  191. package/server/src/Server.js +0 -27
  192. package/server/src/Service.js +0 -248
  193. package/server/src/Subdomain.js +0 -64
  194. package/server/src/Web/Firewall.js +0 -170
  195. package/server/src/Web/Proxy.js +0 -134
  196. package/server/src/Web.js +0 -451
  197. package/server/src/mail/imap.js +0 -1091
  198. package/server/src/mail/server.js +0 -32
  199. package/server/src/mail/smtp.js +0 -786
  200. package/test/server/Client.test.js +0 -338
  201. package/test/server/__mocks__/http-proxy.js +0 -105
  202. package/watchdog/index.js +0 -3
  203. package/watchdog/src/Watchdog.js +0 -156
  204. package/web/config.json +0 -5
  205. package/web/view/footer/main.html +0 -11
  206. /package/{framework/src → src}/Env.js +0 -0
  207. /package/{framework/src → src}/Route/Cron.js +0 -0
  208. /package/{framework/src → src}/Token.js +0 -0
@@ -0,0 +1,403 @@
1
+ const nodeCrypto = require('crypto')
2
+
3
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
4
+ const MAX_PAYLOAD_LENGTH = 10 * 1024 * 1024
5
+ const OPCODE = {
6
+ CONTINUATION: 0x0,
7
+ TEXT: 0x1,
8
+ BINARY: 0x2,
9
+ CLOSE: 0x8,
10
+ PING: 0x9,
11
+ PONG: 0xa
12
+ }
13
+
14
+ class WebSocketClient {
15
+ #socket
16
+ #handlers = {}
17
+ #closed = false
18
+ #server
19
+ #id
20
+ #rooms = new Set()
21
+ data = {}
22
+
23
+ constructor(socket, server, id) {
24
+ this.#socket = socket
25
+ this.#server = server
26
+ this.#id = id
27
+ this.#setupListeners()
28
+ }
29
+
30
+ get id() {
31
+ return this.#id
32
+ }
33
+
34
+ get rooms() {
35
+ return Array.from(this.#rooms)
36
+ }
37
+
38
+ #setupListeners() {
39
+ let buffer = Buffer.alloc(0)
40
+
41
+ this.#socket.on('data', chunk => {
42
+ buffer = Buffer.concat([buffer, chunk])
43
+
44
+ while (buffer.length >= 2) {
45
+ const frame = this.#parseFrame(buffer)
46
+ if (!frame) break
47
+
48
+ buffer = buffer.slice(frame.totalLength)
49
+ this.#handleFrame(frame)
50
+ }
51
+ })
52
+
53
+ this.#socket.on('close', () => this.#handleClose())
54
+ this.#socket.on('error', err => this.#emit('error', err))
55
+ }
56
+
57
+ #parseFrame(buffer) {
58
+ if (buffer.length < 2) return null
59
+
60
+ const firstByte = buffer[0]
61
+ const secondByte = buffer[1]
62
+
63
+ const fin = (firstByte & 0x80) !== 0
64
+ const opcode = firstByte & 0x0f
65
+ const masked = (secondByte & 0x80) !== 0
66
+
67
+ if (!masked) {
68
+ this.close(1002, 'Protocol error: client-to-server frames must be masked.')
69
+ return null
70
+ }
71
+
72
+ let payloadLength = secondByte & 0x7f
73
+
74
+ let offset = 2
75
+
76
+ if (payloadLength === 126) {
77
+ if (buffer.length < 4) return null
78
+ payloadLength = buffer.readUInt16BE(2)
79
+ offset = 4
80
+ } else if (payloadLength === 127) {
81
+ if (buffer.length < 10) return null
82
+ const payloadLengthBigInt = buffer.readBigUInt64BE(2)
83
+ if (payloadLengthBigInt > Number.MAX_SAFE_INTEGER) {
84
+ this.close(1009, 'Payload too large')
85
+ return null
86
+ }
87
+ payloadLength = Number(payloadLengthBigInt)
88
+ offset = 10
89
+ }
90
+
91
+ if (payloadLength > MAX_PAYLOAD_LENGTH) {
92
+ this.close(1009, 'Payload too large')
93
+ return null
94
+ }
95
+
96
+ let maskKey = null
97
+ if (masked) {
98
+ if (buffer.length < offset + 4) return null
99
+ maskKey = buffer.slice(offset, offset + 4)
100
+ offset += 4
101
+ }
102
+
103
+ if (buffer.length < offset + payloadLength) return null
104
+
105
+ let payload = buffer.slice(offset, offset + payloadLength)
106
+
107
+ if (masked && maskKey) {
108
+ payload = Buffer.from(payload)
109
+ for (let i = 0; i < payload.length; i++) {
110
+ payload[i] ^= maskKey[i % 4]
111
+ }
112
+ }
113
+
114
+ return {
115
+ fin,
116
+ opcode,
117
+ payload,
118
+ totalLength: offset + payloadLength
119
+ }
120
+ }
121
+
122
+ #handleFrame(frame) {
123
+ switch (frame.opcode) {
124
+ case OPCODE.TEXT:
125
+ this.#handleMessage(frame.payload.toString('utf8'))
126
+ break
127
+ case OPCODE.BINARY:
128
+ this.#handleMessage(frame.payload)
129
+ break
130
+ case OPCODE.PING:
131
+ this.#sendFrame(OPCODE.PONG, frame.payload)
132
+ break
133
+ case OPCODE.PONG:
134
+ this.#emit('pong')
135
+ break
136
+ case OPCODE.CLOSE:
137
+ this.close()
138
+ break
139
+ }
140
+ }
141
+
142
+ #handleMessage(data) {
143
+ try {
144
+ const parsed = JSON.parse(data)
145
+ this.#emit('message', parsed)
146
+ } catch {
147
+ this.#emit('message', data)
148
+ }
149
+ }
150
+
151
+ #handleClose() {
152
+ if (this.#closed) return
153
+ this.#closed = true
154
+
155
+ for (const room of this.#rooms) {
156
+ this.#server.leaveRoom(this.#id, room)
157
+ }
158
+
159
+ this.#emit('close')
160
+ this.#server.removeClient(this.#id)
161
+ }
162
+
163
+ #sendFrame(opcode, data) {
164
+ if (this.#closed) return
165
+
166
+ const payload = Buffer.isBuffer(data) ? data : Buffer.from(data)
167
+ const length = payload.length
168
+
169
+ let header
170
+ if (length < 126) {
171
+ header = Buffer.alloc(2)
172
+ header[0] = 0x80 | opcode
173
+ header[1] = length
174
+ } else if (length < 65536) {
175
+ header = Buffer.alloc(4)
176
+ header[0] = 0x80 | opcode
177
+ header[1] = 126
178
+ header.writeUInt16BE(length, 2)
179
+ } else {
180
+ header = Buffer.alloc(10)
181
+ header[0] = 0x80 | opcode
182
+ header[1] = 127
183
+ header.writeBigUInt64BE(BigInt(length), 2)
184
+ }
185
+
186
+ this.#socket.write(Buffer.concat([header, payload]))
187
+ }
188
+
189
+ #emit(event, ...args) {
190
+ if (this.#handlers[event]) {
191
+ for (const handler of this.#handlers[event]) {
192
+ handler(...args)
193
+ }
194
+ }
195
+ }
196
+
197
+ on(event, handler) {
198
+ if (!this.#handlers[event]) this.#handlers[event] = []
199
+ this.#handlers[event].push(handler)
200
+ return this
201
+ }
202
+
203
+ off(event, handler) {
204
+ if (!this.#handlers[event]) return this
205
+ if (handler) {
206
+ this.#handlers[event] = this.#handlers[event].filter(h => h !== handler)
207
+ } else {
208
+ delete this.#handlers[event]
209
+ }
210
+ return this
211
+ }
212
+
213
+ send(data) {
214
+ if (this.#closed) return this
215
+ const payload = typeof data === 'object' ? JSON.stringify(data) : String(data)
216
+ this.#sendFrame(OPCODE.TEXT, payload)
217
+ return this
218
+ }
219
+
220
+ sendBinary(data) {
221
+ if (this.#closed) return this
222
+ this.#sendFrame(OPCODE.BINARY, data)
223
+ return this
224
+ }
225
+
226
+ ping() {
227
+ this.#sendFrame(OPCODE.PING, Buffer.alloc(0))
228
+ return this
229
+ }
230
+
231
+ close(code = 1000, reason = '') {
232
+ if (this.#closed) return
233
+ this.#closed = true
234
+
235
+ const reasonBuffer = Buffer.from(reason)
236
+ const payload = Buffer.alloc(2 + reasonBuffer.length)
237
+ payload.writeUInt16BE(code, 0)
238
+ reasonBuffer.copy(payload, 2)
239
+
240
+ this.#sendFrame(OPCODE.CLOSE, payload)
241
+ this.#socket.end()
242
+
243
+ for (const room of this.#rooms) {
244
+ this.#server.leaveRoom(this.#id, room)
245
+ }
246
+
247
+ this.#emit('close')
248
+ this.#server.removeClient(this.#id)
249
+ }
250
+
251
+ join(room) {
252
+ this.#rooms.add(room)
253
+ this.#server.joinRoom(this.#id, room)
254
+ return this
255
+ }
256
+
257
+ leave(room) {
258
+ this.#rooms.delete(room)
259
+ this.#server.leaveRoom(this.#id, room)
260
+ return this
261
+ }
262
+
263
+ to(room) {
264
+ return {
265
+ send: data => this.#server.toRoom(room, data),
266
+ sendBinary: data => this.#server.toRoomBinary(room, data)
267
+ }
268
+ }
269
+
270
+ broadcast(data, includesSelf = false) {
271
+ this.#server.broadcast(data, includesSelf ? null : this.#id)
272
+ return this
273
+ }
274
+ }
275
+
276
+ class WebSocketServer {
277
+ #clients = new Map()
278
+ #rooms = new Map()
279
+ #routes = new Map()
280
+
281
+ route(path, handler) {
282
+ this.#routes.set(path, handler)
283
+ }
284
+
285
+ getRoute(path) {
286
+ if (this.#routes.has(path)) return this.#routes.get(path)
287
+
288
+ for (const [pattern, handler] of this.#routes) {
289
+ if (!pattern.includes('{')) continue
290
+ const regex = new RegExp('^' + pattern.replace(/\{[^}]+\}/g, '([^/]+)') + '$')
291
+ const match = path.match(regex)
292
+ if (match) {
293
+ const params = {}
294
+ const paramNames = pattern.match(/\{([^}]+)\}/g) || []
295
+ paramNames.forEach((name, i) => {
296
+ params[name.slice(1, -1)] = match[i + 1]
297
+ })
298
+ return {handler, params}
299
+ }
300
+ }
301
+ return null
302
+ }
303
+
304
+ handleUpgrade(req, socket, head, Odac) {
305
+ const path = req.url.split('?')[0]
306
+ const routeInfo = this.getRoute(path)
307
+
308
+ if (!routeInfo) {
309
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n')
310
+ socket.destroy()
311
+ return
312
+ }
313
+
314
+ const handler = typeof routeInfo === 'function' ? routeInfo : routeInfo.handler
315
+ const params = routeInfo.params || {}
316
+
317
+ const key = req.headers['sec-websocket-key']
318
+ if (!key) {
319
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n')
320
+ socket.destroy()
321
+ return
322
+ }
323
+
324
+ const acceptKey = nodeCrypto
325
+ .createHash('sha1')
326
+ .update(key + WS_GUID)
327
+ .digest('base64')
328
+
329
+ const responseHeaders = [
330
+ 'HTTP/1.1 101 Switching Protocols',
331
+ 'Upgrade: websocket',
332
+ 'Connection: Upgrade',
333
+ `Sec-WebSocket-Accept: ${acceptKey}`,
334
+ '',
335
+ ''
336
+ ]
337
+
338
+ socket.write(responseHeaders.join('\r\n'))
339
+
340
+ const clientId = nodeCrypto.randomUUID()
341
+ const client = new WebSocketClient(socket, this, clientId)
342
+ this.#clients.set(clientId, client)
343
+
344
+ if (params) {
345
+ for (const [k, v] of Object.entries(params)) {
346
+ Odac.Request.data.url[k] = v
347
+ }
348
+ }
349
+
350
+ if (Odac.Request && req.headers) {
351
+ Odac.Request._wsHeaders = req.headers
352
+ }
353
+
354
+ handler(client, Odac)
355
+ }
356
+
357
+ removeClient(id) {
358
+ this.#clients.delete(id)
359
+ }
360
+
361
+ joinRoom(clientId, room) {
362
+ if (!this.#rooms.has(room)) this.#rooms.set(room, new Set())
363
+ this.#rooms.get(room).add(clientId)
364
+ }
365
+
366
+ leaveRoom(clientId, room) {
367
+ if (!this.#rooms.has(room)) return
368
+ this.#rooms.get(room).delete(clientId)
369
+ if (this.#rooms.get(room).size === 0) this.#rooms.delete(room)
370
+ }
371
+
372
+ toRoom(room, data) {
373
+ if (!this.#rooms.has(room)) return
374
+ for (const clientId of this.#rooms.get(room)) {
375
+ const client = this.#clients.get(clientId)
376
+ if (client) client.send(data)
377
+ }
378
+ }
379
+
380
+ toRoomBinary(room, data) {
381
+ if (!this.#rooms.has(room)) return
382
+ for (const clientId of this.#rooms.get(room)) {
383
+ const client = this.#clients.get(clientId)
384
+ if (client) client.sendBinary(data)
385
+ }
386
+ }
387
+
388
+ broadcast(data, excludeId = null) {
389
+ for (const [id, client] of this.#clients) {
390
+ if (id !== excludeId) client.send(data)
391
+ }
392
+ }
393
+
394
+ get clients() {
395
+ return this.#clients
396
+ }
397
+
398
+ get clientCount() {
399
+ return this.#clients.size
400
+ }
401
+ }
402
+
403
+ module.exports = {WebSocketServer, WebSocketClient}
@@ -0,0 +1,5 @@
1
+ {
2
+ "route": {
3
+ "/assets/js/odac.js": "${odac}/framework/web/odac.js"
4
+ }
5
+ }
@@ -1,23 +1,23 @@
1
1
  /**
2
2
  * About Page Controller
3
3
  *
4
- * This controller renders the about page using CandyPack's skeleton-based view system.
5
- * Provides information about CandyPack and its key components.
4
+ * This controller renders the about page using Odac's skeleton-based view system.
5
+ * Provides information about Odac and its key components.
6
6
  *
7
7
  * For AJAX requests, only content is returned. For full page loads, skeleton + content.
8
8
  */
9
9
 
10
- module.exports = function (Candy) {
10
+ module.exports = function (Odac) {
11
11
  // Set variables for AJAX responses
12
- Candy.set(
12
+ Odac.set(
13
13
  {
14
- pageTitle: 'About CandyPack',
14
+ pageTitle: 'About Odac',
15
15
  version: '1.0.0'
16
16
  },
17
17
  true
18
18
  )
19
19
 
20
- Candy.View.set({
20
+ Odac.View.set({
21
21
  skeleton: 'main',
22
22
  head: 'main',
23
23
  header: 'main',
@@ -1,30 +1,30 @@
1
1
  /**
2
2
  * Home Page Controller
3
3
  *
4
- * This controller renders the home page using CandyPack's skeleton-based view system.
4
+ * This controller renders the home page using Odac's skeleton-based view system.
5
5
  * The skeleton provides the layout (header, nav, footer) and the view provides the content.
6
6
  *
7
- * For AJAX requests (candy-link navigation), only the content is returned.
7
+ * For AJAX requests (odac-link navigation), only the content is returned.
8
8
  * For full page loads, skeleton + content is returned.
9
9
  *
10
10
  * This page demonstrates:
11
11
  * - Modern, responsive design
12
- * - candy.js AJAX form handling
13
- * - candy.js GET requests
14
- * - Dynamic page loading with candy-link
12
+ * - odac.js AJAX form handling
13
+ * - odac.js GET requests
14
+ * - Dynamic page loading with odac-link
15
15
  */
16
16
 
17
- module.exports = function (Candy) {
17
+ module.exports = function (Odac) {
18
18
  // Set variables that will be available in AJAX responses
19
- Candy.set(
19
+ Odac.set(
20
20
  {
21
- welcomeMessage: 'Welcome to CandyPack!',
21
+ welcomeMessage: 'Welcome to Odac!',
22
22
  timestamp: Date.now()
23
23
  },
24
24
  true
25
25
  ) // true = include in AJAX responses
26
26
 
27
- Candy.View.set({
27
+ Odac.View.set({
28
28
  skeleton: 'main',
29
29
  head: 'main',
30
30
  header: 'main',
@@ -3,16 +3,15 @@
3
3
  "version": "1.0.0",
4
4
  "description": "Website for {{domain_original}}",
5
5
  "scripts": {
6
- "start": "candy framework run",
7
- "test": "echo \"Error: no test specified\" && exit 1"
6
+ "dev": "odac dev"
8
7
  },
9
8
  "dependencies": {
10
- "candypack": "*"
9
+ "odac": "*"
11
10
  },
12
11
  "keywords": [
13
- "candypack",
12
+ "odac",
14
13
  "website"
15
14
  ],
16
15
  "author": "",
17
16
  "license": "ISC"
18
- }
17
+ }
@@ -1,5 +1,5 @@
1
1
  /* ============================================
2
- CandyPack Template Stylesheet
2
+ Odac Template Stylesheet
3
3
  Modern, responsive design with smooth transitions
4
4
  ============================================ */
5
5
 
@@ -536,7 +536,7 @@ textarea.error {
536
536
  display: block;
537
537
  }
538
538
 
539
- [candy-form-success] {
539
+ [odac-form-success] {
540
540
  background-color: var(--accent);
541
541
  color: var(--white);
542
542
  padding: var(--spacing-sm);
@@ -545,7 +545,7 @@ textarea.error {
545
545
  display: none;
546
546
  }
547
547
 
548
- [candy-form-success]:not(:empty) {
548
+ [odac-form-success]:not(:empty) {
549
549
  display: block;
550
550
  }
551
551
 
@@ -1333,7 +1333,7 @@ footer a:hover {
1333
1333
  }
1334
1334
 
1335
1335
  /* Smooth page transitions */
1336
- [candy-page] {
1336
+ [odac-page] {
1337
1337
  animation: fadeIn 0.4s ease-out;
1338
1338
  }
1339
1339
 
@@ -1,13 +1,13 @@
1
1
  /**
2
- * CandyPack Template - Client-Side Application
2
+ * Odac Template - Client-Side Application
3
3
  *
4
- * This file demonstrates candy.js features including:
5
- * - AJAX page loading with Candy.loader() for smooth navigation
4
+ * This file demonstrates odac.js features including:
5
+ * - AJAX page loading with odac.loader() for smooth navigation
6
6
  * - History API integration
7
7
  * - Event delegation
8
8
  */
9
9
 
10
- Candy.action({
10
+ odac.action({
11
11
  /**
12
12
  * AJAX Navigation
13
13
  * Enables smooth page transitions without full page reloads
@@ -22,7 +22,7 @@ Candy.action({
22
22
 
23
23
  /**
24
24
  * Custom functions
25
- * These become available as Candy.fn.functionName()
25
+ * These become available as odac.fn.functionName()
26
26
  */
27
27
  function: {
28
28
  /**
@@ -56,7 +56,7 @@ Candy.action({
56
56
  */
57
57
  load: function () {
58
58
  // Set initial active navigation state
59
- Candy.fn.updateActiveNav(window.location.pathname)
59
+ odac.fn.updateActiveNav(window.location.pathname)
60
60
  },
61
61
 
62
62
  /**
@@ -1,19 +1,19 @@
1
1
  // ============================================
2
2
  // Page Routes
3
3
  // ============================================
4
- // Page routes render HTML views and support AJAX loading via candy.js
4
+ // Page routes render HTML views and support AJAX loading via odac.js
5
5
  // Controllers are located in controller/page/ directory
6
6
 
7
7
  // Home page - displays welcome message, features, and interactive demos
8
- Candy.Route.page('/', 'index')
8
+ Odac.Route.page('/', 'index')
9
9
 
10
- // About page - provides information about CandyPack
11
- Candy.Route.page('/about', 'about')
10
+ // About page - provides information about Odac
11
+ Odac.Route.page('/about', 'about')
12
12
 
13
13
  // ============================================
14
14
  // API Routes
15
15
  // ============================================
16
16
  // Add your API routes here
17
17
  // Example:
18
- // Candy.Route.post('/api/contact', 'contact')
19
- // Candy.Route.get('/api/data', 'data')
18
+ // Odac.Route.post('/api/contact', 'contact')
19
+ // Odac.Route.get('/api/data', 'data')
@@ -16,7 +16,7 @@
16
16
  {{ FOOTER }}
17
17
  </footer>
18
18
 
19
- <script src="/assets/js/candy.js"></script>
19
+ <script src="/assets/js/odac.js"></script>
20
20
  <script src="/assets/js/app.js"></script>
21
21
  </body>
22
22
  </html>
@@ -1,13 +1,13 @@
1
1
  <div class="container">
2
2
  <section class="page-hero">
3
3
  <h1>🍭 About This Template</h1>
4
- <p class="lead">This is a starter template for your CandyPack website. Customize it to build your own application.</p>
4
+ <p class="lead">This is a starter template for your Odac website. Customize it to build your own application.</p>
5
5
  </section>
6
6
 
7
7
  <section class="content-section">
8
8
  <h2>What You Can Build</h2>
9
9
  <p>
10
- With CandyPack, you can build any type of web application - from simple websites to complex web apps.
10
+ With Odac, you can build any type of web application - from simple websites to complex web apps.
11
11
  This template provides a solid foundation with modern features and best practices already configured.
12
12
  </p>
13
13
  </section>
@@ -51,13 +51,13 @@
51
51
  <section class="content-section">
52
52
  <h2>Learn More</h2>
53
53
  <div class="links-grid">
54
- <a href="https://docs.candypack.dev" class="link-card" target="_blank" data-navigate="false">
54
+ <a href="https://docs.odac.run" class="link-card" target="_blank" data-navigate="false">
55
55
  <h3>📚 Documentation</h3>
56
56
  <p>Complete guides, API reference, and tutorials</p>
57
57
  </a>
58
58
 
59
- <a href="https://candypack.dev" class="link-card" target="_blank" data-navigate="false">
60
- <h3>🌐 CandyPack.dev</h3>
59
+ <a href="https://odac.run" class="link-card" target="_blank" data-navigate="false">
60
+ <h3>🌐 odac.run</h3>
61
61
  <p>Official website with examples and community</p>
62
62
  </a>
63
63
  </div>