@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.
- package/README.md +478 -0
- package/bin/goki-dev.js +452 -0
- package/bin/mcp-server.js +16 -0
- package/bin/secrets-cli.js +302 -0
- package/cli/ComposeOverrideGenerator.js +226 -0
- package/cli/ComposeParser.js +73 -0
- package/cli/ConfigGenerator.js +304 -0
- package/cli/ConfigManager.js +46 -0
- package/cli/DatabaseManager.js +94 -0
- package/cli/DevToolsChecker.js +21 -0
- package/cli/DevToolsDir.js +66 -0
- package/cli/DevToolsManager.js +451 -0
- package/cli/DockerManager.js +138 -0
- package/cli/FunctionManager.js +95 -0
- package/cli/HttpProxyRewriter.js +91 -0
- package/cli/Logger.js +10 -0
- package/cli/McpConfigManager.js +123 -0
- package/cli/NgrokManager.js +431 -0
- package/cli/ProjectCLI.js +2322 -0
- package/cli/PubSubManager.js +129 -0
- package/cli/SnapshotManager.js +88 -0
- package/cli/UiFormatter.js +292 -0
- package/cli/WebhookUrlRewriter.js +32 -0
- package/cli/secrets/BiometricAuth.js +125 -0
- package/cli/secrets/SecretInjector.js +47 -0
- package/cli/secrets/SecretsConfig.js +141 -0
- package/cli/secrets/SecretsDoctor.js +384 -0
- package/cli/secrets/SecretsManager.js +255 -0
- package/client/dist/client.d.ts +332 -0
- package/client/dist/client.js +507 -0
- package/client/dist/helpers.d.ts +62 -0
- package/client/dist/helpers.js +122 -0
- package/client/dist/index.d.ts +59 -0
- package/client/dist/index.js +78 -0
- package/client/dist/package.json +1 -0
- package/client/dist/types.d.ts +280 -0
- package/client/dist/types.js +7 -0
- package/config.development +46 -0
- package/config.test +18 -0
- package/guidelines/CodingStyleGuideline.md +148 -0
- package/guidelines/CommentingGuideline.md +10 -0
- package/guidelines/HttpApiImplementationGuideline.md +137 -0
- package/guidelines/NamingGuideline.md +182 -0
- package/package.json +138 -0
- package/patterns/api/[collectionName]/Controllers.md +62 -0
- package/patterns/api/[collectionName]/Logic.md +154 -0
- package/patterns/api/[collectionName]/Permissions.md +81 -0
- package/patterns/api/[collectionName]/Router.md +83 -0
- package/patterns/api/[collectionName]/Schemas.md +197 -0
- package/patterns/configs/Patterns.md +7 -0
- package/patterns/enums/Patterns.md +24 -0
- package/patterns/errorHandling/Patterns.md +185 -0
- package/patterns/testing/Patterns.md +232 -0
- package/src/Server.js +238 -0
- package/src/api/dashboard/Controllers.js +9 -0
- package/src/api/dashboard/Logic.js +76 -0
- package/src/api/dashboard/Router.js +11 -0
- package/src/api/dashboard/Schemas.js +47 -0
- package/src/api/data/Controllers.js +26 -0
- package/src/api/data/Logic.js +188 -0
- package/src/api/data/Router.js +16 -0
- package/src/api/docker/Controllers.js +33 -0
- package/src/api/docker/Logic.js +268 -0
- package/src/api/docker/Router.js +15 -0
- package/src/api/docker/Schemas.js +80 -0
- package/src/api/docs/Controllers.js +15 -0
- package/src/api/docs/Logic.js +85 -0
- package/src/api/docs/Router.js +12 -0
- package/src/api/export/Controllers.js +30 -0
- package/src/api/export/Logic.js +143 -0
- package/src/api/export/Router.js +18 -0
- package/src/api/export/Schemas.js +104 -0
- package/src/api/firestore/Controllers.js +152 -0
- package/src/api/firestore/Logic.js +474 -0
- package/src/api/firestore/Router.js +23 -0
- package/src/api/functions/Controllers.js +261 -0
- package/src/api/functions/Logic.js +710 -0
- package/src/api/functions/Router.js +50 -0
- package/src/api/functions/Schemas.js +193 -0
- package/src/api/gateway/Controllers.js +72 -0
- package/src/api/gateway/Logic.js +74 -0
- package/src/api/gateway/Router.js +10 -0
- package/src/api/gateway/Schemas.js +19 -0
- package/src/api/health/Controllers.js +14 -0
- package/src/api/health/Logic.js +24 -0
- package/src/api/health/Router.js +12 -0
- package/src/api/httpTraffic/Controllers.js +29 -0
- package/src/api/httpTraffic/Logic.js +33 -0
- package/src/api/httpTraffic/Router.js +9 -0
- package/src/api/httpTraffic/Schemas.js +23 -0
- package/src/api/logging/Controllers.js +80 -0
- package/src/api/logging/Logic.js +461 -0
- package/src/api/logging/Router.js +24 -0
- package/src/api/logging/Schemas.js +43 -0
- package/src/api/mqtt/Controllers.js +17 -0
- package/src/api/mqtt/Logic.js +66 -0
- package/src/api/mqtt/Router.js +12 -0
- package/src/api/postgres/Controllers.js +97 -0
- package/src/api/postgres/Logic.js +221 -0
- package/src/api/postgres/Router.js +21 -0
- package/src/api/pubsub/Controllers.js +236 -0
- package/src/api/pubsub/Logic.js +732 -0
- package/src/api/pubsub/Router.js +41 -0
- package/src/api/pubsub/Schemas.js +355 -0
- package/src/api/redis/Controllers.js +63 -0
- package/src/api/redis/Logic.js +239 -0
- package/src/api/redis/Router.js +21 -0
- package/src/api/scheduler/Controllers.js +27 -0
- package/src/api/scheduler/Logic.js +49 -0
- package/src/api/scheduler/Router.js +16 -0
- package/src/api/services/Controllers.js +26 -0
- package/src/api/services/Logic.js +205 -0
- package/src/api/services/Router.js +14 -0
- package/src/api/services/Schemas.js +66 -0
- package/src/api/snapshots/Controllers.js +37 -0
- package/src/api/snapshots/Logic.js +797 -0
- package/src/api/snapshots/Router.js +15 -0
- package/src/api/snapshots/Schemas.js +23 -0
- package/src/api/webhooks/Controllers.js +49 -0
- package/src/api/webhooks/Logic.js +137 -0
- package/src/api/webhooks/Router.js +12 -0
- package/src/api/webhooks/Schemas.js +31 -0
- package/src/configs/Application.js +147 -0
- package/src/configs/Default.js +13 -0
- package/src/consumers/BlackboxLogsConsumer.js +235 -0
- package/src/consumers/DockerLogsConsumer.js +687 -0
- package/src/db/Tables.js +66 -0
- package/src/db/schemas/firestore.js +18 -0
- package/src/db/schemas/functions.js +65 -0
- package/src/db/schemas/httpTraffic.js +43 -0
- package/src/db/schemas/logging.js +74 -0
- package/src/db/schemas/migrations.js +64 -0
- package/src/db/schemas/mqtt.js +56 -0
- package/src/db/schemas/pubsub.js +90 -0
- package/src/db/schemas/pubsubRegistry.js +22 -0
- package/src/db/schemas/webhooks.js +28 -0
- package/src/emulation/awsiot/Controllers.js +91 -0
- package/src/emulation/awsiot/Logic.js +70 -0
- package/src/emulation/awsiot/Router.js +19 -0
- package/src/emulation/awsiot/Server.js +100 -0
- package/src/emulation/firestore/Server.js +136 -0
- package/src/emulation/logging/Controllers.js +212 -0
- package/src/emulation/logging/Logic.js +416 -0
- package/src/emulation/logging/Router.js +36 -0
- package/src/emulation/logging/Schemas.js +82 -0
- package/src/emulation/logging/Server.js +108 -0
- package/src/emulation/pubsub/Controllers.js +279 -0
- package/src/emulation/pubsub/DefaultTopics.js +162 -0
- package/src/emulation/pubsub/Logic.js +427 -0
- package/src/emulation/pubsub/README.md +309 -0
- package/src/emulation/pubsub/Router.js +33 -0
- package/src/emulation/pubsub/Server.js +104 -0
- package/src/emulation/pubsub/ShadowPoller.js +276 -0
- package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
- package/src/enums/ContainerNames.js +106 -0
- package/src/enums/ErrorReason.js +28 -0
- package/src/enums/FunctionStatuses.js +15 -0
- package/src/enums/FunctionTriggerTypes.js +15 -0
- package/src/enums/GatewayState.js +7 -0
- package/src/enums/ServiceNames.js +68 -0
- package/src/jobs/DatabaseMaintenance.js +184 -0
- package/src/jobs/MessageHistoryCleanup.js +152 -0
- package/src/mcp/ApiClient.js +25 -0
- package/src/mcp/Server.js +52 -0
- package/src/mcp/prompts/debugging.js +104 -0
- package/src/mcp/resources/platform.js +118 -0
- package/src/mcp/tools/data.js +84 -0
- package/src/mcp/tools/docker.js +166 -0
- package/src/mcp/tools/firestore.js +162 -0
- package/src/mcp/tools/functions.js +380 -0
- package/src/mcp/tools/httpTraffic.js +69 -0
- package/src/mcp/tools/logging.js +174 -0
- package/src/mcp/tools/mqtt.js +37 -0
- package/src/mcp/tools/postgres.js +130 -0
- package/src/mcp/tools/pubsub.js +316 -0
- package/src/mcp/tools/redis.js +146 -0
- package/src/mcp/tools/services.js +169 -0
- package/src/mcp/tools/snapshots.js +88 -0
- package/src/mcp/tools/webhooks.js +115 -0
- package/src/middleware/DevProxy.js +67 -0
- package/src/middleware/ErrorCatcher.js +35 -0
- package/src/middleware/HttpProxy.js +215 -0
- package/src/middleware/Reply.js +24 -0
- package/src/middleware/TraceId.js +9 -0
- package/src/middleware/WebhookProxy.js +234 -0
- package/src/protocols/mqtt/Broker.js +92 -0
- package/src/protocols/mqtt/Handlers.js +175 -0
- package/src/protocols/mqtt/PubSubBridge.js +162 -0
- package/src/protocols/mqtt/Server.js +116 -0
- package/src/runtime/FunctionRunner.js +179 -0
- package/src/services/AppGatewayService.js +582 -0
- package/src/singletons/FirestoreBroadcaster.js +367 -0
- package/src/singletons/FunctionTriggerDispatcher.js +456 -0
- package/src/singletons/FunctionsService.js +418 -0
- package/src/singletons/HttpProxy.js +224 -0
- package/src/singletons/LogBroadcaster.js +159 -0
- package/src/singletons/Logger.js +49 -0
- package/src/singletons/MemoryJsonStore.js +175 -0
- package/src/singletons/MessageBroadcaster.js +190 -0
- package/src/singletons/PostgresBroadcaster.js +367 -0
- package/src/singletons/PostgresClient.js +180 -0
- package/src/singletons/RedisClient.js +184 -0
- package/src/singletons/SqliteStore.js +480 -0
- package/src/singletons/TickService.js +151 -0
- 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,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()
|