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