@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,15 @@
1
+ import KoaRouter from 'koa-router'
2
+ import { Controllers } from './Controllers.js'
3
+
4
+ const Router = new KoaRouter()
5
+ const v1 = new KoaRouter({ prefix: '/v1/snapshots' })
6
+
7
+ v1.post('/create', Controllers.createSnapshot)
8
+ v1.post('/restore', Controllers.restoreSnapshot)
9
+ v1.post('/list', Controllers.listSnapshots)
10
+ v1.post('/details', Controllers.getSnapshotDetails)
11
+ v1.post('/delete', Controllers.deleteSnapshot)
12
+
13
+ Router.use(v1.routes())
14
+
15
+ export { Router }
@@ -0,0 +1,23 @@
1
+ import Joi from 'joi'
2
+
3
+ export const Schemas = {
4
+ create: Joi.object({
5
+ services: Joi.array().items(Joi.string().valid('postgres', 'firestore', 'sqlite')).optional(),
6
+ metadata: Joi.object().optional()
7
+ }),
8
+
9
+ restore: Joi.object({
10
+ snapshotId: Joi.string().required(),
11
+ restartServices: Joi.boolean().default(true)
12
+ }),
13
+
14
+ delete: Joi.object({
15
+ snapshotId: Joi.string().optional()
16
+ }),
17
+
18
+ details: Joi.object({
19
+ snapshotId: Joi.string().required()
20
+ }),
21
+
22
+ list: Joi.object({})
23
+ }
@@ -0,0 +1,49 @@
1
+ import { Logic } from './Logic.js'
2
+
3
+ export const Controllers = {
4
+ async register (ctx) {
5
+ const { traceId } = ctx.state
6
+ const { prefix, target, pathRewrite, description } = ctx.request.body
7
+ const result = await Logic.register({ prefix, target, pathRewrite, description, traceId })
8
+ ctx.reply(result)
9
+ },
10
+
11
+ async list (ctx) {
12
+ const { traceId } = ctx.state
13
+ const result = await Logic.list({ traceId })
14
+ ctx.reply(result)
15
+ },
16
+
17
+ async remove (ctx) {
18
+ const { traceId } = ctx.state
19
+ const { prefix } = ctx.request.body
20
+ const result = await Logic.remove({ prefix, traceId })
21
+ ctx.reply(result)
22
+ },
23
+
24
+ async test (ctx) {
25
+ const { traceId } = ctx.state
26
+ const { prefix } = ctx.request.body
27
+ const result = await Logic.test({ prefix, traceId })
28
+ ctx.reply(result)
29
+ },
30
+
31
+ async tunnelStatus (ctx) {
32
+ const { traceId } = ctx.state
33
+ const result = await Logic.tunnelStatus({ traceId })
34
+ ctx.reply(result)
35
+ },
36
+
37
+ async settingsGet (ctx) {
38
+ const { traceId } = ctx.state
39
+ const result = Logic.settingsGet({ traceId })
40
+ ctx.reply(result)
41
+ },
42
+
43
+ async settingsUpdate (ctx) {
44
+ const { traceId } = ctx.state
45
+ const { settings } = ctx.request.body
46
+ const result = Logic.settingsUpdate({ settings, traceId })
47
+ ctx.reply(result)
48
+ }
49
+ }
@@ -0,0 +1,137 @@
1
+ import { WebhookProxy } from '../../singletons/WebhookProxy.js'
2
+
3
+ export const Logic = {
4
+ async register (params) {
5
+ const { prefix, target, pathRewrite, description, traceId } = params
6
+ if (!prefix || !target) {
7
+ return {
8
+ success: false,
9
+ message: 'prefix and target are required',
10
+ traceId
11
+ }
12
+ }
13
+ WebhookProxy.register({ prefix, target, pathRewrite, description })
14
+ return {
15
+ route: { prefix, target, pathRewrite, description },
16
+ proxyUrl: `/v1/webhooks/proxy/${prefix}`,
17
+ message: `Webhook route registered: /v1/webhooks/proxy/${prefix} -> ${target}${pathRewrite || ''}`,
18
+ traceId
19
+ }
20
+ },
21
+
22
+ async list (params) {
23
+ const { traceId } = params
24
+ const { data, total } = WebhookProxy.listRoutes()
25
+ const ngrokDomain = WebhookProxy.getSetting('ngrok_domain')
26
+ const routes = data.map(route => ({
27
+ ...route,
28
+ proxyUrl: `/v1/webhooks/proxy/${route.prefix}`,
29
+ ...(ngrokDomain && {
30
+ webhookUrl: `https://${ngrokDomain}/v1/webhooks/proxy/${route.prefix}`
31
+ })
32
+ }))
33
+ return { routes, total, traceId }
34
+ },
35
+
36
+ async remove (params) {
37
+ const { prefix, traceId } = params
38
+ if (!prefix) {
39
+ return { success: false, message: 'prefix is required', traceId }
40
+ }
41
+ WebhookProxy.remove(prefix)
42
+ return { message: `Webhook route '${prefix}' removed`, traceId }
43
+ },
44
+
45
+ async test (params) {
46
+ const { prefix, traceId } = params
47
+ if (!prefix) {
48
+ return { success: false, message: 'prefix is required', traceId }
49
+ }
50
+ const route = WebhookProxy.getRoute(prefix)
51
+ if (!route) {
52
+ return { reachable: false, message: 'Route not found', traceId }
53
+ }
54
+ try {
55
+ const testUrl = route.pathRewrite
56
+ ? route.target + route.pathRewrite
57
+ : route.target
58
+ const response = await fetch(testUrl, {
59
+ method: 'GET',
60
+ signal: AbortSignal.timeout(5000)
61
+ })
62
+ return {
63
+ reachable: true,
64
+ statusCode: response.status,
65
+ message: `Target responded with ${response.status}`,
66
+ traceId
67
+ }
68
+ } catch (error) {
69
+ return {
70
+ reachable: false,
71
+ message: `Cannot reach target: ${error.message}`,
72
+ traceId
73
+ }
74
+ }
75
+ },
76
+
77
+ async tunnelStatus (params) {
78
+ const { traceId } = params
79
+ const ngrokDomain = WebhookProxy.getSetting('ngrok_domain')
80
+ const { data } = WebhookProxy.listRoutes()
81
+ // Check if ngrok tunnel is live by pinging the public domain directly
82
+ // This avoids Docker networking issues with ngrok's local API (port 4040)
83
+ let running = false
84
+ let tunnelUrl = null
85
+ if (ngrokDomain) {
86
+ tunnelUrl = `https://${ngrokDomain}`
87
+ try {
88
+ const response = await fetch(`${tunnelUrl}/v1/webhooks/list`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({}),
92
+ signal: AbortSignal.timeout(5000)
93
+ })
94
+ running = response.ok
95
+ } catch {
96
+ running = false
97
+ }
98
+ if (!running) tunnelUrl = null
99
+ }
100
+ const routes = data.map(route => ({
101
+ prefix: route.prefix,
102
+ proxyPath: `/v1/webhooks/proxy/${route.prefix}`,
103
+ ...(tunnelUrl && {
104
+ webhookUrl: `${tunnelUrl}/v1/webhooks/proxy/${route.prefix}`
105
+ })
106
+ }))
107
+ return {
108
+ running,
109
+ url: tunnelUrl,
110
+ domain: ngrokDomain,
111
+ webhookBaseUrl: tunnelUrl ? `${tunnelUrl}/v1/webhooks/proxy` : null,
112
+ routes,
113
+ traceId
114
+ }
115
+ },
116
+
117
+ settingsGet (params) {
118
+ const { traceId } = params
119
+ const settings = WebhookProxy.getSettings()
120
+ return { settings, traceId }
121
+ },
122
+
123
+ settingsUpdate (params) {
124
+ const { settings, traceId } = params
125
+ if (!settings || typeof settings !== 'object') {
126
+ return { success: false, message: 'settings object is required', traceId }
127
+ }
128
+ for (const [key, value] of Object.entries(settings)) {
129
+ WebhookProxy.setSetting(key, value)
130
+ }
131
+ return {
132
+ message: `Updated ${Object.keys(settings).length} setting(s)`,
133
+ settings: WebhookProxy.getSettings(),
134
+ traceId
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,12 @@
1
+ import KoaRouter from 'koa-router'
2
+ import { Controllers } from './Controllers.js'
3
+
4
+ export const Router = new KoaRouter({ prefix: '/v1/webhooks' })
5
+
6
+ Router.post('/register', Controllers.register)
7
+ Router.post('/list', Controllers.list)
8
+ Router.post('/remove', Controllers.remove)
9
+ Router.post('/test', Controllers.test)
10
+ Router.post('/tunnel/status', Controllers.tunnelStatus)
11
+ Router.post('/settings/get', Controllers.settingsGet)
12
+ Router.post('/settings/update', Controllers.settingsUpdate)
@@ -0,0 +1,31 @@
1
+ import Joi from 'joi'
2
+
3
+ export const Schemas = {
4
+ register: {
5
+ body: Joi.object({
6
+ prefix: Joi.string().pattern(/^[a-z0-9-]+$/).required()
7
+ .description('URL-safe prefix for the webhook route'),
8
+ target: Joi.string().uri({ scheme: ['http', 'https'] }).required()
9
+ .description('Target URL to forward requests to'),
10
+ pathRewrite: Joi.string().optional()
11
+ .description('Path prefix on the target (e.g., /v1/webhooks)'),
12
+ description: Joi.string().optional()
13
+ .description('Human-readable description')
14
+ })
15
+ },
16
+ remove: {
17
+ body: Joi.object({
18
+ prefix: Joi.string().required()
19
+ })
20
+ },
21
+ test: {
22
+ body: Joi.object({
23
+ prefix: Joi.string().required()
24
+ })
25
+ },
26
+ settingsUpdate: {
27
+ body: Joi.object({
28
+ settings: Joi.object().required()
29
+ })
30
+ }
31
+ }
@@ -0,0 +1,147 @@
1
+ import Moment from 'moment'
2
+
3
+ const {
4
+ NODE_ENV = 'development',
5
+ LOG_LEVEL = 'info',
6
+ WEB_UI_PORT,
7
+ PUBSUB_EMULATOR_HOST,
8
+ PUBSUB_PORT,
9
+ PUBSUB_PROJECT_ID = 'tipi-development',
10
+ SHADOW_POLL_INTERVAL_MS = '200',
11
+ SHADOW_SUBSCRIPTION_CHECK_INTERVAL_MS = '5000',
12
+ LOGGING_PORT,
13
+ MQTT_PORT,
14
+ AWS_IOT_PORT,
15
+ FIRESTORE_PORT,
16
+ FIRESTORE_EMULATOR_HOST,
17
+ FIRESTORE_PROJECT_ID,
18
+ FIRESTORE_PROJECT_IDS,
19
+ DATA_DIR,
20
+ REDIS_HOST,
21
+ REDIS_PORT,
22
+ REDIS_LOGS_HOST,
23
+ REDIS_LOGS_PORT,
24
+ REDIS_LOGS_DB,
25
+ POSTGRES_HOST,
26
+ POSTGRES_PORT,
27
+ POSTGRES_USER,
28
+ POSTGRES_PASSWORD,
29
+ POSTGRES_DATABASE,
30
+ APP_GATEWAY_PROPERTY_ID,
31
+ APP_GATEWAY_DEVICE_NATIVE_URL,
32
+ APP_GATEWAY_DEVICE_SIMULATOR_URL,
33
+ APP_GATEWAY_SCAN_INTERVAL_SECONDS,
34
+ APP_GATEWAY_AUTO_START,
35
+ APP_GATEWAY_RETRY_INTERVAL_SECONDS,
36
+ DOCKER_LOG_CAPTURE_ENABLED,
37
+ DOCKER_LOG_NETWORK,
38
+ DOCKER_LOG_EXCLUDED_CONTAINERS,
39
+ FUNCTIONS_SOURCE_PATH,
40
+ FUNCTIONS_BASE_PORT,
41
+ FUNCTIONS_MAX_INSTANCES,
42
+ HOST_PROJECT_DIR,
43
+ DB_MAINTENANCE_INTERVAL_HOURS,
44
+ DB_RETENTION_LOGGING_DAYS,
45
+ DB_RETENTION_HTTP_TRAFFIC_DAYS,
46
+ DB_RETENTION_MQTT_DAYS,
47
+ DB_RETENTION_PUBSUB_DAYS,
48
+ DB_RETENTION_INVOCATIONS_DAYS,
49
+ DB_MAX_LOGGING_ENTRIES
50
+ } = process.env
51
+
52
+ const runId = Moment().valueOf()
53
+ const envShortNameMap = {
54
+ development: 'dev',
55
+ production: 'prd',
56
+ test: 'test'
57
+ }
58
+
59
+ export const Application = {
60
+ runId,
61
+ environment: NODE_ENV,
62
+ envShortName: envShortNameMap[NODE_ENV],
63
+ logLevel: LOG_LEVEL,
64
+ ports: {
65
+ webUi: parseInt(WEB_UI_PORT),
66
+ pubsub: parseInt(PUBSUB_PORT) || 8085,
67
+ logging: parseInt(LOGGING_PORT),
68
+ mqtt: parseInt(MQTT_PORT),
69
+ awsIot: parseInt(AWS_IOT_PORT) || 8086,
70
+ firestore: parseInt(FIRESTORE_PORT)
71
+ },
72
+ pubsub: {
73
+ projectId: PUBSUB_PROJECT_ID,
74
+ emulatorHost: PUBSUB_EMULATOR_HOST?.split(':')[0] || 'localhost',
75
+ emulatorPort: parseInt(PUBSUB_EMULATOR_HOST?.split(':')[1]) || parseInt(PUBSUB_PORT) || 8085,
76
+ shadowPollIntervalMs: parseInt(SHADOW_POLL_INTERVAL_MS) || 200,
77
+ shadowSubscriptionCheckIntervalMs: parseInt(SHADOW_SUBSCRIPTION_CHECK_INTERVAL_MS) || 5000
78
+ },
79
+ storage: {
80
+ dataDir: DATA_DIR
81
+ },
82
+ redis: {
83
+ host: REDIS_HOST || 'localhost',
84
+ port: parseInt(REDIS_PORT) || 6379,
85
+ db: 0,
86
+ url: `redis://${REDIS_HOST || 'localhost'}:${REDIS_PORT || 6379}`
87
+ },
88
+ redisLogs: {
89
+ host: REDIS_LOGS_HOST || REDIS_HOST || 'localhost',
90
+ port: parseInt(REDIS_LOGS_PORT) || parseInt(REDIS_PORT) || 6379,
91
+ db: parseInt(REDIS_LOGS_DB) || 0,
92
+ url: `redis://${REDIS_LOGS_HOST || REDIS_HOST || 'localhost'}:${REDIS_LOGS_PORT || REDIS_PORT || 6379}`
93
+ },
94
+ postgres: {
95
+ host: POSTGRES_HOST || 'localhost',
96
+ port: parseInt(POSTGRES_PORT) || 5432,
97
+ user: POSTGRES_USER || 'postgres',
98
+ password: POSTGRES_PASSWORD || 'postgres',
99
+ database: POSTGRES_DATABASE || 'postgres'
100
+ },
101
+ firestore: {
102
+ projectId: FIRESTORE_PROJECT_ID || 'tipi-development',
103
+ projectIds: FIRESTORE_PROJECT_IDS
104
+ ? FIRESTORE_PROJECT_IDS.split(',').map(s => s.trim())
105
+ : [FIRESTORE_PROJECT_ID || 'tipi-development'],
106
+ emulatorHost: FIRESTORE_EMULATOR_HOST || `localhost:${FIRESTORE_PORT || 8080}`
107
+ },
108
+ messageHistory: {
109
+ enabled: true,
110
+ maxMessages: 10000, // Global limit (size-based, not time-based)
111
+ minMessages: 100, // Always keep at least this many
112
+ cleanupIntervalHours: 24, // How often to check
113
+ cleanupThreshold: 0.9 // Trigger cleanup at 90% capacity
114
+ },
115
+ appGateway: {
116
+ propertyId: APP_GATEWAY_PROPERTY_ID || '550e8400-e29b-41d4-a716-446655440000',
117
+ deviceNativeUrl: APP_GATEWAY_DEVICE_NATIVE_URL || 'http://localhost:3000',
118
+ deviceSimulatorUrl: APP_GATEWAY_DEVICE_SIMULATOR_URL || 'http://localhost:3001',
119
+ scanIntervalSeconds: parseInt(APP_GATEWAY_SCAN_INTERVAL_SECONDS) || 30,
120
+ autoStart: APP_GATEWAY_AUTO_START !== 'false',
121
+ retryIntervalSeconds: parseInt(APP_GATEWAY_RETRY_INTERVAL_SECONDS) || 10
122
+ },
123
+ logging: {
124
+ dockerCapture: DOCKER_LOG_CAPTURE_ENABLED !== 'false',
125
+ dockerNetwork: DOCKER_LOG_NETWORK || 'goki-network',
126
+ dockerExcludedContainers: DOCKER_LOG_EXCLUDED_CONTAINERS
127
+ ? DOCKER_LOG_EXCLUDED_CONTAINERS.split(',').map(s => s.trim())
128
+ : null
129
+ },
130
+ functions: {
131
+ sourcePath: FUNCTIONS_SOURCE_PATH || null,
132
+ basePort: parseInt(FUNCTIONS_BASE_PORT) || 9100,
133
+ maxInstances: parseInt(FUNCTIONS_MAX_INSTANCES) || 50
134
+ },
135
+ hostProjectDir: HOST_PROJECT_DIR || null,
136
+ dbMaintenance: {
137
+ intervalHours: parseInt(DB_MAINTENANCE_INTERVAL_HOURS) || 6,
138
+ maxLoggingEntries: parseInt(DB_MAX_LOGGING_ENTRIES) || 100000,
139
+ retentionDays: {
140
+ loggingEntries: parseInt(DB_RETENTION_LOGGING_DAYS) || 7,
141
+ httpTraffic: parseInt(DB_RETENTION_HTTP_TRAFFIC_DAYS) || 3,
142
+ mqttMessages: parseInt(DB_RETENTION_MQTT_DAYS) || 3,
143
+ pubsubMessages: parseInt(DB_RETENTION_PUBSUB_DAYS) || 3,
144
+ functionInvocations: parseInt(DB_RETENTION_INVOCATIONS_DAYS) || 7
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,13 @@
1
+ const {
2
+ AUTO_FLUSH_INTERVAL_MS
3
+ } = process.env
4
+
5
+ export const Default = {
6
+ storage: {
7
+ autoFlushIntervalMs: parseInt(AUTO_FLUSH_INTERVAL_MS)
8
+ },
9
+ pagination: Object.freeze({
10
+ defaultLimit: 50,
11
+ maxLimit: 1000
12
+ })
13
+ }
@@ -0,0 +1,235 @@
1
+ import Queue from 'bull'
2
+ import { SqliteStore } from '../singletons/SqliteStore.js'
3
+ import { LOGGING_ENTRIES, LOGGING_LOGS } from '../db/Tables.js'
4
+ import { Logger } from '../singletons/Logger.js'
5
+ import { LogBroadcaster } from '../singletons/LogBroadcaster.js'
6
+
7
+ export class BlackboxLogsConsumer {
8
+ constructor (redisConfigs) {
9
+ this.queues = []
10
+ this.redisConfigs = redisConfigs
11
+ redisConfigs.forEach((config, index) => {
12
+ const queueName = 'blackboxLogs'
13
+ const queue = new Queue(queueName, {
14
+ redis: config,
15
+ settings: {
16
+ drainDelay: 1000,
17
+ stalledInterval: 30000
18
+ },
19
+ limiter: {
20
+ max: 20,
21
+ duration: 1000
22
+ },
23
+ defaultJobOptions: {
24
+ attempts: 3,
25
+ backoff: {
26
+ type: 'exponential',
27
+ delay: 2000
28
+ },
29
+ removeOnComplete: true,
30
+ removeOnFail: true,
31
+ timeout: 30000
32
+ }
33
+ })
34
+ queue.on('error', (error) => {
35
+ Logger.log({
36
+ level: 'error',
37
+ message: `Bull queue error on Redis ${config.host}:${config.port}`,
38
+ data: { error: error.message, stack: error.stack }
39
+ })
40
+ })
41
+ queue.on('ready', () => {
42
+ Logger.log({
43
+ level: 'info',
44
+ message: `Bull queue consumer connected to Redis ${config.host}:${config.port}`,
45
+ data: { queueName, redis: config }
46
+ })
47
+ })
48
+ queue.process(async (job) => {
49
+ const startTime = Date.now()
50
+ try {
51
+ Logger.log({
52
+ level: 'debug',
53
+ message: 'Bull queue job received',
54
+ data: {
55
+ jobId: job.id,
56
+ redisHost: config.host,
57
+ redisPort: config.port,
58
+ service: job.data?.service
59
+ }
60
+ })
61
+ await this.processLog(job.data, config, job)
62
+ } catch (error) {
63
+ Logger.log({
64
+ level: 'error',
65
+ message: 'Failed to process Bull queue log',
66
+ data: {
67
+ jobId: job.id,
68
+ attemptNumber: job.attemptsMade + 1,
69
+ maxAttempts: job.opts.attempts,
70
+ willRetry: job.attemptsMade < job.opts.attempts - 1,
71
+ error: error.message,
72
+ stack: error.stack,
73
+ redisHost: config.host,
74
+ redisPort: config.port,
75
+ processingTime: Date.now() - startTime
76
+ }
77
+ })
78
+ throw error
79
+ }
80
+ })
81
+ this.queues.push({ queue, config })
82
+ Logger.log({
83
+ level: 'info',
84
+ message: 'Bull queue consumer registered',
85
+ data: { queueName, redisHost: config.host, redisPort: config.port }
86
+ })
87
+ })
88
+ }
89
+
90
+ async processLog (data, redisConfig, job) {
91
+ const { service, log } = data
92
+ Logger.log({
93
+ level: 'debug',
94
+ message: 'Processing Bull queue job',
95
+ data: {
96
+ service,
97
+ message: log?.message,
98
+ redisHost: redisConfig.host,
99
+ redisPort: redisConfig.port
100
+ }
101
+ })
102
+ if (!log || !log.level) {
103
+ Logger.log({
104
+ level: 'warn',
105
+ message: 'Invalid log entry in Bull queue - job will be marked complete',
106
+ data: { service, log, jobId: job?.id }
107
+ })
108
+ return
109
+ }
110
+ const severityMap = {
111
+ error: 'ERROR',
112
+ warn: 'WARNING',
113
+ info: 'INFO',
114
+ http: 'INFO',
115
+ verbose: 'DEBUG',
116
+ debug: 'DEBUG'
117
+ }
118
+ const logName = `projects/dev/logs/${service || 'unknown'}`
119
+ const severity = severityMap[log.level] || 'INFO'
120
+ const timestamp = log.timestamp || new Date().toISOString()
121
+ const timeWindow = 2000
122
+ const timestampDate = new Date(timestamp)
123
+ const startTime = new Date(timestampDate.getTime() - timeWindow).toISOString()
124
+ const endTime = new Date(timestampDate.getTime() + timeWindow).toISOString()
125
+ const serviceName = service || 'unknown'
126
+ const isDuplicate = SqliteStore.exists(LOGGING_ENTRIES,
127
+ 'text_payload = ? AND service_name = ? AND timestamp >= ? AND timestamp <= ?',
128
+ [log.message, serviceName, startTime, endTime]
129
+ )
130
+ if (isDuplicate) {
131
+ return
132
+ }
133
+ const logId = serviceName
134
+ try {
135
+ SqliteStore.create(LOGGING_LOGS, {
136
+ projectId: 'dev',
137
+ logId,
138
+ logName,
139
+ createdAt: new Date().toISOString(),
140
+ updatedAt: new Date().toISOString()
141
+ })
142
+ } catch (error) {
143
+ // Ignore duplicate errors - log stream already exists
144
+ if (!error.message.includes('UNIQUE')) {
145
+ throw error
146
+ }
147
+ }
148
+ // Merge log.metadata into labels (minus traceId/requestId which get own columns)
149
+ const metadata = log.metadata || {}
150
+ const labels = { ...metadata }
151
+ delete labels.traceId
152
+ delete labels.requestId
153
+ const logEntry = {
154
+ logName,
155
+ serviceName,
156
+ severity,
157
+ source: 'bull-queue',
158
+ level: log.level,
159
+ timestamp,
160
+ receiveTimestamp: new Date().toISOString(),
161
+ textPayload: log.message,
162
+ jsonPayload: JSON.stringify(log),
163
+ labels: JSON.stringify(labels)
164
+ }
165
+ if (metadata.traceId) logEntry.trace = metadata.traceId
166
+ if (metadata.requestId) logEntry.insertId = metadata.requestId
167
+ // Extract error info
168
+ if (metadata.error) {
169
+ logEntry.errorMessage = typeof metadata.error === 'string'
170
+ ? metadata.error
171
+ : metadata.error.message || JSON.stringify(metadata.error)
172
+ } else if (log.stack) {
173
+ logEntry.errorMessage = log.message
174
+ }
175
+ const entry = await SqliteStore.create(LOGGING_ENTRIES, logEntry)
176
+ LogBroadcaster.broadcast(logEntry)
177
+ Logger.log({
178
+ level: 'debug',
179
+ message: 'Processed Bull queue log successfully',
180
+ data: {
181
+ service,
182
+ severity,
183
+ logName,
184
+ redisHost: redisConfig.host,
185
+ redisPort: redisConfig.port,
186
+ entryId: entry._id
187
+ }
188
+ })
189
+ }
190
+
191
+ async close () {
192
+ Logger.log({
193
+ level: 'info',
194
+ message: 'Closing Bull queue consumers'
195
+ })
196
+ for (const { queue, config } of this.queues) {
197
+ await queue.close()
198
+ Logger.log({
199
+ level: 'info',
200
+ message: 'Closed Bull queue consumer',
201
+ data: { redis: `${config.host}:${config.port}` }
202
+ })
203
+ }
204
+ }
205
+
206
+ getStats () {
207
+ return Promise.all(
208
+ this.queues.map(async ({ queue, config }) => {
209
+ const counts = await queue.getJobCounts()
210
+ return {
211
+ redis: `${config.host}:${config.port}`,
212
+ counts
213
+ }
214
+ })
215
+ )
216
+ }
217
+
218
+ async getHealth () {
219
+ const health = []
220
+ for (const { queue, config } of this.queues) {
221
+ const counts = await queue.getJobCounts()
222
+ const isHealthy = counts.failed < 100 && counts.active < 50
223
+ health.push({
224
+ redis: `${config.host}:${config.port}`,
225
+ healthy: isHealthy,
226
+ counts,
227
+ warnings: [
228
+ counts.failed >= 100 ? 'High failed job count' : null,
229
+ counts.active >= 50 ? 'High active job count' : null
230
+ ].filter(Boolean)
231
+ })
232
+ }
233
+ return health
234
+ }
235
+ }