@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,169 @@
1
+ export function registerServicesTools (server, apiClient) {
2
+ server.tool(
3
+ 'services_list',
4
+ 'List all dev-tools platform services and their status',
5
+ {},
6
+ async () => {
7
+ try {
8
+ const data = await apiClient.post('/v1/services/list')
9
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
10
+ } catch (error) {
11
+ return { content: [{ type: 'text', text: `Failed to list services: ${error.message}` }], isError: true }
12
+ }
13
+ }
14
+ )
15
+
16
+ server.tool(
17
+ 'services_status',
18
+ 'Get detailed status of all services',
19
+ {},
20
+ async () => {
21
+ try {
22
+ const data = await apiClient.post('/v1/services/status')
23
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
24
+ } catch (error) {
25
+ return { content: [{ type: 'text', text: `Failed to get services status: ${error.message}` }], isError: true }
26
+ }
27
+ }
28
+ )
29
+
30
+ server.tool(
31
+ 'services_restart',
32
+ 'Restart dev-tools services',
33
+ {},
34
+ async () => {
35
+ try {
36
+ const data = await apiClient.post('/v1/services/restart')
37
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
38
+ } catch (error) {
39
+ return { content: [{ type: 'text', text: `Failed to restart services: ${error.message}` }], isError: true }
40
+ }
41
+ }
42
+ )
43
+
44
+ server.tool(
45
+ 'scheduler_start',
46
+ 'Start the one-minute scheduler tick publisher',
47
+ {},
48
+ async () => {
49
+ try {
50
+ const data = await apiClient.post('/v1/scheduler/tick/start')
51
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
52
+ } catch (error) {
53
+ return { content: [{ type: 'text', text: `Failed to start scheduler: ${error.message}` }], isError: true }
54
+ }
55
+ }
56
+ )
57
+
58
+ server.tool(
59
+ 'scheduler_stop',
60
+ 'Stop the scheduler tick',
61
+ {},
62
+ async () => {
63
+ try {
64
+ const data = await apiClient.post('/v1/scheduler/tick/stop')
65
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
66
+ } catch (error) {
67
+ return { content: [{ type: 'text', text: `Failed to stop scheduler: ${error.message}` }], isError: true }
68
+ }
69
+ }
70
+ )
71
+
72
+ server.tool(
73
+ 'scheduler_status',
74
+ 'Get scheduler tick status (running/stopped, interval)',
75
+ {},
76
+ async () => {
77
+ try {
78
+ const data = await apiClient.post('/v1/scheduler/tick/status')
79
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
80
+ } catch (error) {
81
+ return { content: [{ type: 'text', text: `Failed to get scheduler status: ${error.message}` }], isError: true }
82
+ }
83
+ }
84
+ )
85
+
86
+ server.tool(
87
+ 'scheduler_trigger',
88
+ 'Manually trigger a scheduler tick (useful for testing)',
89
+ {},
90
+ async () => {
91
+ try {
92
+ const data = await apiClient.post('/v1/scheduler/tick/trigger')
93
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
94
+ } catch (error) {
95
+ return { content: [{ type: 'text', text: `Failed to trigger scheduler tick: ${error.message}` }], isError: true }
96
+ }
97
+ }
98
+ )
99
+
100
+ server.tool(
101
+ 'gateway_start',
102
+ 'Start the app gateway service',
103
+ {},
104
+ async () => {
105
+ try {
106
+ const data = await apiClient.post('/v1/gateway/start')
107
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
108
+ } catch (error) {
109
+ return { content: [{ type: 'text', text: `Failed to start gateway: ${error.message}` }], isError: true }
110
+ }
111
+ }
112
+ )
113
+
114
+ server.tool(
115
+ 'gateway_stop',
116
+ 'Stop the app gateway service',
117
+ {},
118
+ async () => {
119
+ try {
120
+ const data = await apiClient.post('/v1/gateway/stop')
121
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
122
+ } catch (error) {
123
+ return { content: [{ type: 'text', text: `Failed to stop gateway: ${error.message}` }], isError: true }
124
+ }
125
+ }
126
+ )
127
+
128
+ server.tool(
129
+ 'gateway_status',
130
+ 'Get the app gateway status',
131
+ {},
132
+ async () => {
133
+ try {
134
+ const data = await apiClient.post('/v1/gateway/status')
135
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
136
+ } catch (error) {
137
+ return { content: [{ type: 'text', text: `Failed to get gateway status: ${error.message}` }], isError: true }
138
+ }
139
+ }
140
+ )
141
+
142
+ server.tool(
143
+ 'gateway_trigger_scan',
144
+ 'Trigger a gateway scan for connected services',
145
+ {},
146
+ async () => {
147
+ try {
148
+ const data = await apiClient.post('/v1/gateway/triggerScan')
149
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
150
+ } catch (error) {
151
+ return { content: [{ type: 'text', text: `Failed to trigger gateway scan: ${error.message}` }], isError: true }
152
+ }
153
+ }
154
+ )
155
+
156
+ server.tool(
157
+ 'gateway_health_check',
158
+ 'Run health checks on all gateway endpoints',
159
+ {},
160
+ async () => {
161
+ try {
162
+ const data = await apiClient.post('/v1/gateway/healthCheck')
163
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
164
+ } catch (error) {
165
+ return { content: [{ type: 'text', text: `Failed to run gateway health check: ${error.message}` }], isError: true }
166
+ }
167
+ }
168
+ )
169
+ }
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod'
2
+
3
+ export function registerSnapshotTools (server, apiClient) {
4
+ server.tool(
5
+ 'snapshots_create',
6
+ 'Create a snapshot of development environment (PostgreSQL, Firestore, and SQLite metadata). This saves the current state before running tests.',
7
+ {
8
+ services: z.array(z.enum(['postgres', 'firestore', 'sqlite'])).optional().describe('Services to snapshot. Defaults to all configured services.')
9
+ },
10
+ async ({ services }) => {
11
+ try {
12
+ const body = {}
13
+ if (services !== undefined) body.services = services
14
+ const data = await apiClient.post('/v1/snapshots/create', body)
15
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
16
+ } catch (error) {
17
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
18
+ }
19
+ }
20
+ )
21
+
22
+ server.tool(
23
+ 'snapshots_restore',
24
+ 'Restore development environment from a snapshot. This will stop microservices, restore data, and restart them.',
25
+ {
26
+ snapshotId: z.string().describe('Snapshot ID to restore from (e.g., "e2e_2026-02-26_10-30-45")'),
27
+ restartServices: z.boolean().optional().describe('Whether to restart microservices after restore (default: true)')
28
+ },
29
+ async ({ snapshotId, restartServices }) => {
30
+ try {
31
+ const body = { snapshotId }
32
+ if (restartServices !== undefined) body.restartServices = restartServices
33
+ const data = await apiClient.post('/v1/snapshots/restore', body)
34
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
35
+ } catch (error) {
36
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
37
+ }
38
+ }
39
+ )
40
+
41
+ server.tool(
42
+ 'snapshots_list',
43
+ 'List all available snapshots with their metadata',
44
+ {},
45
+ async () => {
46
+ try {
47
+ const data = await apiClient.post('/v1/snapshots/list', {})
48
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
49
+ } catch (error) {
50
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
51
+ }
52
+ }
53
+ )
54
+
55
+ server.tool(
56
+ 'snapshots_details',
57
+ 'Get detailed information about a specific snapshot',
58
+ {
59
+ snapshotId: z.string().describe('Snapshot ID to get details for')
60
+ },
61
+ async ({ snapshotId }) => {
62
+ try {
63
+ const data = await apiClient.post('/v1/snapshots/details', { snapshotId })
64
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
65
+ } catch (error) {
66
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
67
+ }
68
+ }
69
+ )
70
+
71
+ server.tool(
72
+ 'snapshots_delete',
73
+ 'Delete a snapshot and its files',
74
+ {
75
+ snapshotId: z.string().optional().describe('Snapshot ID to delete. If omitted, deletes all snapshots.')
76
+ },
77
+ async ({ snapshotId }) => {
78
+ try {
79
+ const body = {}
80
+ if (snapshotId !== undefined) body.snapshotId = snapshotId
81
+ const data = await apiClient.post('/v1/snapshots/delete', body)
82
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
83
+ } catch (error) {
84
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }
85
+ }
86
+ }
87
+ )
88
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod'
2
+
3
+ export function registerWebhooksTools (server, apiClient) {
4
+ server.tool(
5
+ 'webhooks_register',
6
+ 'Register a webhook endpoint for forwarding requests',
7
+ {
8
+ prefix: z.string().describe('URL prefix to match incoming requests against'),
9
+ target: z.string().describe('Target URL to forward matching requests to'),
10
+ pathRewrite: z.string().optional().describe('Optional path rewrite pattern for the forwarded request'),
11
+ description: z.string().optional().describe('Optional human-readable description of the webhook')
12
+ },
13
+ async ({ prefix, target, pathRewrite, description }) => {
14
+ try {
15
+ const body = { prefix, target }
16
+ if (pathRewrite !== undefined) body.pathRewrite = pathRewrite
17
+ if (description !== undefined) body.description = description
18
+ const data = await apiClient.post('/v1/webhooks/register', body)
19
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
20
+ } catch (error) {
21
+ return { content: [{ type: 'text', text: `Failed to register webhook: ${error.message}` }], isError: true }
22
+ }
23
+ }
24
+ )
25
+
26
+ server.tool(
27
+ 'webhooks_list',
28
+ 'List all registered webhooks',
29
+ {},
30
+ async () => {
31
+ try {
32
+ const data = await apiClient.post('/v1/webhooks/list')
33
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
34
+ } catch (error) {
35
+ return { content: [{ type: 'text', text: `Failed to list webhooks: ${error.message}` }], isError: true }
36
+ }
37
+ }
38
+ )
39
+
40
+ server.tool(
41
+ 'webhooks_remove',
42
+ 'Remove a registered webhook by its prefix',
43
+ {
44
+ prefix: z.string().describe('URL prefix of the webhook to remove')
45
+ },
46
+ async ({ prefix }) => {
47
+ try {
48
+ const data = await apiClient.post('/v1/webhooks/remove', { prefix })
49
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
50
+ } catch (error) {
51
+ return { content: [{ type: 'text', text: `Failed to remove webhook: ${error.message}` }], isError: true }
52
+ }
53
+ }
54
+ )
55
+
56
+ server.tool(
57
+ 'webhooks_test',
58
+ 'Send a test request to a webhook endpoint',
59
+ {
60
+ prefix: z.string().describe('URL prefix of the webhook to test')
61
+ },
62
+ async ({ prefix }) => {
63
+ try {
64
+ const data = await apiClient.post('/v1/webhooks/test', { prefix })
65
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
66
+ } catch (error) {
67
+ return { content: [{ type: 'text', text: `Failed to test webhook: ${error.message}` }], isError: true }
68
+ }
69
+ }
70
+ )
71
+
72
+ server.tool(
73
+ 'webhooks_tunnel_status',
74
+ 'Get ngrok tunnel status (connected/disconnected, public URL)',
75
+ {},
76
+ async () => {
77
+ try {
78
+ const data = await apiClient.post('/v1/webhooks/tunnel/status')
79
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
80
+ } catch (error) {
81
+ return { content: [{ type: 'text', text: `Failed to get tunnel status: ${error.message}` }], isError: true }
82
+ }
83
+ }
84
+ )
85
+
86
+ server.tool(
87
+ 'webhooks_settings_get',
88
+ 'Get current webhook/ngrok settings',
89
+ {},
90
+ async () => {
91
+ try {
92
+ const data = await apiClient.post('/v1/webhooks/settings/get')
93
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
94
+ } catch (error) {
95
+ return { content: [{ type: 'text', text: `Failed to get webhook settings: ${error.message}` }], isError: true }
96
+ }
97
+ }
98
+ )
99
+
100
+ server.tool(
101
+ 'webhooks_settings_update',
102
+ 'Update webhook/ngrok settings',
103
+ {
104
+ settings: z.record(z.any()).describe('Settings object with key-value pairs to update')
105
+ },
106
+ async ({ settings }) => {
107
+ try {
108
+ const data = await apiClient.post('/v1/webhooks/settings/update', { settings })
109
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
110
+ } catch (error) {
111
+ return { content: [{ type: 'text', text: `Failed to update webhook settings: ${error.message}` }], isError: true }
112
+ }
113
+ }
114
+ )
115
+ }
@@ -0,0 +1,67 @@
1
+ import httpProxy from 'http-proxy'
2
+ import { Logger } from '../singletons/Logger.js'
3
+
4
+ /**
5
+ * Development proxy middleware
6
+ * Forwards requests to the React dev server running on port 3000
7
+ * Only used in development mode
8
+ */
9
+ export const DevProxy = (targetUrl = 'http://localhost:3000') => {
10
+ const proxy = httpProxy.createProxyServer({
11
+ target: targetUrl,
12
+ changeOrigin: true,
13
+ ws: true, // Support WebSocket (for React Hot Reload)
14
+ logLevel: 'warn'
15
+ })
16
+
17
+ // Handle proxy errors
18
+ proxy.on('error', (err, req, res) => {
19
+ Logger.log({
20
+ level: 'error',
21
+ message: 'Proxy error',
22
+ data: {
23
+ error: err.message,
24
+ url: req.url,
25
+ method: req.method
26
+ }
27
+ })
28
+
29
+ // Send error response
30
+ if (!res.headersSent) {
31
+ res.writeHead(502, { 'Content-Type': 'text/plain' })
32
+ }
33
+ res.end('Proxy Error: Unable to connect to React dev server. Make sure it\'s running on port 3000.')
34
+ })
35
+
36
+ // Koa middleware
37
+ return async (ctx, next) => {
38
+ // Skip proxy for API routes
39
+ if (ctx.path.startsWith('/v1') || ctx.path.startsWith('/v2')) {
40
+ return next()
41
+ }
42
+
43
+ // Log proxy request
44
+ Logger.log({
45
+ level: 'debug',
46
+ message: 'Proxying request to React dev server',
47
+ data: {
48
+ method: ctx.method,
49
+ path: ctx.path,
50
+ target: targetUrl
51
+ }
52
+ })
53
+
54
+ // Proxy the request
55
+ ctx.respond = false // Bypass Koa's response handling
56
+
57
+ return new Promise((resolve, reject) => {
58
+ proxy.web(ctx.req, ctx.res, {}, (err) => {
59
+ if (err) {
60
+ reject(err)
61
+ } else {
62
+ resolve()
63
+ }
64
+ })
65
+ })
66
+ }
67
+ }
@@ -0,0 +1,35 @@
1
+ import { Logger } from '../singletons/Logger.js'
2
+
3
+ export const ErrorCatcher = () => {
4
+ return async (ctx, next) => {
5
+ try {
6
+ await next()
7
+ } catch (error) {
8
+ const isOops = error.isOops
9
+ const status = isOops ? error.data.status : 500
10
+ const errorReason = isOops ? error.data.reason : 'unknownError'
11
+ const message = error.message || 'An unexpected error occurred'
12
+ const traceId = ctx.state.traceId || ctx.request.body?.traceId
13
+ Logger.log({
14
+ level: 'error',
15
+ message: 'Request error',
16
+ data: {
17
+ error: isOops ? error.data : { message: error.message, stack: error.stack },
18
+ traceId,
19
+ path: ctx.path,
20
+ method: ctx.method
21
+ }
22
+ })
23
+ ctx.status = status
24
+ ctx.body = {
25
+ success: false,
26
+ status,
27
+ errorReason,
28
+ message,
29
+ data: {
30
+ traceId
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,215 @@
1
+ import { Readable } from 'stream'
2
+ import zlib from 'zlib'
3
+ import { HttpProxy } from '../singletons/HttpProxy.js'
4
+ import { Logger } from '../singletons/Logger.js'
5
+
6
+ function bufferRequestBody (req) {
7
+ return new Promise((resolve) => {
8
+ const chunks = []
9
+ let size = 0
10
+ req.on('data', (chunk) => {
11
+ chunks.push(chunk)
12
+ size += chunk.length
13
+ })
14
+ req.on('end', () => {
15
+ try {
16
+ resolve(Buffer.concat(chunks))
17
+ } catch {
18
+ resolve(null)
19
+ }
20
+ })
21
+ req.on('error', () => {
22
+ resolve(null)
23
+ })
24
+ if (req.readableEnded || req.complete) {
25
+ resolve(null)
26
+ }
27
+ })
28
+ }
29
+
30
+ function decompressBuffer (buffer, encoding) {
31
+ if (!encoding) return buffer
32
+ try {
33
+ if (encoding === 'gzip' || encoding === 'x-gzip') {
34
+ return zlib.gunzipSync(buffer)
35
+ }
36
+ if (encoding === 'deflate') {
37
+ return zlib.inflateSync(buffer)
38
+ }
39
+ if (encoding === 'br') {
40
+ return zlib.brotliDecompressSync(buffer)
41
+ }
42
+ } catch {
43
+ return buffer
44
+ }
45
+ return buffer
46
+ }
47
+
48
+ function collectStreamBody (stream, contentEncoding) {
49
+ return new Promise((resolve) => {
50
+ const chunks = []
51
+ stream.on('data', (chunk) => {
52
+ chunks.push(chunk)
53
+ })
54
+ stream.on('end', () => {
55
+ try {
56
+ const raw = decompressBuffer(Buffer.concat(chunks), contentEncoding).toString('utf-8')
57
+ resolve({ raw })
58
+ } catch {
59
+ resolve({ raw: null })
60
+ }
61
+ })
62
+ stream.on('error', () => {
63
+ resolve({ raw: null })
64
+ })
65
+ })
66
+ }
67
+
68
+ function parseCookies (cookieHeader) {
69
+ if (!cookieHeader) return null
70
+ const cookies = {}
71
+ cookieHeader.split(';').forEach(part => {
72
+ const [key, ...rest] = part.trim().split('=')
73
+ if (key) cookies[key.trim()] = rest.join('=').trim()
74
+ })
75
+ return Object.keys(cookies).length > 0 ? cookies : null
76
+ }
77
+
78
+ export const HttpProxyMiddleware = () => {
79
+ return async (ctx, next) => {
80
+ if (!ctx.path.startsWith('/proxy/')) {
81
+ return next()
82
+ }
83
+ // Reconstruct full URL including query string (Koa strips it from ctx.path)
84
+ const fullProxyPath = ctx.querystring
85
+ ? ctx.path + '?' + ctx.querystring
86
+ : ctx.path
87
+ const resolved = HttpProxy.parseTargetUrl(fullProxyPath)
88
+ if (!resolved) {
89
+ ctx.status = 400
90
+ ctx.body = {
91
+ success: false,
92
+ status: 400,
93
+ message: 'Invalid proxy target URL',
94
+ data: { path: ctx.path }
95
+ }
96
+ return
97
+ }
98
+ const startTime = Date.now()
99
+ const startedAt = new Date().toISOString()
100
+ const traceId = ctx.get('X-Trace-Id') || ctx.state?.traceId || null
101
+ const rawBodyBuffer = await bufferRequestBody(ctx.req)
102
+ const requestBodyStr = rawBodyBuffer ? rawBodyBuffer.toString('utf-8') : null
103
+ const requestHeaders = { ...ctx.req.headers }
104
+ const cookies = parseCookies(requestHeaders.cookie)
105
+ const contentType = requestHeaders['content-type'] || null
106
+ const targetUrl = new URL(resolved.fullUrl)
107
+ const queryParams = targetUrl.search
108
+ ? Object.fromEntries(targetUrl.searchParams)
109
+ : null
110
+ Logger.log({
111
+ level: 'info',
112
+ message: `HTTP Proxy IN: ${ctx.method} ${resolved.fullUrl}`,
113
+ data: {
114
+ method: ctx.method,
115
+ target: resolved.fullUrl,
116
+ service: resolved.serviceName
117
+ }
118
+ })
119
+ ctx.respond = false
120
+ // resolved.path already includes the query string from the target URL
121
+ ctx.req.url = resolved.path
122
+ const bodyStream = rawBodyBuffer
123
+ ? Readable.from(rawBodyBuffer)
124
+ : Readable.from(Buffer.alloc(0))
125
+ return new Promise((resolve) => {
126
+ HttpProxy.proxy.web(ctx.req, ctx.res, {
127
+ target: resolved.origin,
128
+ buffer: bodyStream
129
+ }, (err) => {
130
+ const elapsed = Date.now() - startTime
131
+ if (err) {
132
+ Logger.log({
133
+ level: 'error',
134
+ message: `HTTP Proxy ERR: ${ctx.method} ${resolved.fullUrl} → 502 (${elapsed}ms) ${err.message}`,
135
+ data: {
136
+ method: ctx.method,
137
+ target: resolved.fullUrl,
138
+ error: err.message,
139
+ responseTimeMs: elapsed
140
+ }
141
+ })
142
+ HttpProxy.logTraffic({
143
+ method: ctx.method,
144
+ targetUrl: resolved.fullUrl,
145
+ targetHost: resolved.host,
146
+ targetPath: resolved.path,
147
+ queryParams,
148
+ requestHeaders,
149
+ requestBody: requestBodyStr,
150
+ requestCookies: cookies,
151
+ contentType,
152
+ statusCode: 502,
153
+ responseHeaders: null,
154
+ responseBody: null,
155
+ responseContentType: null,
156
+ responseTimeMs: elapsed,
157
+ startedAt,
158
+ completedAt: new Date().toISOString(),
159
+ sourceService: resolved.serviceName,
160
+ error: err.message,
161
+ traceId
162
+ })
163
+ if (!ctx.res.headersSent) {
164
+ ctx.res.writeHead(502, { 'Content-Type': 'application/json' })
165
+ }
166
+ ctx.res.end(JSON.stringify({
167
+ success: false,
168
+ message: `HTTP proxy error: ${err.message}`,
169
+ target: resolved.fullUrl
170
+ }))
171
+ }
172
+ resolve()
173
+ })
174
+ HttpProxy.proxy.once('proxyRes', async (proxyRes) => {
175
+ const elapsed = Date.now() - startTime
176
+ const contentEncoding = (proxyRes.headers['content-encoding'] || '').trim().toLowerCase()
177
+ const { raw: responseBody } = await collectStreamBody(proxyRes, contentEncoding || null)
178
+ const responseHeaders = { ...proxyRes.headers }
179
+ const responseContentType = responseHeaders['content-type'] || null
180
+ Logger.log({
181
+ level: proxyRes.statusCode >= 400 ? 'warn' : 'info',
182
+ message: `HTTP Proxy OUT: ${ctx.method} ${resolved.fullUrl} → ${proxyRes.statusCode} (${elapsed}ms)`,
183
+ data: {
184
+ method: ctx.method,
185
+ target: resolved.fullUrl,
186
+ statusCode: proxyRes.statusCode,
187
+ responseTimeMs: elapsed,
188
+ service: resolved.serviceName
189
+ }
190
+ })
191
+ HttpProxy.logTraffic({
192
+ method: ctx.method,
193
+ targetUrl: resolved.fullUrl,
194
+ targetHost: resolved.host,
195
+ targetPath: resolved.path,
196
+ queryParams,
197
+ requestHeaders,
198
+ requestBody: requestBodyStr,
199
+ requestCookies: cookies,
200
+ contentType,
201
+ statusCode: proxyRes.statusCode,
202
+ responseHeaders,
203
+ responseBody,
204
+ responseContentType,
205
+ responseTimeMs: elapsed,
206
+ startedAt,
207
+ completedAt: new Date().toISOString(),
208
+ sourceService: resolved.serviceName,
209
+ error: null,
210
+ traceId
211
+ })
212
+ })
213
+ })
214
+ }
215
+ }