odac 0.9.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 (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,535 @@
1
+ const {log} = Candy.core('Log', false).init('Hub')
2
+
3
+ const axios = require('axios')
4
+ const nodeCrypto = require('crypto')
5
+ const os = require('os')
6
+ const fs = require('fs')
7
+
8
+ class Hub {
9
+ constructor() {
10
+ this.websocket = null
11
+ this.httpInterval = null
12
+ this.websocketReconnectAttempts = 0
13
+ this.maxReconnectAttempts = 5
14
+ this.lastNetworkStats = null
15
+ this.lastNetworkTime = null
16
+
17
+ this.startHttpPolling()
18
+ }
19
+
20
+ startHttpPolling() {
21
+ if (this.httpInterval) {
22
+ return
23
+ }
24
+
25
+ log('Starting HTTP polling (60s interval)')
26
+ this.check()
27
+ this.httpInterval = setInterval(() => {
28
+ if (!this.websocket) {
29
+ this.check()
30
+ }
31
+ }, 10000)
32
+ }
33
+
34
+ stopHttpPolling() {
35
+ if (this.httpInterval) {
36
+ log('Stopping HTTP polling')
37
+ clearInterval(this.httpInterval)
38
+ this.httpInterval = null
39
+ }
40
+ }
41
+
42
+ async check() {
43
+ const hub = Candy.core('Config').config.hub
44
+ if (!hub || !hub.token) {
45
+ return
46
+ }
47
+
48
+ try {
49
+ const status = this.getSystemStatus()
50
+ status.timestamp = Math.floor(Date.now() / 1000)
51
+
52
+ const response = await this.call('status', status)
53
+
54
+ if (!response.authenticated) {
55
+ log('Server not authenticated: %s', response.reason || 'unknown')
56
+ if (response.reason === 'token_invalid' || response.reason === 'signature_invalid') {
57
+ log('Authentication credentials invalid, clearing config')
58
+ delete Candy.core('Config').config.hub
59
+ }
60
+ return
61
+ }
62
+
63
+ if (response.websocket && !this.websocket) {
64
+ log('WebSocket requested by cloud')
65
+ this.connectWebSocket(response.websocketUrl, response.websocketToken)
66
+ }
67
+ } catch (error) {
68
+ log('Failed to report status: %s', error)
69
+ }
70
+ }
71
+
72
+ connectWebSocket(url, token) {
73
+ if (this.websocket) {
74
+ log('WebSocket already connected')
75
+ return
76
+ }
77
+
78
+ try {
79
+ const WebSocket = require('ws')
80
+ const wsUrl = `${url}?token=${token}`
81
+
82
+ log('Connecting to WebSocket: %s', url)
83
+ this.websocket = new WebSocket(wsUrl, {
84
+ rejectUnauthorized: true
85
+ })
86
+
87
+ this.websocket.on('open', () => {
88
+ log('WebSocket connected')
89
+ this.websocketReconnectAttempts = 0
90
+ this.stopHttpPolling()
91
+ this.sendWebSocketStatus()
92
+ })
93
+
94
+ this.websocket.on('message', data => {
95
+ this.handleWebSocketMessage(data)
96
+ })
97
+
98
+ this.websocket.on('close', () => {
99
+ log('WebSocket disconnected')
100
+ this.websocket = null
101
+ this.startHttpPolling()
102
+ })
103
+
104
+ this.websocket.on('error', error => {
105
+ log('WebSocket error: %s', error.message)
106
+ })
107
+ } catch (error) {
108
+ log('Failed to connect WebSocket: %s', error.message)
109
+ this.websocket = null
110
+ this.startHttpPolling()
111
+ }
112
+ }
113
+
114
+ disconnectWebSocket() {
115
+ if (this.websocket) {
116
+ log('Disconnecting WebSocket')
117
+ this.websocket.close()
118
+ this.websocket = null
119
+ this.startHttpPolling()
120
+ }
121
+ }
122
+
123
+ sendWebSocketStatus() {
124
+ if (!this.websocket || this.websocket.readyState !== 1) {
125
+ return
126
+ }
127
+
128
+ const status = this.getSystemStatus()
129
+ const timestamp = Math.floor(Date.now() / 1000)
130
+
131
+ const message = {
132
+ type: 'status',
133
+ data: status,
134
+ timestamp: timestamp,
135
+ signature: this.signWebSocketMessage({type: 'status', data: status, timestamp})
136
+ }
137
+
138
+ this.websocket.send(JSON.stringify(message))
139
+ }
140
+
141
+ handleWebSocketMessage(data) {
142
+ try {
143
+ const message = JSON.parse(data.toString())
144
+
145
+ if (message.type === 'disconnect') {
146
+ log('Cloud requested disconnect: %s', message.reason || 'unknown')
147
+ this.disconnectWebSocket()
148
+ return
149
+ }
150
+
151
+ if (message.type === 'command') {
152
+ if (this.verifyWebSocketMessage(message)) {
153
+ this.processCommand(message.data)
154
+ } else {
155
+ log('WebSocket message verification failed')
156
+ }
157
+ }
158
+ } catch (error) {
159
+ log('Failed to handle WebSocket message: %s', error.message)
160
+ }
161
+ }
162
+
163
+ signWebSocketMessage(message) {
164
+ const hub = Candy.core('Config').config.hub
165
+ if (!hub || !hub.secret) {
166
+ return null
167
+ }
168
+
169
+ const payload = JSON.stringify({type: message.type, data: message.data, timestamp: message.timestamp})
170
+ return nodeCrypto.createHmac('sha256', hub.secret).update(payload).digest('hex')
171
+ }
172
+
173
+ verifyWebSocketMessage(message) {
174
+ const {type, data, timestamp, signature} = message
175
+
176
+ if (!signature || !timestamp) {
177
+ log('Missing signature or timestamp in WebSocket message')
178
+ return false
179
+ }
180
+
181
+ const now = Math.floor(Date.now() / 1000)
182
+ if (Math.abs(now - timestamp) > 300) {
183
+ log('WebSocket message timestamp too old or in future')
184
+ return false
185
+ }
186
+
187
+ const expectedSignature = this.signWebSocketMessage({type, data, timestamp})
188
+ if (signature !== expectedSignature) {
189
+ log('Invalid WebSocket message signature')
190
+ return false
191
+ }
192
+
193
+ return true
194
+ }
195
+
196
+ processCommand(command) {
197
+ log('Processing command: %s', command.action)
198
+ }
199
+
200
+ getSystemStatus() {
201
+ const totalMem = os.totalmem()
202
+ const freeMem = os.freemem()
203
+ const diskInfo = this.getDiskUsage()
204
+ const networkInfo = this.getNetworkUsage()
205
+ const servicesInfo = this.getServicesInfo()
206
+
207
+ const serverStarted = Candy.core('Config').config.server.started
208
+ const candypackUptime = serverStarted ? Math.floor((Date.now() - serverStarted) / 1000) : 0
209
+
210
+ return {
211
+ cpu: this.getCpuUsage(),
212
+ memory: {
213
+ used: totalMem - freeMem,
214
+ total: totalMem
215
+ },
216
+ disk: diskInfo,
217
+ network: networkInfo,
218
+ services: servicesInfo,
219
+ uptime: candypackUptime,
220
+ hostname: os.hostname(),
221
+ platform: os.platform(),
222
+ arch: os.arch(),
223
+ node: process.version
224
+ }
225
+ }
226
+
227
+ getServicesInfo() {
228
+ try {
229
+ const config = Candy.core('Config').config
230
+
231
+ const websites = config.websites ? Object.keys(config.websites).length : 0
232
+ const services = config.services ? config.services.length : 0
233
+ const mailAccounts = config.mail && config.mail.accounts ? Object.keys(config.mail.accounts).length : 0
234
+
235
+ return {
236
+ websites: websites,
237
+ services: services,
238
+ mail: mailAccounts
239
+ }
240
+ } catch (error) {
241
+ log('Failed to get services info: %s', error.message)
242
+ return {
243
+ websites: 0,
244
+ services: 0,
245
+ mail: 0
246
+ }
247
+ }
248
+ }
249
+
250
+ getDiskUsage() {
251
+ try {
252
+ const {execSync} = require('child_process')
253
+ let command
254
+
255
+ if (os.platform() === 'win32') {
256
+ command = 'wmic logicaldisk get size,freespace,caption'
257
+ } else {
258
+ command = "df -k / | tail -1 | awk '{print $2,$3}'"
259
+ }
260
+
261
+ const output = execSync(command, {encoding: 'utf8'})
262
+
263
+ if (os.platform() === 'win32') {
264
+ const lines = output.trim().split('\n')
265
+ if (lines.length > 1) {
266
+ const parts = lines[1].trim().split(/\s+/)
267
+ const free = parseInt(parts[1]) || 0
268
+ const total = parseInt(parts[2]) || 0
269
+ return {
270
+ used: total - free,
271
+ total: total
272
+ }
273
+ }
274
+ } else {
275
+ const parts = output.trim().split(/\s+/)
276
+ const total = parseInt(parts[0]) * 1024
277
+ const used = parseInt(parts[1]) * 1024
278
+ return {
279
+ used: used,
280
+ total: total
281
+ }
282
+ }
283
+ } catch (error) {
284
+ log('Failed to get disk usage: %s', error.message)
285
+ }
286
+
287
+ return {
288
+ used: 0,
289
+ total: 0
290
+ }
291
+ }
292
+
293
+ getNetworkUsage() {
294
+ try {
295
+ const {execSync} = require('child_process')
296
+ let command
297
+
298
+ if (os.platform() === 'win32') {
299
+ command = 'netstat -e'
300
+ } else if (os.platform() === 'darwin') {
301
+ command = "netstat -ib | grep -e 'en0' | head -1 | awk '{print $7,$10}'"
302
+ } else {
303
+ command = "cat /proc/net/dev | grep -E 'eth0|ens|enp' | head -1 | awk '{print $2,$10}'"
304
+ }
305
+
306
+ const output = execSync(command, {encoding: 'utf8', timeout: 5000})
307
+ let currentStats = {received: 0, sent: 0}
308
+
309
+ if (os.platform() === 'win32') {
310
+ const lines = output.split('\n')
311
+ for (const line of lines) {
312
+ if (line.includes('Bytes')) {
313
+ const parts = line.trim().split(/\s+/)
314
+ currentStats.received = parseInt(parts[1]) || 0
315
+ currentStats.sent = parseInt(parts[2]) || 0
316
+ break
317
+ }
318
+ }
319
+ } else {
320
+ const parts = output.trim().split(/\s+/)
321
+ currentStats.received = parseInt(parts[0]) || 0
322
+ currentStats.sent = parseInt(parts[1]) || 0
323
+ }
324
+
325
+ const now = Date.now()
326
+
327
+ if (this.lastNetworkStats && this.lastNetworkTime) {
328
+ const timeDiff = (now - this.lastNetworkTime) / 1000
329
+ const receivedDiff = currentStats.received - this.lastNetworkStats.received
330
+ const sentDiff = currentStats.sent - this.lastNetworkStats.sent
331
+
332
+ const bandwidth = {
333
+ download: Math.max(0, Math.round(receivedDiff / timeDiff)),
334
+ upload: Math.max(0, Math.round(sentDiff / timeDiff))
335
+ }
336
+
337
+ this.lastNetworkStats = currentStats
338
+ this.lastNetworkTime = now
339
+
340
+ return bandwidth
341
+ }
342
+
343
+ this.lastNetworkStats = currentStats
344
+ this.lastNetworkTime = now
345
+
346
+ return {
347
+ download: 0,
348
+ upload: 0
349
+ }
350
+ } catch (error) {
351
+ log('Failed to get network usage: %s', error.message)
352
+ }
353
+
354
+ return {
355
+ download: 0,
356
+ upload: 0
357
+ }
358
+ }
359
+
360
+ getCpuUsage() {
361
+ const cpus = os.cpus()
362
+ let totalIdle = 0
363
+ let totalTick = 0
364
+
365
+ for (const cpu of cpus) {
366
+ for (const type in cpu.times) {
367
+ totalTick += cpu.times[type]
368
+ }
369
+ totalIdle += cpu.times.idle
370
+ }
371
+
372
+ const idle = totalIdle / cpus.length
373
+ const total = totalTick / cpus.length
374
+ const usage = 100 - ~~((100 * idle) / total)
375
+
376
+ return usage
377
+ }
378
+
379
+ getLinuxDistro() {
380
+ log('Getting Linux distro info...')
381
+ if (os.platform() !== 'linux') {
382
+ log('Platform is not Linux: %s', os.platform())
383
+ return null
384
+ }
385
+
386
+ try {
387
+ log('Reading /etc/os-release...')
388
+ const osRelease = fs.readFileSync('/etc/os-release', 'utf8')
389
+ const lines = osRelease.split('\n')
390
+ const distro = {}
391
+
392
+ for (const line of lines) {
393
+ const [key, value] = line.split('=')
394
+ if (key && value) {
395
+ distro[key] = value.replace(/"/g, '')
396
+ }
397
+ }
398
+
399
+ const result = {
400
+ name: distro.NAME || distro.ID || 'Unknown',
401
+ version: distro.VERSION_ID || distro.VERSION || 'Unknown',
402
+ id: distro.ID || 'unknown'
403
+ }
404
+ log('Distro detected: %s %s', result.name, result.version)
405
+ return result
406
+ } catch (err) {
407
+ log('Failed to read distro info: %s', err.message)
408
+ return null
409
+ }
410
+ }
411
+
412
+ async auth(code) {
413
+ log('CandyPack authenticating...')
414
+ log('Auth code received: %s', code ? code.substring(0, 8) + '...' : 'none')
415
+ const packageJson = require('../../package.json')
416
+ const distro = this.getLinuxDistro()
417
+
418
+ let data = {
419
+ code: code,
420
+ os: os.platform(),
421
+ arch: os.arch(),
422
+ hostname: os.hostname(),
423
+ version: packageJson.version,
424
+ node: process.version
425
+ }
426
+
427
+ log('Auth data prepared: os=%s, arch=%s, hostname=%s, version=%s', data.os, data.arch, data.hostname, data.version)
428
+
429
+ if (distro) {
430
+ data.distro = distro
431
+ log('Distro info added to auth data')
432
+ }
433
+ try {
434
+ log('Calling hub API for authentication...')
435
+ const response = await this.call('auth', data)
436
+ let token = response.token
437
+ let secret = response.secret
438
+ log('Token received: %s...', token ? token.substring(0, 8) : 'none')
439
+ Candy.core('Config').config.hub = {token: token, secret: secret}
440
+ log('CandyPack authenticated!')
441
+ return Candy.server('Api').result(true, __('Authentication successful'))
442
+ } catch (error) {
443
+ log('Authentication failed: %s', error ? error : 'Unknown error')
444
+ return Candy.server('Api').result(false, error || __('Authentication failed'))
445
+ }
446
+ }
447
+
448
+ signRequest(data) {
449
+ const hub = Candy.core('Config').config.hub
450
+ if (!hub || !hub.secret) {
451
+ return null
452
+ }
453
+
454
+ const signature = nodeCrypto.createHmac('sha256', hub.secret).update(JSON.stringify(data)).digest('hex')
455
+
456
+ return signature
457
+ }
458
+
459
+ call(action, data) {
460
+ log('Hub API call: %s', action)
461
+ return new Promise((resolve, reject) => {
462
+ const url = 'https://hub.candypack.dev/' + action
463
+ log('POST request to: %s', url)
464
+
465
+ const headers = {}
466
+ const hub = Candy.core('Config').config.hub
467
+ if (hub && hub.token) {
468
+ headers['Authorization'] = `Bearer ${hub.token}`
469
+ }
470
+
471
+ if (action !== 'auth' && data.timestamp) {
472
+ const signature = this.signRequest(data)
473
+ if (signature) {
474
+ headers['X-Signature'] = signature
475
+ }
476
+ }
477
+
478
+ axios
479
+ .post(url, data, {
480
+ headers,
481
+ httpsAgent: new (require('https').Agent)({
482
+ rejectUnauthorized: true
483
+ })
484
+ })
485
+ .then(response => {
486
+ log('Raw response received for %s', action)
487
+ log('Response structure: %j', {
488
+ hasData: !!response.data,
489
+ hasResult: !!(response.data && response.data.result),
490
+ dataKeys: response.data ? Object.keys(response.data) : []
491
+ })
492
+
493
+ if (!response.data) {
494
+ log('Response has no data')
495
+ return reject('Invalid response: no data')
496
+ }
497
+
498
+ if (!response.data.result) {
499
+ log('Response has no result field')
500
+ return reject('Invalid response: no result field')
501
+ }
502
+
503
+ if (!response.data.result.success) {
504
+ log('API returned error: %s', response.data.result.message)
505
+
506
+ if (response.data.result.authenticated === false) {
507
+ log('Authentication failed, returning result for handling')
508
+ return resolve(response.data.result)
509
+ }
510
+
511
+ return reject(response.data.result.message)
512
+ }
513
+
514
+ log('API call successful: %s', action)
515
+ resolve(response.data.data)
516
+ })
517
+ .catch(error => {
518
+ log('API call failed: %s - %s', action, error.message)
519
+ if (error.response) {
520
+ log('Error response status: %s', error.response.status)
521
+ log('Error response data: %j', error.response.data)
522
+ reject(error.response.data)
523
+ } else if (error.request) {
524
+ log('No response received, request was made')
525
+ reject('No response from server')
526
+ } else {
527
+ log('Request setup error: %s', error.message)
528
+ reject(error.message)
529
+ }
530
+ })
531
+ })
532
+ }
533
+ }
534
+
535
+ module.exports = new Hub()