@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,582 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import {
|
|
4
|
+
BridgeScanResultRecord,
|
|
5
|
+
BridgeScanStatusNotification,
|
|
6
|
+
DcpV2DeviceNotificationPacket,
|
|
7
|
+
DcpV2DeviceResponseForServerPacket,
|
|
8
|
+
DcpV2OnlineServerRequestPacket,
|
|
9
|
+
ErrorCode,
|
|
10
|
+
ForwardPacketRequest,
|
|
11
|
+
ForwardPacketResponse,
|
|
12
|
+
ForwardPacketV2Request,
|
|
13
|
+
ForwardPacketV2Response,
|
|
14
|
+
Interface,
|
|
15
|
+
NotificationType,
|
|
16
|
+
Operation,
|
|
17
|
+
Protocol
|
|
18
|
+
} from '@portalteam/protocols'
|
|
19
|
+
import { Application } from '../configs/Application.js'
|
|
20
|
+
import { Logger } from '../singletons/Logger.js'
|
|
21
|
+
import { Broker } from '../protocols/mqtt/Broker.js'
|
|
22
|
+
import { PublishMessages } from '../emulation/pubsub/Logic.js'
|
|
23
|
+
import { GatewayState } from '../enums/GatewayState.js'
|
|
24
|
+
|
|
25
|
+
class AppGatewayServiceClass {
|
|
26
|
+
constructor () {
|
|
27
|
+
this.state = GatewayState.STOPPED
|
|
28
|
+
this.gatewayDevice = null
|
|
29
|
+
this.masterSecret = null
|
|
30
|
+
this.scanInterval = null
|
|
31
|
+
this.startedAt = null
|
|
32
|
+
this.retryTimer = null
|
|
33
|
+
this.dependencyHealth = {
|
|
34
|
+
deviceNative: { reachable: false, lastChecked: null, error: null },
|
|
35
|
+
deviceSimulator: { reachable: false, lastChecked: null, error: null }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get isRunning () {
|
|
40
|
+
return this.state === GatewayState.RUNNING
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Auto-start and dependency health ---
|
|
44
|
+
|
|
45
|
+
async autoStart () {
|
|
46
|
+
const { autoStart } = Application.appGateway
|
|
47
|
+
if (!autoStart) {
|
|
48
|
+
Logger.log({
|
|
49
|
+
level: 'info',
|
|
50
|
+
message: 'Gateway auto-start is disabled (APP_GATEWAY_AUTO_START=false)'
|
|
51
|
+
})
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
Logger.log({
|
|
55
|
+
level: 'info',
|
|
56
|
+
message: 'Gateway auto-start initiated'
|
|
57
|
+
})
|
|
58
|
+
const health = await this.checkDependencyHealth()
|
|
59
|
+
if (health.allHealthy) {
|
|
60
|
+
const result = await this.start()
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
Logger.log({
|
|
63
|
+
level: 'warn',
|
|
64
|
+
message: 'Gateway immediate start failed, entering retry loop',
|
|
65
|
+
data: { error: result.message }
|
|
66
|
+
})
|
|
67
|
+
this.startRetryLoop()
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
this.startRetryLoop()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async checkDependencyHealth () {
|
|
75
|
+
const { deviceNativeUrl, deviceSimulatorUrl } = Application.appGateway
|
|
76
|
+
const check = async (url, name) => {
|
|
77
|
+
try {
|
|
78
|
+
const controller = new AbortController()
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), 3000)
|
|
80
|
+
const response = await fetch(`${url}/v1/health/liveness`, {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
signal: controller.signal
|
|
83
|
+
})
|
|
84
|
+
clearTimeout(timeout)
|
|
85
|
+
const reachable = response.ok
|
|
86
|
+
this.dependencyHealth[name] = {
|
|
87
|
+
reachable,
|
|
88
|
+
lastChecked: Date.now(),
|
|
89
|
+
error: reachable ? null : `HTTP ${response.status}`
|
|
90
|
+
}
|
|
91
|
+
return reachable
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.dependencyHealth[name] = {
|
|
94
|
+
reachable: false,
|
|
95
|
+
lastChecked: Date.now(),
|
|
96
|
+
error: error.name === 'AbortError' ? 'Timeout' : error.message
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const [nativeOk, simulatorOk] = await Promise.all([
|
|
102
|
+
check(deviceNativeUrl, 'deviceNative'),
|
|
103
|
+
check(deviceSimulatorUrl, 'deviceSimulator')
|
|
104
|
+
])
|
|
105
|
+
return { allHealthy: nativeOk && simulatorOk, deviceNative: nativeOk, deviceSimulator: simulatorOk }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
startRetryLoop () {
|
|
109
|
+
if (this.retryTimer) return
|
|
110
|
+
const { retryIntervalSeconds } = Application.appGateway
|
|
111
|
+
this.state = GatewayState.WAITING_FOR_DEPENDENCIES
|
|
112
|
+
Logger.log({
|
|
113
|
+
level: 'info',
|
|
114
|
+
message: 'Gateway waiting for dependencies',
|
|
115
|
+
data: {
|
|
116
|
+
retryIntervalSeconds,
|
|
117
|
+
deviceNativeUrl: Application.appGateway.deviceNativeUrl,
|
|
118
|
+
deviceSimulatorUrl: Application.appGateway.deviceSimulatorUrl
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
this.retryTimer = setInterval(async () => {
|
|
122
|
+
const health = await this.checkDependencyHealth()
|
|
123
|
+
if (health.allHealthy) {
|
|
124
|
+
Logger.log({
|
|
125
|
+
level: 'info',
|
|
126
|
+
message: 'All dependencies available, starting gateway'
|
|
127
|
+
})
|
|
128
|
+
this.stopRetryLoop()
|
|
129
|
+
const result = await this.start()
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
Logger.log({
|
|
132
|
+
level: 'warn',
|
|
133
|
+
message: 'Gateway start failed after dependencies ready, will retry',
|
|
134
|
+
data: { error: result.message }
|
|
135
|
+
})
|
|
136
|
+
this.startRetryLoop()
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
Logger.log({
|
|
140
|
+
level: 'debug',
|
|
141
|
+
message: 'Dependencies not ready yet',
|
|
142
|
+
data: { deviceNative: health.deviceNative, deviceSimulator: health.deviceSimulator }
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}, retryIntervalSeconds * 1000)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stopRetryLoop () {
|
|
149
|
+
if (this.retryTimer) {
|
|
150
|
+
clearInterval(this.retryTimer)
|
|
151
|
+
this.retryTimer = null
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Lifecycle methods ---
|
|
156
|
+
|
|
157
|
+
async start () {
|
|
158
|
+
if (this.state === GatewayState.RUNNING) {
|
|
159
|
+
Logger.log({
|
|
160
|
+
level: 'warn',
|
|
161
|
+
message: 'App Gateway is already running'
|
|
162
|
+
})
|
|
163
|
+
return { success: false, message: 'App Gateway is already running' }
|
|
164
|
+
}
|
|
165
|
+
this.stopRetryLoop()
|
|
166
|
+
this.state = GatewayState.STARTING
|
|
167
|
+
const { propertyId, deviceNativeUrl, deviceSimulatorUrl } = Application.appGateway
|
|
168
|
+
try {
|
|
169
|
+
Logger.log({
|
|
170
|
+
level: 'info',
|
|
171
|
+
message: 'Starting App Gateway service...',
|
|
172
|
+
data: { propertyId, deviceNativeUrl, deviceSimulatorUrl }
|
|
173
|
+
})
|
|
174
|
+
await this.ensureGatewayExists()
|
|
175
|
+
this.subscribeToRequestTopic()
|
|
176
|
+
this.startScanInterval()
|
|
177
|
+
this.state = GatewayState.RUNNING
|
|
178
|
+
this.startedAt = Date.now()
|
|
179
|
+
Logger.log({
|
|
180
|
+
level: 'info',
|
|
181
|
+
message: 'App Gateway service started successfully',
|
|
182
|
+
data: {
|
|
183
|
+
gatewayId: this.gatewayDevice?.id,
|
|
184
|
+
macAddress: this.gatewayDevice?.macAddress
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
message: 'App Gateway started',
|
|
190
|
+
gateway: this.gatewayDevice
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.state = GatewayState.ERROR
|
|
194
|
+
Logger.log({
|
|
195
|
+
level: 'error',
|
|
196
|
+
message: 'Failed to start App Gateway service',
|
|
197
|
+
data: { error: error.message, stack: error.stack }
|
|
198
|
+
})
|
|
199
|
+
return { success: false, message: error.message }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async stop () {
|
|
204
|
+
if (this.state === GatewayState.STOPPED) {
|
|
205
|
+
return { success: false, message: 'App Gateway is not running' }
|
|
206
|
+
}
|
|
207
|
+
Logger.log({
|
|
208
|
+
level: 'info',
|
|
209
|
+
message: 'Stopping App Gateway service...'
|
|
210
|
+
})
|
|
211
|
+
this.stopRetryLoop()
|
|
212
|
+
if (this.scanInterval) {
|
|
213
|
+
clearInterval(this.scanInterval)
|
|
214
|
+
this.scanInterval = null
|
|
215
|
+
}
|
|
216
|
+
this.state = GatewayState.STOPPED
|
|
217
|
+
this.startedAt = null
|
|
218
|
+
Logger.log({
|
|
219
|
+
level: 'info',
|
|
220
|
+
message: 'App Gateway service stopped'
|
|
221
|
+
})
|
|
222
|
+
return { success: true, message: 'App Gateway stopped' }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getStatus () {
|
|
226
|
+
const { propertyId, deviceNativeUrl, deviceSimulatorUrl, scanIntervalSeconds, autoStart } = Application.appGateway
|
|
227
|
+
return {
|
|
228
|
+
state: this.state,
|
|
229
|
+
isRunning: this.isRunning,
|
|
230
|
+
startedAt: this.startedAt,
|
|
231
|
+
uptime: this.startedAt ? Date.now() - this.startedAt : null,
|
|
232
|
+
gateway: this.gatewayDevice
|
|
233
|
+
? {
|
|
234
|
+
id: this.gatewayDevice.id,
|
|
235
|
+
macAddress: this.gatewayDevice.macAddress,
|
|
236
|
+
propertyId: this.gatewayDevice.propertyId
|
|
237
|
+
}
|
|
238
|
+
: null,
|
|
239
|
+
dependencies: this.dependencyHealth,
|
|
240
|
+
config: {
|
|
241
|
+
propertyId,
|
|
242
|
+
deviceNativeUrl,
|
|
243
|
+
deviceSimulatorUrl,
|
|
244
|
+
scanIntervalSeconds,
|
|
245
|
+
autoStart
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async triggerScan () {
|
|
251
|
+
if (!this.isRunning) {
|
|
252
|
+
return { success: false, message: 'App Gateway is not running' }
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await this.publishScanNotification()
|
|
256
|
+
return { success: true, message: 'Scan notification published' }
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return { success: false, message: error.message }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Gateway registration ---
|
|
263
|
+
|
|
264
|
+
async ensureGatewayExists () {
|
|
265
|
+
const { propertyId, deviceNativeUrl } = Application.appGateway
|
|
266
|
+
const macAddress = this.generateMacAddress()
|
|
267
|
+
const masterSecret = crypto.randomBytes(16).toString('base64')
|
|
268
|
+
try {
|
|
269
|
+
const createResponse = await fetch(`${deviceNativeUrl}/v1/devices/createAppGateway`, {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers: { 'Content-Type': 'application/json' },
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
propertyId,
|
|
274
|
+
macAddress,
|
|
275
|
+
masterSecret,
|
|
276
|
+
supportedProtocols: [Protocol.dcpV2],
|
|
277
|
+
supportedOperations: [Operation.forwardPacket, Operation.forwardPacketV2],
|
|
278
|
+
supportedInterfaces: [Interface.ble],
|
|
279
|
+
supportedNotifications: [NotificationType.bridgeScanStatus]
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
const createResult = await createResponse.json()
|
|
283
|
+
if (createResult.success && createResult.data?.device) {
|
|
284
|
+
this.gatewayDevice = createResult.data.device
|
|
285
|
+
this.masterSecret = masterSecret
|
|
286
|
+
Logger.log({
|
|
287
|
+
level: 'info',
|
|
288
|
+
message: 'Created app gateway',
|
|
289
|
+
data: { gatewayId: this.gatewayDevice.id, macAddress: this.gatewayDevice.macAddress }
|
|
290
|
+
})
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
throw new Error(createResult.message || 'Failed to create app gateway')
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error.message.includes('Failed to create')) {
|
|
296
|
+
throw error
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Failed to connect to device-native at ${deviceNativeUrl}: ${error.message}`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
generateMacAddress () {
|
|
303
|
+
const bytes = crypto.randomBytes(6)
|
|
304
|
+
bytes[0] = (bytes[0] | 0x02) & 0xfe
|
|
305
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- MQTT subscription and message handling ---
|
|
309
|
+
|
|
310
|
+
subscribeToRequestTopic () {
|
|
311
|
+
if (!this.gatewayDevice) {
|
|
312
|
+
throw new Error('Gateway device not initialized')
|
|
313
|
+
}
|
|
314
|
+
const broker = Broker.getInstance()
|
|
315
|
+
const requestTopic = `rq/4/${this.gatewayDevice.id}`
|
|
316
|
+
broker.subscribe(
|
|
317
|
+
requestTopic,
|
|
318
|
+
(packet, cb) => {
|
|
319
|
+
this.handleRequest(packet.topic, packet.payload).catch(error => {
|
|
320
|
+
Logger.log({
|
|
321
|
+
level: 'error',
|
|
322
|
+
message: 'Error handling gateway request',
|
|
323
|
+
data: { error: error.message }
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
if (cb) cb()
|
|
327
|
+
},
|
|
328
|
+
() => {
|
|
329
|
+
Logger.log({
|
|
330
|
+
level: 'info',
|
|
331
|
+
message: `Subscribed to request topic: ${requestTopic}`
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --- Scan notifications ---
|
|
338
|
+
|
|
339
|
+
startScanInterval () {
|
|
340
|
+
const { scanIntervalSeconds } = Application.appGateway
|
|
341
|
+
this.publishScanNotification().catch(error => {
|
|
342
|
+
Logger.log({
|
|
343
|
+
level: 'error',
|
|
344
|
+
message: 'Error publishing initial scan notification',
|
|
345
|
+
data: { error: error.message }
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
this.scanInterval = setInterval(async () => {
|
|
349
|
+
try {
|
|
350
|
+
await this.publishScanNotification()
|
|
351
|
+
} catch (error) {
|
|
352
|
+
Logger.log({
|
|
353
|
+
level: 'error',
|
|
354
|
+
message: 'Error publishing scan notification',
|
|
355
|
+
data: { error: error.message }
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
}, scanIntervalSeconds * 1000)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async publishScanNotification () {
|
|
362
|
+
if (!this.gatewayDevice || !this.masterSecret) {
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
const { propertyId, deviceSimulatorUrl } = Application.appGateway
|
|
366
|
+
let scanResults = []
|
|
367
|
+
try {
|
|
368
|
+
const response = await fetch(`${deviceSimulatorUrl}/v1/devices/scanProperty`, {
|
|
369
|
+
method: 'POST',
|
|
370
|
+
headers: { 'Content-Type': 'application/json' },
|
|
371
|
+
body: JSON.stringify({ propertyId })
|
|
372
|
+
})
|
|
373
|
+
const result = await response.json()
|
|
374
|
+
if (result.success && result.data?.scanResults) {
|
|
375
|
+
scanResults = result.data.scanResults
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
Logger.log({
|
|
379
|
+
level: 'debug',
|
|
380
|
+
message: 'Failed to scan property',
|
|
381
|
+
data: { error: error.message }
|
|
382
|
+
})
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if (!scanResults.length) {
|
|
386
|
+
Logger.log({
|
|
387
|
+
level: 'debug',
|
|
388
|
+
message: 'No devices found during scan'
|
|
389
|
+
})
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
const notification = new BridgeScanStatusNotification({
|
|
393
|
+
scanResultRecords: scanResults.map(({ rssi, macAddress, bcpPacket }) => {
|
|
394
|
+
const bcpPacketBuffer = Buffer.from(bcpPacket, 'base64')
|
|
395
|
+
return new BridgeScanResultRecord({
|
|
396
|
+
macAddress,
|
|
397
|
+
rssi: rssi || -50,
|
|
398
|
+
bcpPacket: bcpPacketBuffer,
|
|
399
|
+
timeSinceScannedSeconds: 10
|
|
400
|
+
})
|
|
401
|
+
}),
|
|
402
|
+
recordCount: scanResults.length,
|
|
403
|
+
rssi: -50
|
|
404
|
+
})
|
|
405
|
+
const transport = new DcpV2DeviceNotificationPacket({
|
|
406
|
+
notificationPayload: notification.serialize(),
|
|
407
|
+
notificationType: NotificationType.bridgeScanStatus
|
|
408
|
+
})
|
|
409
|
+
const key = Buffer.from(this.masterSecret, 'base64')
|
|
410
|
+
const encryptedPacket = transport.serialize({ key })
|
|
411
|
+
const notificationTopic = `nf/4/${this.gatewayDevice.id}`
|
|
412
|
+
const broker = Broker.getInstance()
|
|
413
|
+
broker.publish({
|
|
414
|
+
topic: notificationTopic,
|
|
415
|
+
payload: encryptedPacket,
|
|
416
|
+
qos: 1,
|
|
417
|
+
retain: true
|
|
418
|
+
})
|
|
419
|
+
await this.forwardToPubSub(notificationTopic, encryptedPacket)
|
|
420
|
+
Logger.log({
|
|
421
|
+
level: 'debug',
|
|
422
|
+
message: 'Published scan notification',
|
|
423
|
+
data: { deviceCount: scanResults.length, topic: notificationTopic }
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// --- Request handling ---
|
|
428
|
+
|
|
429
|
+
async handleRequest (topic, payload) {
|
|
430
|
+
if (!this.gatewayDevice || !this.masterSecret) {
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
const { propertyId, deviceSimulatorUrl } = Application.appGateway
|
|
434
|
+
const key = Buffer.from(this.masterSecret, 'base64')
|
|
435
|
+
const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
|
|
436
|
+
let operationId, operationPayload, requestId
|
|
437
|
+
try {
|
|
438
|
+
const deserialized = DcpV2OnlineServerRequestPacket.deserialize({ buffer, keys: [key] })
|
|
439
|
+
operationId = deserialized.operationId
|
|
440
|
+
operationPayload = deserialized.operationPayload
|
|
441
|
+
requestId = deserialized.requestId
|
|
442
|
+
} catch (error) {
|
|
443
|
+
Logger.log({
|
|
444
|
+
level: 'error',
|
|
445
|
+
message: 'Failed to deserialize request packet',
|
|
446
|
+
data: { error: error.message }
|
|
447
|
+
})
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
const responseTopic = `rs/4/${this.gatewayDevice.id}`
|
|
451
|
+
let transportPacketRaw
|
|
452
|
+
if (operationId === Operation.forwardPacket) {
|
|
453
|
+
const { destinationDeviceMacAddress, packet } = ForwardPacketRequest.deserialize({ buffer: operationPayload })
|
|
454
|
+
try {
|
|
455
|
+
const response = await fetch(`${deviceSimulatorUrl}/v1/devices/processRequest`, {
|
|
456
|
+
method: 'POST',
|
|
457
|
+
headers: { 'Content-Type': 'application/json' },
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
propertyId,
|
|
460
|
+
macAddress: destinationDeviceMacAddress,
|
|
461
|
+
requestPacket: packet.toString('base64')
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
const result = await response.json()
|
|
465
|
+
if (result.success && result.data?.responsePacket) {
|
|
466
|
+
const forwardPacketResponse = new ForwardPacketResponse({
|
|
467
|
+
packet: Buffer.from(result.data.responsePacket, 'base64')
|
|
468
|
+
})
|
|
469
|
+
transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
|
|
470
|
+
requestId,
|
|
471
|
+
responsePayload: forwardPacketResponse.serialize()
|
|
472
|
+
})
|
|
473
|
+
} else {
|
|
474
|
+
transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
|
|
475
|
+
requestId,
|
|
476
|
+
errorCode: ErrorCode.cannotFindDevice
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
Logger.log({
|
|
481
|
+
level: 'error',
|
|
482
|
+
message: 'Failed to process forward packet request',
|
|
483
|
+
data: { error: error.message, macAddress: destinationDeviceMacAddress }
|
|
484
|
+
})
|
|
485
|
+
transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
|
|
486
|
+
requestId,
|
|
487
|
+
errorCode: ErrorCode.cannotFindDevice
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
} else if (operationId === Operation.forwardPacketV2) {
|
|
491
|
+
const { destinationDeviceMacAddress, packet } = ForwardPacketV2Request.deserialize({ buffer: operationPayload })
|
|
492
|
+
try {
|
|
493
|
+
const response = await fetch(`${deviceSimulatorUrl}/v1/devices/processRequest`, {
|
|
494
|
+
method: 'POST',
|
|
495
|
+
headers: { 'Content-Type': 'application/json' },
|
|
496
|
+
body: JSON.stringify({
|
|
497
|
+
propertyId,
|
|
498
|
+
macAddress: destinationDeviceMacAddress,
|
|
499
|
+
requestPacket: packet.toString('base64')
|
|
500
|
+
})
|
|
501
|
+
})
|
|
502
|
+
const result = await response.json()
|
|
503
|
+
if (result.success && result.data?.responsePacket) {
|
|
504
|
+
const forwardPacketResponse = new ForwardPacketV2Response({
|
|
505
|
+
packet: Buffer.from(result.data.responsePacket, 'base64'),
|
|
506
|
+
remainingAttemptCount: 2
|
|
507
|
+
})
|
|
508
|
+
transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
|
|
509
|
+
requestId,
|
|
510
|
+
responsePayload: forwardPacketResponse.serialize()
|
|
511
|
+
})
|
|
512
|
+
} else {
|
|
513
|
+
transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
|
|
514
|
+
requestId,
|
|
515
|
+
errorCode: ErrorCode.cannotFindDevice
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
Logger.log({
|
|
520
|
+
level: 'error',
|
|
521
|
+
message: 'Failed to process forward packet v2 request',
|
|
522
|
+
data: { error: error.message, macAddress: destinationDeviceMacAddress }
|
|
523
|
+
})
|
|
524
|
+
transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
|
|
525
|
+
requestId,
|
|
526
|
+
errorCode: ErrorCode.cannotFindDevice
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
Logger.log({
|
|
531
|
+
level: 'warn',
|
|
532
|
+
message: 'Unhandled operation type',
|
|
533
|
+
data: { operationId }
|
|
534
|
+
})
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
const encryptedResponse = transportPacketRaw.serializeWithAes128Gcm({ key })
|
|
538
|
+
const broker = Broker.getInstance()
|
|
539
|
+
broker.publish({
|
|
540
|
+
topic: responseTopic,
|
|
541
|
+
payload: encryptedResponse,
|
|
542
|
+
qos: 1,
|
|
543
|
+
retain: true
|
|
544
|
+
})
|
|
545
|
+
await this.forwardToPubSub(responseTopic, encryptedResponse)
|
|
546
|
+
Logger.log({
|
|
547
|
+
level: 'debug',
|
|
548
|
+
message: 'Published response',
|
|
549
|
+
data: { topic: responseTopic, operationId }
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- Pub/Sub forwarding ---
|
|
554
|
+
|
|
555
|
+
async forwardToPubSub (topicName, payload) {
|
|
556
|
+
const { pubsub } = Application
|
|
557
|
+
const messageData = {
|
|
558
|
+
clientId: this.gatewayDevice?.id || 'app-gateway',
|
|
559
|
+
topicName,
|
|
560
|
+
packet: payload.toString('base64'),
|
|
561
|
+
receivedAtMilliseconds: Date.now()
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
PublishMessages({
|
|
565
|
+
projectId: pubsub.projectId,
|
|
566
|
+
topicId: 'mqttMessageReceived',
|
|
567
|
+
messages: [{
|
|
568
|
+
data: Buffer.from(JSON.stringify(messageData)).toString('base64'),
|
|
569
|
+
attributes: { traceId: uuid() }
|
|
570
|
+
}]
|
|
571
|
+
})
|
|
572
|
+
} catch (error) {
|
|
573
|
+
Logger.log({
|
|
574
|
+
level: 'error',
|
|
575
|
+
message: 'Failed to forward to Pub/Sub',
|
|
576
|
+
data: { error: error.message, topicName }
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export const AppGatewayService = new AppGatewayServiceClass()
|