@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,418 @@
1
+ import { v4 as uuid } from 'uuid'
2
+ import path from 'path'
3
+ import chokidar from 'chokidar'
4
+ import { FunctionRunner } from '../runtime/FunctionRunner.js'
5
+ import { SqliteStore } from './SqliteStore.js'
6
+ import { LogBroadcaster } from './LogBroadcaster.js'
7
+ import { Logger } from './Logger.js'
8
+ import { Application } from '../configs/Application.js'
9
+ import { CLOUD_FUNCTIONS, LOGGING_ENTRIES } from '../db/Tables.js'
10
+ import { FunctionStatuses } from '../enums/FunctionStatuses.js'
11
+
12
+ const { functions } = Application
13
+
14
+ /**
15
+ * Process manager singleton for Cloud Functions.
16
+ * Manages all FunctionRunner instances, port allocation, and SSE broadcasting.
17
+ */
18
+ class FunctionsServiceClass {
19
+ constructor () {
20
+ this.runners = new Map()
21
+ this.sseClients = new Map()
22
+ this.invocationSseClients = new Map()
23
+ this.fileChangeClients = new Map()
24
+ this.fileWatchers = new Map()
25
+ this.portPool = new Set()
26
+ this.heartbeatId = null
27
+ this.heartbeatIntervalMs = 30000
28
+ const basePort = functions.basePort || 9100
29
+ const maxInstances = functions.maxInstances || 50
30
+ for (let i = 0; i < maxInstances; i++) {
31
+ this.portPool.add(basePort + i)
32
+ }
33
+ }
34
+
35
+ async initialize () {
36
+ try {
37
+ const enabledFunctions = SqliteStore.find(CLOUD_FUNCTIONS, { enabled: 1 })
38
+ for (const fn of enabledFunctions) {
39
+ if (fn.status === FunctionStatuses.RUNNING || fn.status === FunctionStatuses.STARTING) {
40
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, { status: FunctionStatuses.STOPPED, pid: null })
41
+ }
42
+ }
43
+ for (const fn of enabledFunctions) {
44
+ try {
45
+ await this.deployFunction(fn)
46
+ } catch (error) {
47
+ Logger.log({
48
+ level: 'error',
49
+ message: `Failed to start function on initialize: ${fn.name}`,
50
+ data: { error: error.message }
51
+ })
52
+ }
53
+ }
54
+ Logger.log({
55
+ level: 'info',
56
+ message: 'FunctionsService initialized',
57
+ data: { functionCount: enabledFunctions.length, runningCount: this.runners.size }
58
+ })
59
+ } catch (error) {
60
+ Logger.log({
61
+ level: 'error',
62
+ message: 'Failed to initialize FunctionsService',
63
+ data: { error: error.message }
64
+ })
65
+ }
66
+ }
67
+
68
+ async shutdown () {
69
+ const stopPromises = []
70
+ for (const [, runner] of this.runners) {
71
+ stopPromises.push(runner.stop().catch(err => {
72
+ Logger.log({
73
+ level: 'warn',
74
+ message: `Error stopping function during shutdown: ${runner.name}`,
75
+ data: { error: err.message }
76
+ })
77
+ }))
78
+ }
79
+ await Promise.all(stopPromises)
80
+ this.runners.clear()
81
+ this.stopHeartbeat()
82
+ this.sseClients.clear()
83
+ this.invocationSseClients.clear()
84
+ Logger.log({ level: 'info', message: 'FunctionsService shut down' })
85
+ }
86
+
87
+ async deployFunction (config) {
88
+ const port = config.port || this.allocatePort()
89
+ if (!port) {
90
+ throw new Error('No available ports for function deployment')
91
+ }
92
+ const runner = new FunctionRunner({
93
+ name: config.name,
94
+ source: config.source,
95
+ sourcePath: config.sourcePath,
96
+ entryPoint: config.entryPoint,
97
+ port,
98
+ signatureType: config.signatureType || 'cloudevent',
99
+ env: typeof config.environmentVariables === 'string'
100
+ ? JSON.parse(config.environmentVariables)
101
+ : config.environmentVariables || {},
102
+ timeoutSeconds: config.timeoutSeconds || 60
103
+ })
104
+ runner.onLog = (name, stream, message) => {
105
+ this.broadcastLog(name, stream, message)
106
+ }
107
+ runner.onStatusChange = (name, status) => {
108
+ this.updateFunctionStatus(name, status)
109
+ }
110
+ try {
111
+ await runner.start()
112
+ this.runners.set(config.name, runner)
113
+ this.updateFunctionStatus(config.name, FunctionStatuses.RUNNING, null, port, runner.process?.pid)
114
+ return runner.getStatus()
115
+ } catch (error) {
116
+ this.releasePort(port)
117
+ this.updateFunctionStatus(config.name, FunctionStatuses.ERROR, error.message)
118
+ throw error
119
+ }
120
+ }
121
+
122
+ async removeFunction (name) {
123
+ const runner = this.runners.get(name)
124
+ if (runner) {
125
+ const port = runner.port
126
+ await runner.stop()
127
+ this.releasePort(port)
128
+ this.runners.delete(name)
129
+ }
130
+ }
131
+
132
+ async startFunction (name) {
133
+ const fn = SqliteStore.find(CLOUD_FUNCTIONS, { name }).pop()
134
+ if (!fn) throw new Error(`Function not found: ${name}`)
135
+ if (this.runners.has(name)) {
136
+ const runner = this.runners.get(name)
137
+ if (runner.status === FunctionStatuses.RUNNING) return runner.getStatus()
138
+ }
139
+ return this.deployFunction(fn)
140
+ }
141
+
142
+ async stopFunction (name) {
143
+ const runner = this.runners.get(name)
144
+ if (!runner) throw new Error(`Function not running: ${name}`)
145
+ const port = runner.port
146
+ await runner.stop()
147
+ this.releasePort(port)
148
+ this.runners.delete(name)
149
+ this.updateFunctionStatus(name, FunctionStatuses.STOPPED)
150
+ }
151
+
152
+ async restartFunction (name) {
153
+ await this.stopFunction(name)
154
+ return this.startFunction(name)
155
+ }
156
+
157
+ getEndpointUrl (name) {
158
+ const runner = this.runners.get(name)
159
+ if (!runner || runner.status !== FunctionStatuses.RUNNING) return null
160
+ return `http://localhost:${runner.port}`
161
+ }
162
+
163
+ getAllStatuses () {
164
+ const statuses = []
165
+ for (const [, runner] of this.runners) {
166
+ statuses.push(runner.getStatus())
167
+ }
168
+ return statuses
169
+ }
170
+
171
+ allocatePort () {
172
+ const iterator = this.portPool.values()
173
+ const first = iterator.next()
174
+ if (first.done) return null
175
+ this.portPool.delete(first.value)
176
+ return first.value
177
+ }
178
+
179
+ releasePort (port) {
180
+ if (port) this.portPool.add(port)
181
+ }
182
+
183
+ updateFunctionStatus (name, status, errorMessage, port, pid) {
184
+ try {
185
+ const fn = SqliteStore.find(CLOUD_FUNCTIONS, { name }).pop()
186
+ if (!fn) return
187
+ const updates = { status, updatedAt: new Date().toISOString() }
188
+ if (errorMessage !== undefined) updates.errorMessage = errorMessage
189
+ if (port !== undefined) updates.port = port
190
+ if (pid !== undefined) updates.pid = pid
191
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, updates)
192
+ } catch (error) {
193
+ Logger.log({
194
+ level: 'warn',
195
+ message: 'Failed to update function status in DB',
196
+ data: { name, status, error: error.message }
197
+ })
198
+ }
199
+ }
200
+
201
+ // --- Log SSE ---
202
+
203
+ addLogClient (id, ctx, filters = {}) {
204
+ this.sseClients.set(id, { ctx, filters })
205
+ this.startHeartbeat()
206
+ Logger.log({
207
+ level: 'info',
208
+ message: 'Functions log SSE client connected',
209
+ data: { connectionId: id, totalClients: this.sseClients.size }
210
+ })
211
+ }
212
+
213
+ removeLogClient (id) {
214
+ const removed = this.sseClients.delete(id)
215
+ if (removed && this.sseClients.size === 0 && this.invocationSseClients.size === 0) {
216
+ this.stopHeartbeat()
217
+ }
218
+ }
219
+
220
+ broadcastLog (functionName, level, message) {
221
+ const trimmedMessage = message.trimEnd()
222
+ const now = new Date().toISOString()
223
+ // Broadcast to Functions-specific SSE clients
224
+ if (this.sseClients.size > 0) {
225
+ const payload = { functionName, level, message: trimmedMessage, timestamp: now }
226
+ for (const [id, { ctx, filters }] of this.sseClients) {
227
+ if (filters.functionName && filters.functionName !== functionName) continue
228
+ try {
229
+ this.sendSSE(ctx, 'log', payload)
230
+ } catch {
231
+ this.removeLogClient(id)
232
+ }
233
+ }
234
+ }
235
+ // Also write to the unified logging system
236
+ try {
237
+ const severity = level === 'stderr' ? 'ERROR' : 'INFO'
238
+ let jsonPayload = null
239
+ try {
240
+ const parsed = JSON.parse(trimmedMessage)
241
+ if (parsed && typeof parsed === 'object') {
242
+ jsonPayload = JSON.stringify(parsed)
243
+ }
244
+ } catch {}
245
+ const entry = {
246
+ _id: uuid(),
247
+ logName: 'projects/dev-tools/logs/cloud-functions',
248
+ serviceName: functionName,
249
+ severity,
250
+ textPayload: trimmedMessage,
251
+ jsonPayload,
252
+ timestamp: now,
253
+ receiveTimestamp: now,
254
+ insertId: uuid(),
255
+ source: 'cloud-functions',
256
+ labels: JSON.stringify({ functionName, source: 'cloud-functions' })
257
+ }
258
+ SqliteStore.create(LOGGING_ENTRIES, entry)
259
+ LogBroadcaster.broadcast(entry)
260
+ } catch (error) {
261
+ Logger.log({ level: 'error', message: 'Failed to write function log to logging system', data: { functionName, error: error.message } })
262
+ }
263
+ }
264
+
265
+ // --- Invocation SSE ---
266
+
267
+ addInvocationClient (id, ctx, filters = {}) {
268
+ this.invocationSseClients.set(id, { ctx, filters })
269
+ this.startHeartbeat()
270
+ Logger.log({
271
+ level: 'info',
272
+ message: 'Functions invocation SSE client connected',
273
+ data: { connectionId: id, totalClients: this.invocationSseClients.size }
274
+ })
275
+ }
276
+
277
+ removeInvocationClient (id) {
278
+ const removed = this.invocationSseClients.delete(id)
279
+ if (removed && this.sseClients.size === 0 && this.invocationSseClients.size === 0) {
280
+ this.stopHeartbeat()
281
+ }
282
+ }
283
+
284
+ broadcastInvocation (invocation) {
285
+ if (this.invocationSseClients.size === 0) return
286
+ for (const [id, { ctx, filters }] of this.invocationSseClients) {
287
+ if (filters.functionName && filters.functionName !== invocation.functionName) continue
288
+ try {
289
+ this.sendSSE(ctx, 'invocation', invocation)
290
+ } catch {
291
+ this.removeInvocationClient(id)
292
+ }
293
+ }
294
+ }
295
+
296
+ // --- File Change SSE ---
297
+
298
+ addFileChangeClient (id, ctx, functionName) {
299
+ this.fileChangeClients.set(id, { ctx, functionName })
300
+ this.startHeartbeat()
301
+ this.startFileWatcher(functionName)
302
+ Logger.log({ level: 'info', message: 'File change SSE client connected', data: { connectionId: id, functionName } })
303
+ }
304
+
305
+ removeFileChangeClient (id) {
306
+ const client = this.fileChangeClients.get(id)
307
+ this.fileChangeClients.delete(id)
308
+ if (client) {
309
+ const stillWatching = Array.from(this.fileChangeClients.values())
310
+ .some(c => c.functionName === client.functionName)
311
+ if (!stillWatching) {
312
+ this.stopFileWatcher(client.functionName)
313
+ }
314
+ }
315
+ if (this.sseClients.size === 0 && this.invocationSseClients.size === 0 && this.fileChangeClients.size === 0) {
316
+ this.stopHeartbeat()
317
+ }
318
+ }
319
+
320
+ startFileWatcher (functionName) {
321
+ if (this.fileWatchers.has(functionName)) return
322
+ const { data } = SqliteStore.list(CLOUD_FUNCTIONS, { filter: { name: functionName }, limit: 1 })
323
+ const fn = data[0]
324
+ if (!fn || !fn.sourcePath) return
325
+ const watchDir = fn.sourcePath
326
+ Logger.log({ level: 'info', message: 'Starting file watcher', data: { functionName, watchDir } })
327
+ const watcher = chokidar.watch(watchDir, {
328
+ ignoreInitial: true,
329
+ ignored: [
330
+ '**/node_modules/**',
331
+ '**/.git/**',
332
+ '**/package-lock.json'
333
+ ],
334
+ usePolling: true,
335
+ interval: 1000,
336
+ awaitWriteFinish: {
337
+ stabilityThreshold: 500,
338
+ pollInterval: 200
339
+ }
340
+ })
341
+ const handleChange = (eventType, filePath) => {
342
+ const relativePath = path.relative(watchDir, filePath)
343
+ if (relativePath.startsWith('node_modules')) return
344
+ Logger.log({
345
+ level: 'debug',
346
+ message: 'File change detected',
347
+ data: { functionName, eventType, relativePath }
348
+ })
349
+ for (const [id, client] of this.fileChangeClients) {
350
+ if (client.functionName !== functionName) continue
351
+ try {
352
+ this.sendSSE(client.ctx, 'fileChange', { functionName, eventType, filePath: relativePath })
353
+ } catch {
354
+ this.removeFileChangeClient(id)
355
+ }
356
+ }
357
+ }
358
+ watcher.on('ready', () => {
359
+ Logger.log({ level: 'info', message: 'File watcher ready', data: { functionName, watchDir } })
360
+ })
361
+ watcher.on('error', (err) => {
362
+ Logger.log({ level: 'error', message: 'File watcher error', data: { functionName, error: err.message } })
363
+ })
364
+ watcher.on('change', (fp) => handleChange('change', fp))
365
+ watcher.on('add', (fp) => handleChange('add', fp))
366
+ watcher.on('unlink', (fp) => handleChange('unlink', fp))
367
+ this.fileWatchers.set(functionName, watcher)
368
+ }
369
+
370
+ stopFileWatcher (functionName) {
371
+ const watcher = this.fileWatchers.get(functionName)
372
+ if (watcher) {
373
+ watcher.close()
374
+ this.fileWatchers.delete(functionName)
375
+ }
376
+ }
377
+
378
+ // --- SSE Helpers ---
379
+
380
+ sendSSE (ctx, event, data) {
381
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
382
+ ctx.res.write(message)
383
+ }
384
+
385
+ startHeartbeat () {
386
+ if (this.heartbeatId) return
387
+ this.heartbeatId = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
388
+ }
389
+
390
+ stopHeartbeat () {
391
+ if (this.heartbeatId) {
392
+ clearInterval(this.heartbeatId)
393
+ this.heartbeatId = null
394
+ }
395
+ }
396
+
397
+ sendHeartbeat () {
398
+ const allClients = [
399
+ ...Array.from(this.sseClients.entries()),
400
+ ...Array.from(this.invocationSseClients.entries()),
401
+ ...Array.from(this.fileChangeClients.entries())
402
+ ]
403
+ for (const [id, { ctx }] of allClients) {
404
+ try {
405
+ ctx.res.write(': heartbeat\n\n')
406
+ } catch {
407
+ this.sseClients.delete(id)
408
+ this.invocationSseClients.delete(id)
409
+ this.fileChangeClients.delete(id)
410
+ }
411
+ }
412
+ if (this.sseClients.size === 0 && this.invocationSseClients.size === 0 && this.fileChangeClients.size === 0) {
413
+ this.stopHeartbeat()
414
+ }
415
+ }
416
+ }
417
+
418
+ export const FunctionsService = new FunctionsServiceClass()
@@ -0,0 +1,224 @@
1
+ import httpProxy from 'http-proxy'
2
+ import { v4 as uuidv4 } from 'uuid'
3
+ import { SqliteStore } from './SqliteStore.js'
4
+ import { Logger } from './Logger.js'
5
+ import { LogBroadcaster } from './LogBroadcaster.js'
6
+ import { HTTP_TRAFFIC } from '../db/Tables.js'
7
+ import { ServiceNames } from '../enums/ServiceNames.js'
8
+
9
+ const BINARY_CONTENT_TYPES = [
10
+ 'image/', 'audio/', 'video/', 'application/octet-stream',
11
+ 'application/zip', 'application/gzip', 'application/pdf'
12
+ ]
13
+
14
+ function isBinaryContentType (contentType) {
15
+ if (!contentType) return false
16
+ return BINARY_CONTENT_TYPES.some(type => contentType.startsWith(type))
17
+ }
18
+
19
+ function tryParseJson (str) {
20
+ if (!str || typeof str !== 'string') return null
21
+ const trimmed = str.trim()
22
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
23
+ try { return JSON.parse(trimmed) } catch { return null }
24
+ }
25
+ return null
26
+ }
27
+
28
+ function parseCookies (cookieHeader) {
29
+ if (!cookieHeader) return null
30
+ const cookies = {}
31
+ cookieHeader.split(';').forEach(part => {
32
+ const [key, ...rest] = part.trim().split('=')
33
+ if (key) cookies[key.trim()] = rest.join('=').trim()
34
+ })
35
+ return Object.keys(cookies).length > 0 ? cookies : null
36
+ }
37
+
38
+ function extractServiceName (hostname) {
39
+ if (!hostname) return null
40
+ return hostname.split(':')[0]
41
+ }
42
+
43
+ class HttpProxyClass {
44
+ constructor () {
45
+ this.proxy = null
46
+ }
47
+
48
+ initialize () {
49
+ this.proxy = httpProxy.createProxyServer({
50
+ changeOrigin: true,
51
+ secure: false,
52
+ xfwd: true,
53
+ proxyTimeout: 30000
54
+ })
55
+ this.proxy.on('error', (err, req, res) => {
56
+ Logger.log({
57
+ level: 'error',
58
+ message: 'HTTP proxy error',
59
+ data: { error: err.message, url: req.url, method: req.method }
60
+ })
61
+ })
62
+ Logger.log({
63
+ level: 'info',
64
+ message: 'HttpProxy initialized'
65
+ })
66
+ }
67
+
68
+ parseTargetUrl (rawPath) {
69
+ const match = rawPath.match(/^\/proxy\/(.+)$/)
70
+ if (!match) return null
71
+ let rawTarget = match[1]
72
+ if (!rawTarget.match(/^https?:\/\//)) {
73
+ rawTarget = 'http://' + rawTarget
74
+ }
75
+ try {
76
+ const url = new URL(rawTarget)
77
+ return {
78
+ origin: url.origin,
79
+ host: url.host,
80
+ hostname: url.hostname,
81
+ path: url.pathname + url.search,
82
+ fullUrl: rawTarget,
83
+ serviceName: extractServiceName(url.hostname)
84
+ }
85
+ } catch {
86
+ return null
87
+ }
88
+ }
89
+
90
+ logTraffic (entry) {
91
+ const {
92
+ method, targetUrl, targetHost, targetPath,
93
+ queryParams, requestHeaders, requestBody, requestCookies,
94
+ contentType, statusCode, responseHeaders, responseBody,
95
+ responseContentType, responseTimeMs, startedAt, completedAt,
96
+ sourceService, error, traceId
97
+ } = entry
98
+ const id = uuidv4()
99
+ const trafficBody = isBinaryContentType(contentType)
100
+ ? `[binary, ${requestBody?.length || 0} bytes]`
101
+ : requestBody || null
102
+ const trafficResponseBody = isBinaryContentType(responseContentType)
103
+ ? `[binary, ${responseBody?.length || 0} bytes]`
104
+ : responseBody || null
105
+ try {
106
+ SqliteStore.create(HTTP_TRAFFIC, {
107
+ _id: id,
108
+ method,
109
+ targetUrl,
110
+ targetHost,
111
+ targetPath,
112
+ queryParams: queryParams || null,
113
+ requestHeaders: requestHeaders || null,
114
+ requestBody: trafficBody,
115
+ requestCookies: requestCookies || null,
116
+ contentType: contentType || null,
117
+ statusCode: statusCode || null,
118
+ responseHeaders: responseHeaders || null,
119
+ responseBody: trafficResponseBody,
120
+ responseContentType: responseContentType || null,
121
+ responseTimeMs: responseTimeMs || null,
122
+ startedAt,
123
+ completedAt: completedAt || null,
124
+ sourceService: sourceService || null,
125
+ error: error || null,
126
+ traceId: traceId || null
127
+ })
128
+ } catch (err) {
129
+ Logger.log({ level: 'error', message: 'Failed to store http traffic', data: { error: err.message } })
130
+ }
131
+ const now = new Date().toISOString()
132
+ const severity = error || (statusCode && statusCode >= 400) ? 'ERROR' : 'INFO'
133
+ const statusText = error ? `ERR ${error}` : `${statusCode}`
134
+ const textPayload = `${method} ${targetUrl} → ${statusText} (${responseTimeMs}ms)`
135
+ const logRequestBody = tryParseJson(requestBody) || requestBody || null
136
+ const logResponseBody = tryParseJson(responseBody) || responseBody || null
137
+ const jsonPayload = {
138
+ method,
139
+ targetUrl,
140
+ targetHost,
141
+ statusCode,
142
+ responseTimeMs,
143
+ error: error || null,
144
+ ...(queryParams && { queryParams }),
145
+ ...(logRequestBody && { requestBody: logRequestBody }),
146
+ ...(logResponseBody && { responseBody: logResponseBody })
147
+ }
148
+ const logEntry = {
149
+ _id: uuidv4(),
150
+ logName: 'projects/dev-tools/logs/http',
151
+ source: ServiceNames.HTTP_PROXY,
152
+ serviceName: ServiceNames.HTTP_PROXY,
153
+ severity,
154
+ textPayload,
155
+ jsonPayload,
156
+ timestamp: now,
157
+ receiveTimestamp: now,
158
+ insertId: uuidv4(),
159
+ createdAt: now
160
+ }
161
+ try {
162
+ SqliteStore.create('logging_entries', logEntry)
163
+ } catch (err) {
164
+ Logger.log({ level: 'error', message: 'Failed to store http proxy log', data: { error: err.message } })
165
+ }
166
+ LogBroadcaster.broadcast(logEntry)
167
+ }
168
+
169
+ listTraffic (options = {}) {
170
+ const { filter, limit = 50, offset = 0 } = options
171
+ const listOptions = {
172
+ orderBy: 'created_at DESC',
173
+ limit,
174
+ offset
175
+ }
176
+ if (filter && Object.keys(filter).length > 0) {
177
+ listOptions.filter = filter
178
+ }
179
+ return SqliteStore.list(HTTP_TRAFFIC, listOptions)
180
+ }
181
+
182
+ getTrafficEntry (id) {
183
+ return SqliteStore.get(HTTP_TRAFFIC, id, '_id')
184
+ }
185
+
186
+ clearTraffic () {
187
+ SqliteStore.clear(HTTP_TRAFFIC)
188
+ }
189
+
190
+ getStats () {
191
+ try {
192
+ const total = SqliteStore.db.prepare(`SELECT COUNT(*) as count FROM ${HTTP_TRAFFIC}`).get().count
193
+ const byMethod = SqliteStore.db.prepare(`
194
+ SELECT method, COUNT(*) as count FROM ${HTTP_TRAFFIC} GROUP BY method ORDER BY count DESC
195
+ `).all()
196
+ const byHost = SqliteStore.db.prepare(`
197
+ SELECT source_service, COUNT(*) as count FROM ${HTTP_TRAFFIC} GROUP BY source_service ORDER BY count DESC
198
+ `).all()
199
+ const byStatus = SqliteStore.db.prepare(`
200
+ SELECT status_code, COUNT(*) as count FROM ${HTTP_TRAFFIC} GROUP BY status_code ORDER BY count DESC
201
+ `).all()
202
+ const avgResponseTime = SqliteStore.db.prepare(`
203
+ SELECT AVG(response_time_ms) as avg_ms FROM ${HTTP_TRAFFIC} WHERE response_time_ms IS NOT NULL
204
+ `).get()
205
+ const errorCount = SqliteStore.db.prepare(`
206
+ SELECT COUNT(*) as count FROM ${HTTP_TRAFFIC} WHERE status_code >= 400 OR error IS NOT NULL
207
+ `).get().count
208
+ return {
209
+ total,
210
+ errorCount,
211
+ errorRate: total > 0 ? (errorCount / total * 100).toFixed(1) : '0.0',
212
+ avgResponseTimeMs: Math.round(avgResponseTime?.avg_ms || 0),
213
+ byMethod,
214
+ byHost,
215
+ byStatus
216
+ }
217
+ } catch (err) {
218
+ Logger.log({ level: 'error', message: 'Failed to get http traffic stats', data: { error: err.message } })
219
+ return { total: 0, errorCount: 0, errorRate: '0.0', avgResponseTimeMs: 0, byMethod: [], byHost: [], byStatus: [] }
220
+ }
221
+ }
222
+ }
223
+
224
+ export const HttpProxy = new HttpProxyClass()