@gokiteam/goki-dev 0.2.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 (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,24 @@
1
+ export const Reply = () => {
2
+ return async (ctx, next) => {
3
+ ctx.reply = (data, status = 200) => {
4
+ ctx.status = status
5
+ ctx.body = {
6
+ success: true,
7
+ status,
8
+ message: getDefaultMessage(status),
9
+ data
10
+ }
11
+ }
12
+ await next()
13
+ }
14
+ }
15
+
16
+ const getDefaultMessage = (status) => {
17
+ const messages = {
18
+ 200: 'The request succeeded.',
19
+ 201: 'The resource was created successfully.',
20
+ 202: 'The request was accepted for processing.',
21
+ 204: 'The request succeeded with no content to return.'
22
+ }
23
+ return messages[status] || 'Request completed.'
24
+ }
@@ -0,0 +1,9 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ export const TraceId = () => {
4
+ return async (ctx, next) => {
5
+ const traceId = ctx.request.body?.traceId || ctx.headers['x-trace-id'] || randomUUID()
6
+ ctx.state.traceId = traceId
7
+ await next()
8
+ }
9
+ }
@@ -0,0 +1,234 @@
1
+ import zlib from 'zlib'
2
+ import { WebhookProxy } from '../singletons/WebhookProxy.js'
3
+ import { Logger } from '../singletons/Logger.js'
4
+
5
+ const MAX_BODY_LOG_SIZE = 8192
6
+
7
+ function tryParseJson (str) {
8
+ if (!str || typeof str !== 'string') return null
9
+ const trimmed = str.trim()
10
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
11
+ try { return JSON.parse(trimmed) } catch { return null }
12
+ }
13
+ return null
14
+ }
15
+
16
+ function truncateBody (body, maxSize = MAX_BODY_LOG_SIZE) {
17
+ if (!body) return null
18
+ const str = typeof body === 'string' ? body : JSON.stringify(body)
19
+ if (str.length > maxSize) {
20
+ return str.substring(0, maxSize) + `... [truncated, total ${str.length} bytes]`
21
+ }
22
+ return tryParseJson(str) || str
23
+ }
24
+
25
+ function decompressBuffer (buffer, encoding) {
26
+ if (!encoding) return buffer
27
+ try {
28
+ if (encoding === 'gzip' || encoding === 'x-gzip') {
29
+ return zlib.gunzipSync(buffer)
30
+ }
31
+ if (encoding === 'deflate') {
32
+ return zlib.inflateSync(buffer)
33
+ }
34
+ if (encoding === 'br') {
35
+ return zlib.brotliDecompressSync(buffer)
36
+ }
37
+ } catch {
38
+ return buffer
39
+ }
40
+ return buffer
41
+ }
42
+
43
+ function collectStreamBody (stream, contentEncoding) {
44
+ return new Promise((resolve) => {
45
+ const chunks = []
46
+ let size = 0
47
+ stream.on('data', (chunk) => {
48
+ if (size < MAX_BODY_LOG_SIZE) {
49
+ chunks.push(chunk)
50
+ }
51
+ size += chunk.length
52
+ })
53
+ stream.on('end', () => {
54
+ try {
55
+ const raw = decompressBuffer(Buffer.concat(chunks), contentEncoding).toString('utf-8')
56
+ resolve({ raw, totalSize: size })
57
+ } catch {
58
+ resolve({ raw: null, totalSize: size })
59
+ }
60
+ })
61
+ stream.on('error', () => {
62
+ resolve({ raw: null, totalSize: size })
63
+ })
64
+ })
65
+ }
66
+
67
+ function bufferRequestBody (req) {
68
+ return new Promise((resolve) => {
69
+ const chunks = []
70
+ let size = 0
71
+ req.on('data', (chunk) => {
72
+ chunks.push(chunk)
73
+ size += chunk.length
74
+ })
75
+ req.on('end', () => {
76
+ try {
77
+ resolve(Buffer.concat(chunks))
78
+ } catch {
79
+ resolve(null)
80
+ }
81
+ })
82
+ req.on('error', () => {
83
+ resolve(null)
84
+ })
85
+ // If already consumed or no body, resolve immediately
86
+ if (req.readableEnded || req.complete) {
87
+ resolve(null)
88
+ }
89
+ })
90
+ }
91
+
92
+ export const WebhookProxyMiddleware = () => {
93
+ return async (ctx, next) => {
94
+ // Handle both /v1/webhooks/proxy/* (explicit routes) and /v1/webhooks/* (auto-routing)
95
+ if (!ctx.path.startsWith('/v1/webhooks/')) {
96
+ return next()
97
+ }
98
+ let resolved = WebhookProxy.resolveTarget(ctx.path)
99
+ if (!resolved) {
100
+ // Auto-route to gateway-webhook if path matches /v1/webhooks/{endpoint}
101
+ const webhookMatch = ctx.path.match(/^\/v1\/webhooks\/([^/?]+)/)
102
+ if (webhookMatch) {
103
+ const endpoint = webhookMatch[1]
104
+ // Skip 'proxy' prefix - those use explicit routing
105
+ if (endpoint !== 'proxy') {
106
+ resolved = {
107
+ target: 'http://gateway-webhook-app:3000',
108
+ path: ctx.path,
109
+ fullUrl: `http://gateway-webhook-app:3000${ctx.path}`,
110
+ prefix: endpoint,
111
+ isAutoRouted: true
112
+ }
113
+ Logger.log({
114
+ level: 'info',
115
+ message: 'Webhook proxy: auto-routing to gateway-webhook',
116
+ data: { method: ctx.method, path: ctx.path, endpoint, target: resolved.target }
117
+ })
118
+ }
119
+ }
120
+ if (!resolved) {
121
+ Logger.log({
122
+ level: 'warn',
123
+ message: 'Webhook proxy: no route configured',
124
+ data: { method: ctx.method, path: ctx.path }
125
+ })
126
+ ctx.status = 404
127
+ ctx.body = {
128
+ success: false,
129
+ status: 404,
130
+ message: 'No webhook route configured for this path',
131
+ data: { path: ctx.path }
132
+ }
133
+ return
134
+ }
135
+ }
136
+ const startTime = Date.now()
137
+ const queryParams = ctx.querystring ? Object.fromEntries(new URLSearchParams(ctx.querystring)) : null
138
+ // Buffer the raw request body so we can log it AND pass it to the proxy
139
+ const rawBodyBuffer = await bufferRequestBody(ctx.req)
140
+ const requestBodyStr = rawBodyBuffer ? rawBodyBuffer.toString('utf-8') : null
141
+ Logger.log({
142
+ level: 'info',
143
+ message: `Webhook IN: ${ctx.method} ${ctx.path} → ${resolved.fullUrl}`,
144
+ data: {
145
+ method: ctx.method,
146
+ path: ctx.path,
147
+ target: resolved.fullUrl,
148
+ queryParams,
149
+ requestBody: truncateBody(requestBodyStr)
150
+ }
151
+ })
152
+ // Bypass Koa response handling
153
+ ctx.respond = false
154
+ // Rewrite the URL before proxying
155
+ ctx.req.url = resolved.path + (ctx.querystring ? '?' + ctx.querystring : '')
156
+ // Create a readable stream from the buffered body so http-proxy can forward it
157
+ const { Readable } = await import('stream')
158
+ const bodyStream = rawBodyBuffer
159
+ ? Readable.from(rawBodyBuffer)
160
+ : Readable.from(Buffer.alloc(0))
161
+ return new Promise((resolve) => {
162
+ WebhookProxy.proxy.web(ctx.req, ctx.res, {
163
+ target: resolved.target,
164
+ buffer: bodyStream
165
+ }, (err) => {
166
+ const elapsed = Date.now() - startTime
167
+ if (err) {
168
+ Logger.log({
169
+ level: 'error',
170
+ message: `Webhook ERR: ${ctx.method} ${ctx.path} → 502 (${elapsed}ms) ${err.message}`,
171
+ data: {
172
+ method: ctx.method,
173
+ path: ctx.path,
174
+ target: resolved.fullUrl,
175
+ error: err.message,
176
+ responseTimeMs: elapsed,
177
+ requestBody: truncateBody(requestBodyStr)
178
+ }
179
+ })
180
+ WebhookProxy.logRequest({
181
+ prefix: resolved.prefix,
182
+ method: ctx.method,
183
+ originalPath: ctx.path,
184
+ targetUrl: resolved.fullUrl,
185
+ statusCode: 502,
186
+ responseTimeMs: elapsed,
187
+ error: err.message,
188
+ queryParams,
189
+ requestBody: truncateBody(requestBodyStr)
190
+ })
191
+ if (!ctx.res.headersSent) {
192
+ ctx.res.writeHead(502, { 'Content-Type': 'application/json' })
193
+ }
194
+ ctx.res.end(JSON.stringify({
195
+ success: false,
196
+ message: `Webhook proxy error: ${err.message}`,
197
+ target: resolved.fullUrl
198
+ }))
199
+ }
200
+ resolve()
201
+ })
202
+ // Log successful responses via proxyRes event
203
+ WebhookProxy.proxy.once('proxyRes', async (proxyRes) => {
204
+ const elapsed = Date.now() - startTime
205
+ const contentEncoding = (proxyRes.headers['content-encoding'] || '').trim().toLowerCase()
206
+ const { raw: responseBody, totalSize: responseSize } = await collectStreamBody(proxyRes, contentEncoding || null)
207
+ Logger.log({
208
+ level: proxyRes.statusCode >= 400 ? 'warn' : 'info',
209
+ message: `Webhook OUT: ${ctx.method} ${ctx.path} → ${proxyRes.statusCode} (${elapsed}ms)`,
210
+ data: {
211
+ method: ctx.method,
212
+ path: ctx.path,
213
+ target: resolved.fullUrl,
214
+ statusCode: proxyRes.statusCode,
215
+ responseTimeMs: elapsed,
216
+ responseBody: truncateBody(responseBody),
217
+ responseSize
218
+ }
219
+ })
220
+ WebhookProxy.logRequest({
221
+ prefix: resolved.prefix,
222
+ method: ctx.method,
223
+ originalPath: ctx.path,
224
+ targetUrl: resolved.fullUrl,
225
+ statusCode: proxyRes.statusCode,
226
+ responseTimeMs: elapsed,
227
+ queryParams,
228
+ requestBody: truncateBody(requestBodyStr),
229
+ responseBody: truncateBody(responseBody)
230
+ })
231
+ })
232
+ })
233
+ }
234
+ }
@@ -0,0 +1,92 @@
1
+ import aedes from 'aedes'
2
+ import { Logger } from '../../singletons/Logger.js'
3
+ import { Handlers } from './Handlers.js'
4
+
5
+ class BrokerClass {
6
+ constructor () {
7
+ this.broker = null
8
+ this.isInitialized = false
9
+ }
10
+
11
+ initialize () {
12
+ if (this.isInitialized) {
13
+ Logger.log({
14
+ level: 'warn',
15
+ message: 'MQTT Broker already initialized'
16
+ })
17
+ return this.broker
18
+ }
19
+
20
+ // Create Aedes broker instance
21
+ this.broker = aedes()
22
+
23
+ // Register event handlers
24
+ this.setupEventHandlers()
25
+
26
+ this.isInitialized = true
27
+
28
+ Logger.log({
29
+ level: 'info',
30
+ message: 'MQTT Broker initialized'
31
+ })
32
+
33
+ return this.broker
34
+ }
35
+
36
+ setupEventHandlers () {
37
+ // Client connected
38
+ this.broker.on('client', (client) => {
39
+ Handlers.handleClientConnect(client)
40
+ })
41
+
42
+ // Client disconnected
43
+ this.broker.on('clientDisconnect', (client) => {
44
+ Handlers.handleClientDisconnect(client)
45
+ })
46
+
47
+ // Message published
48
+ this.broker.on('publish', (packet, client) => {
49
+ Handlers.handlePublish(packet, client)
50
+ })
51
+
52
+ // Client subscribed to topic
53
+ this.broker.on('subscribe', (subscriptions, client) => {
54
+ Handlers.handleSubscribe(subscriptions, client)
55
+ })
56
+
57
+ // Client unsubscribed from topic
58
+ this.broker.on('unsubscribe', (unsubscriptions, client) => {
59
+ Handlers.handleUnsubscribe(unsubscriptions, client)
60
+ })
61
+
62
+ // Client error
63
+ this.broker.on('clientError', (client, error) => {
64
+ Handlers.handleClientError(client, error)
65
+ })
66
+
67
+ // Connection error
68
+ this.broker.on('connectionError', (client, error) => {
69
+ Handlers.handleConnectionError(client, error)
70
+ })
71
+ }
72
+
73
+ getInstance () {
74
+ if (!this.isInitialized) {
75
+ throw new Error('MQTT Broker not initialized. Call initialize() first.')
76
+ }
77
+ return this.broker
78
+ }
79
+
80
+ shutdown () {
81
+ if (this.broker) {
82
+ this.broker.close(() => {
83
+ Logger.log({
84
+ level: 'info',
85
+ message: 'MQTT Broker closed'
86
+ })
87
+ })
88
+ }
89
+ }
90
+ }
91
+
92
+ export const Broker = new BrokerClass()
@@ -0,0 +1,175 @@
1
+ import Moment from 'moment'
2
+ import { Logger } from '../../singletons/Logger.js'
3
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
4
+ import { PubSubBridge } from './PubSubBridge.js'
5
+
6
+ class HandlersClass {
7
+ handleClientConnect (client) {
8
+ const clientData = {
9
+ clientId: client.id,
10
+ connectedAt: Moment().toISOString(),
11
+ disconnectedAt: null,
12
+ will: client.will || null,
13
+ clean: client.clean || false,
14
+ version: client.version || null
15
+ }
16
+
17
+ // Store client connection
18
+ SqliteStore.create('mqtt_clients', clientData)
19
+
20
+ Logger.log({
21
+ level: 'info',
22
+ message: 'MQTT client connected',
23
+ data: {
24
+ clientId: client.id,
25
+ clean: client.clean,
26
+ will: !!client.will
27
+ }
28
+ })
29
+
30
+ // Publish lifecycle event to Pub/Sub
31
+ PubSubBridge.publishLifecycleEvent({
32
+ clientId: client.id,
33
+ eventType: 'connected'
34
+ })
35
+ }
36
+
37
+ handleClientDisconnect (client) {
38
+ try {
39
+ // Update client disconnection time
40
+ const existingClient = SqliteStore.get('mqtt_clients', client.id, 'clientId')
41
+ if (existingClient) {
42
+ SqliteStore.update(
43
+ 'mqtt_clients',
44
+ client.id,
45
+ { disconnectedAt: Moment().toISOString() },
46
+ 'clientId'
47
+ )
48
+ }
49
+
50
+ Logger.log({
51
+ level: 'info',
52
+ message: 'MQTT client disconnected',
53
+ data: { clientId: client.id }
54
+ })
55
+
56
+ // Publish lifecycle event to Pub/Sub
57
+ PubSubBridge.publishLifecycleEvent({
58
+ clientId: client.id,
59
+ eventType: 'disconnected'
60
+ })
61
+ } catch (error) {
62
+ Logger.log({
63
+ level: 'error',
64
+ message: 'Error handling client disconnect',
65
+ data: { clientId: client.id, error: error.message }
66
+ })
67
+ }
68
+ }
69
+
70
+ handlePublish (packet, client) {
71
+ // Skip internal $SYS topics
72
+ if (packet.topic.startsWith('$SYS/')) {
73
+ return
74
+ }
75
+
76
+ const messageData = {
77
+ id: `msg-${Moment().valueOf()}-${Math.random().toString(36).substr(2, 9)}`,
78
+ topic: packet.topic,
79
+ payload: packet.payload.toString(),
80
+ qos: packet.qos || 0,
81
+ retain: packet.retain || false,
82
+ timestamp: Moment().toISOString(),
83
+ clientId: client ? client.id : 'broker'
84
+ }
85
+
86
+ // Store message
87
+ SqliteStore.create('mqtt_messages', messageData)
88
+
89
+ Logger.log({
90
+ level: 'debug',
91
+ message: 'MQTT message published',
92
+ data: {
93
+ topic: packet.topic,
94
+ clientId: messageData.clientId,
95
+ qos: messageData.qos,
96
+ retain: messageData.retain,
97
+ payloadSize: packet.payload.length
98
+ }
99
+ })
100
+
101
+ // Bridge message to Pub/Sub (mimics AWS IoT Core behavior)
102
+ // Only bridge messages from actual clients, not broker-published messages
103
+ if (client) {
104
+ PubSubBridge.publishToPubSub({
105
+ clientId: client.id,
106
+ topicName: packet.topic,
107
+ payload: packet.payload
108
+ })
109
+ }
110
+ }
111
+
112
+ handleSubscribe (subscriptions, client) {
113
+ subscriptions.forEach(sub => {
114
+ const subscriptionData = {
115
+ id: `sub-${Moment().valueOf()}-${Math.random().toString(36).substr(2, 9)}`,
116
+ clientId: client.id,
117
+ topic: sub.topic,
118
+ qos: sub.qos || 0,
119
+ timestamp: Moment().toISOString()
120
+ }
121
+
122
+ // Store subscription
123
+ SqliteStore.create('mqtt_subscriptions', subscriptionData)
124
+
125
+ Logger.log({
126
+ level: 'debug',
127
+ message: 'MQTT client subscribed',
128
+ data: {
129
+ clientId: client.id,
130
+ topic: sub.topic,
131
+ qos: sub.qos
132
+ }
133
+ })
134
+ })
135
+ }
136
+
137
+ handleUnsubscribe (unsubscriptions, client) {
138
+ unsubscriptions.forEach(topic => {
139
+ Logger.log({
140
+ level: 'debug',
141
+ message: 'MQTT client unsubscribed',
142
+ data: {
143
+ clientId: client.id,
144
+ topic
145
+ }
146
+ })
147
+ })
148
+ }
149
+
150
+ handleClientError (client, error) {
151
+ Logger.log({
152
+ level: 'error',
153
+ message: 'MQTT client error',
154
+ data: {
155
+ clientId: client.id,
156
+ error: error.message,
157
+ stack: error.stack
158
+ }
159
+ })
160
+ }
161
+
162
+ handleConnectionError (client, error) {
163
+ Logger.log({
164
+ level: 'error',
165
+ message: 'MQTT connection error',
166
+ data: {
167
+ clientId: client ? client.id : 'unknown',
168
+ error: error.message,
169
+ stack: error.stack
170
+ }
171
+ })
172
+ }
173
+ }
174
+
175
+ export const Handlers = new HandlersClass()
@@ -0,0 +1,162 @@
1
+ import { Application } from '../../configs/Application.js'
2
+ import { Logger } from '../../singletons/Logger.js'
3
+
4
+ const { pubsub } = Application
5
+ const PUBSUB_API = `http://${pubsub.emulatorHost}:${pubsub.emulatorPort}`
6
+ const PROJECT_ID = pubsub.projectId
7
+
8
+ /**
9
+ * MQTT → Pub/Sub Bridge
10
+ *
11
+ * Routes MQTT messages to Google Cloud Pub/Sub emulator
12
+ * Mimics AWS IoT Core's behavior of bridging MQTT to Pub/Sub
13
+ *
14
+ * Message format matches AWS IoT Core:
15
+ * {
16
+ * clientId: string,
17
+ * topicName: string,
18
+ * packet: string (base64-encoded binary),
19
+ * receivedAtMilliseconds: number
20
+ * }
21
+ */
22
+ class PubSubBridgeClass {
23
+ constructor () {
24
+ this.enabled = true
25
+ this.pubsubTopic = 'mqttMessageReceived'
26
+ }
27
+
28
+ /**
29
+ * Publish MQTT message to Pub/Sub
30
+ *
31
+ * @param {Object} params
32
+ * @param {string} params.clientId - MQTT client ID
33
+ * @param {string} params.topicName - MQTT topic name
34
+ * @param {Buffer} params.payload - Binary message payload
35
+ * @returns {Promise<void>}
36
+ */
37
+ async publishToPubSub ({ clientId, topicName, payload }) {
38
+ if (!this.enabled) {
39
+ return
40
+ }
41
+
42
+ try {
43
+ // Format message to match AWS IoT Core format
44
+ const message = {
45
+ clientId: clientId || 'N/A',
46
+ topicName,
47
+ packet: payload.toString('base64'),
48
+ receivedAtMilliseconds: Date.now()
49
+ }
50
+
51
+ // Convert message to base64-encoded JSON
52
+ const messageData = Buffer.from(JSON.stringify(message)).toString('base64')
53
+
54
+ // Publish to Pub/Sub emulator
55
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${this.pubsubTopic}:publish`, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({
59
+ messages: [{ data: messageData }]
60
+ })
61
+ })
62
+
63
+ if (!response.ok) {
64
+ const errorText = await response.text()
65
+ throw new Error(`Pub/Sub publish failed: ${response.status} - ${errorText}`)
66
+ }
67
+
68
+ Logger.log({
69
+ level: 'debug',
70
+ message: 'MQTT → Pub/Sub: Message bridged',
71
+ data: {
72
+ clientId,
73
+ topic: topicName,
74
+ pubsubTopic: this.pubsubTopic,
75
+ payloadSize: payload.length
76
+ }
77
+ })
78
+ } catch (error) {
79
+ Logger.log({
80
+ level: 'error',
81
+ message: 'MQTT → Pub/Sub: Bridge failed',
82
+ data: {
83
+ clientId,
84
+ topic: topicName,
85
+ error: error.message,
86
+ stack: error.stack
87
+ }
88
+ })
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Publish lifecycle event (connected/disconnected)
94
+ *
95
+ * @param {Object} params
96
+ * @param {string} params.clientId - MQTT client ID
97
+ * @param {string} params.eventType - 'connected' or 'disconnected'
98
+ * @returns {Promise<void>}
99
+ */
100
+ async publishLifecycleEvent ({ clientId, eventType }) {
101
+ if (!this.enabled) {
102
+ return
103
+ }
104
+
105
+ try {
106
+ const topicName = `simulator/${eventType}`
107
+ const eventPayload = JSON.stringify({
108
+ clientId,
109
+ eventType,
110
+ timestamp: Date.now()
111
+ })
112
+
113
+ // Format as MQTT message for consistency
114
+ const message = {
115
+ clientId,
116
+ topicName,
117
+ packet: Buffer.from(eventPayload).toString('base64'),
118
+ receivedAtMilliseconds: Date.now()
119
+ }
120
+
121
+ // Convert message to base64-encoded JSON
122
+ const messageData = Buffer.from(JSON.stringify(message)).toString('base64')
123
+
124
+ // Publish to Pub/Sub emulator
125
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${this.pubsubTopic}:publish`, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({
129
+ messages: [{ data: messageData }]
130
+ })
131
+ })
132
+
133
+ if (!response.ok) {
134
+ const errorText = await response.text()
135
+ throw new Error(`Lifecycle event publish failed: ${response.status} - ${errorText}`)
136
+ }
137
+
138
+ Logger.log({
139
+ level: 'debug',
140
+ message: 'MQTT → Pub/Sub: Lifecycle event published',
141
+ data: {
142
+ clientId,
143
+ eventType,
144
+ topic: topicName
145
+ }
146
+ })
147
+ } catch (error) {
148
+ Logger.log({
149
+ level: 'error',
150
+ message: 'MQTT → Pub/Sub: Lifecycle event failed',
151
+ data: {
152
+ clientId,
153
+ eventType,
154
+ error: error.message,
155
+ stack: error.stack
156
+ }
157
+ })
158
+ }
159
+ }
160
+ }
161
+
162
+ export const PubSubBridge = new PubSubBridgeClass()