@tapicker/bridge 1.0.0 → 1.1.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 +15 -15
- package/dist/bridge/bridge-service.d.ts +1 -1
- package/dist/bridge/bridge-service.js +7 -7
- package/dist/bridge/control.js +1 -1
- package/dist/bridge/export-store.d.ts +35 -0
- package/dist/bridge/export-store.js +195 -0
- package/dist/bridge/extension-session.d.ts +9 -2
- package/dist/bridge/extension-session.js +56 -8
- package/dist/cli.js +563 -148
- package/dist/commands.d.ts +8 -0
- package/dist/commands.js +276 -40
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +4 -2
- package/dist/http-client.d.ts +24 -0
- package/dist/http-client.js +101 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +4 -3
- package/dist/mcp.js +221 -134
- package/dist/protocol/schema.d.ts +503 -34
- package/dist/protocol/schema.js +396 -15
- package/dist/server.d.ts +7 -4
- package/dist/server.js +70 -26
- package/dist/update-notifier-worker.d.ts +2 -0
- package/dist/update-notifier-worker.js +3 -0
- package/dist/update-notifier.d.ts +9 -0
- package/dist/update-notifier.js +178 -0
- package/dist/workflow-watch.d.ts +8 -7
- package/dist/workflow-watch.js +39 -29
- package/package.json +16 -6
- package/docs/cli.md +0 -112
- package/docs/http-api.md +0 -149
- package/docs/mcp.md +0 -90
- package/docs/operations.md +0 -85
- package/docs/workflow-watch.md +0 -86
- package/src/bridge/bridge-service.ts +0 -291
- package/src/bridge/control.ts +0 -281
- package/src/bridge/daemon.ts +0 -102
- package/src/bridge/extension-session.ts +0 -286
- package/src/bridge/pairing-state.ts +0 -92
- package/src/cli.ts +0 -1019
- package/src/commands.ts +0 -158
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -72
- package/src/index.ts +0 -35
- package/src/mcp.ts +0 -386
- package/src/protocol/schema.ts +0 -202
- package/src/server.ts +0 -623
- package/src/workflow-watch.ts +0 -364
package/src/workflow-watch.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { WebSocket } from 'ws'
|
|
3
|
-
import { DEFAULT_WORKFLOW_WATCH_WS_PATH } from './constants.js'
|
|
4
|
-
import { BridgeError } from './errors.js'
|
|
5
|
-
import { SafeResultSchema, WorkflowStatusEventSchema, createEnvelope, parseEnvelope } from './protocol/schema.js'
|
|
6
|
-
import type { WorkflowStatusEvent } from './protocol/schema.js'
|
|
7
|
-
|
|
8
|
-
export type WorkflowWatchInput = {
|
|
9
|
-
clientId?: string
|
|
10
|
-
workflowId: string
|
|
11
|
-
timeoutMs?: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type WorkflowWatchResult = WorkflowStatusEvent & {
|
|
15
|
-
timedOut: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type WorkflowWatchConnection = {
|
|
19
|
-
clientId: string
|
|
20
|
-
workflowId: string
|
|
21
|
-
onStatus: (listener: (event: WorkflowStatusEvent) => void) => () => void
|
|
22
|
-
onClose: (listener: (error: BridgeError) => void) => () => void
|
|
23
|
-
close: () => void
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type WaitForWorkflowStatusOptions = {
|
|
27
|
-
clientId: string
|
|
28
|
-
workflowId: string
|
|
29
|
-
subscribe: (onStatus: (event: WorkflowStatusEvent) => void, onClose: (error: BridgeError) => void) => () => void
|
|
30
|
-
readStatus: () => Promise<WorkflowStatusEvent>
|
|
31
|
-
onStatus?: (event: WorkflowStatusEvent) => void
|
|
32
|
-
timeoutMs?: number
|
|
33
|
-
signal?: AbortSignal
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const terminalStates = new Set(['completed', 'stopped', 'failed'])
|
|
37
|
-
|
|
38
|
-
export function workflowWatchWebSocketUrl(bridgeUrl: URL) {
|
|
39
|
-
const url = new URL(DEFAULT_WORKFLOW_WATCH_WS_PATH, bridgeUrl)
|
|
40
|
-
url.protocol = bridgeUrl.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
41
|
-
return url
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function connectWorkflowWatch(
|
|
45
|
-
url: string | URL,
|
|
46
|
-
input: Omit<WorkflowWatchInput, 'timeoutMs'>,
|
|
47
|
-
token?: string,
|
|
48
|
-
signal?: AbortSignal,
|
|
49
|
-
): Promise<WorkflowWatchConnection> {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
if (signal?.aborted) {
|
|
52
|
-
reject(new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 }))
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const headers = token ? { Authorization: `Bearer ${token}` } : undefined
|
|
57
|
-
const socket = new WebSocket(url, headers ? { headers } : undefined)
|
|
58
|
-
const requestId = randomUUID()
|
|
59
|
-
const statusListeners = new Set<(event: WorkflowStatusEvent) => void>()
|
|
60
|
-
const closeListeners = new Set<(error: BridgeError) => void>()
|
|
61
|
-
const pendingStatusEvents: WorkflowStatusEvent[] = []
|
|
62
|
-
let acceptedClientId: string | undefined
|
|
63
|
-
let settled = false
|
|
64
|
-
let intentionalClose = false
|
|
65
|
-
let closedError: BridgeError | undefined
|
|
66
|
-
let abortListener: (() => void) | undefined
|
|
67
|
-
|
|
68
|
-
const cleanupAbortListener = () => {
|
|
69
|
-
if (abortListener) signal?.removeEventListener('abort', abortListener)
|
|
70
|
-
abortListener = undefined
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const failBeforeAccept = (error: BridgeError) => {
|
|
74
|
-
if (settled) return
|
|
75
|
-
settled = true
|
|
76
|
-
cleanupAbortListener()
|
|
77
|
-
socket.terminate()
|
|
78
|
-
reject(error)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const notifyClose = (error: BridgeError) => {
|
|
82
|
-
closedError = error
|
|
83
|
-
cleanupAbortListener()
|
|
84
|
-
for (const listener of closeListeners) {
|
|
85
|
-
try {
|
|
86
|
-
listener(error)
|
|
87
|
-
} catch {
|
|
88
|
-
// A consumer callback must not escape the WebSocket event handler.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
closeListeners.clear()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const close = (reason?: BridgeError) => {
|
|
95
|
-
intentionalClose = true
|
|
96
|
-
cleanupAbortListener()
|
|
97
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
98
|
-
socket.close(1000, 'Workflow watch closed')
|
|
99
|
-
} else if (socket.readyState === WebSocket.CONNECTING) {
|
|
100
|
-
socket.terminate()
|
|
101
|
-
}
|
|
102
|
-
if (acceptedClientId) {
|
|
103
|
-
notifyClose(
|
|
104
|
-
reason ?? new BridgeError('WorkflowWatchClosed', 'Workflow watch was closed by the client', { status: 499 }),
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
abortListener = () => {
|
|
110
|
-
const error = new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 })
|
|
111
|
-
if (!acceptedClientId) {
|
|
112
|
-
failBeforeAccept(error)
|
|
113
|
-
} else {
|
|
114
|
-
close(error)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
signal?.addEventListener('abort', abortListener, { once: true })
|
|
118
|
-
|
|
119
|
-
socket.once('open', () => {
|
|
120
|
-
socket.send(
|
|
121
|
-
JSON.stringify(
|
|
122
|
-
createEnvelope(
|
|
123
|
-
'workflow.watch',
|
|
124
|
-
{
|
|
125
|
-
clientId: input.clientId,
|
|
126
|
-
input: { workflowId: input.workflowId },
|
|
127
|
-
},
|
|
128
|
-
{ id: requestId },
|
|
129
|
-
),
|
|
130
|
-
),
|
|
131
|
-
)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
socket.on('message', (raw) => {
|
|
135
|
-
const parsedEnvelope = parseEnvelope(raw)
|
|
136
|
-
if (!parsedEnvelope.success) {
|
|
137
|
-
const error = new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid Workflow watch message', {
|
|
138
|
-
status: 502,
|
|
139
|
-
cause: parsedEnvelope.error,
|
|
140
|
-
})
|
|
141
|
-
if (!acceptedClientId) failBeforeAccept(error)
|
|
142
|
-
else {
|
|
143
|
-
socket.close(1007, 'Invalid Workflow watch message')
|
|
144
|
-
notifyClose(error)
|
|
145
|
-
}
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const message = parsedEnvelope.data
|
|
150
|
-
if (message.type === 'workflow.watch:res' && message.id === requestId && !acceptedClientId) {
|
|
151
|
-
const result = SafeResultSchema.safeParse(message.data)
|
|
152
|
-
if (!result.success) {
|
|
153
|
-
failBeforeAccept(
|
|
154
|
-
new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid Workflow watch response', {
|
|
155
|
-
status: 502,
|
|
156
|
-
}),
|
|
157
|
-
)
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
if (!result.data.ok) {
|
|
161
|
-
failBeforeAccept(
|
|
162
|
-
new BridgeError(result.data.error.code, result.data.error.message, {
|
|
163
|
-
status: 409,
|
|
164
|
-
details: result.data.error.details,
|
|
165
|
-
}),
|
|
166
|
-
)
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const accepted = WorkflowStatusEventSchema.pick({ clientId: true, workflowId: true }).safeParse(
|
|
171
|
-
result.data.data,
|
|
172
|
-
)
|
|
173
|
-
if (!accepted.success || accepted.data.workflowId !== input.workflowId) {
|
|
174
|
-
failBeforeAccept(
|
|
175
|
-
new BridgeError('InvalidBridgeResponse', 'Bridge accepted a different Workflow watch target', {
|
|
176
|
-
status: 502,
|
|
177
|
-
}),
|
|
178
|
-
)
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
acceptedClientId = accepted.data.clientId
|
|
183
|
-
settled = true
|
|
184
|
-
const connection: WorkflowWatchConnection = {
|
|
185
|
-
clientId: accepted.data.clientId,
|
|
186
|
-
workflowId: accepted.data.workflowId,
|
|
187
|
-
onStatus(listener) {
|
|
188
|
-
statusListeners.add(listener)
|
|
189
|
-
for (const event of pendingStatusEvents.splice(0)) listener(event)
|
|
190
|
-
return () => statusListeners.delete(listener)
|
|
191
|
-
},
|
|
192
|
-
onClose(listener) {
|
|
193
|
-
if (closedError) {
|
|
194
|
-
listener(closedError)
|
|
195
|
-
return () => undefined
|
|
196
|
-
}
|
|
197
|
-
closeListeners.add(listener)
|
|
198
|
-
return () => closeListeners.delete(listener)
|
|
199
|
-
},
|
|
200
|
-
close,
|
|
201
|
-
}
|
|
202
|
-
resolve(connection)
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (message.type !== 'workflow.status' || !acceptedClientId) return
|
|
207
|
-
const event = WorkflowStatusEventSchema.safeParse(message.data)
|
|
208
|
-
if (!event.success) return
|
|
209
|
-
if (event.data.clientId !== acceptedClientId || event.data.workflowId !== input.workflowId) return
|
|
210
|
-
if (statusListeners.size === 0) {
|
|
211
|
-
if (pendingStatusEvents.length >= 128) pendingStatusEvents.shift()
|
|
212
|
-
pendingStatusEvents.push(event.data)
|
|
213
|
-
} else {
|
|
214
|
-
for (const listener of statusListeners) {
|
|
215
|
-
try {
|
|
216
|
-
listener(event.data)
|
|
217
|
-
} catch {
|
|
218
|
-
// A consumer callback must not escape the WebSocket event handler.
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
socket.once('unexpected-response', (_request, response) => {
|
|
225
|
-
const chunks: Buffer[] = []
|
|
226
|
-
response.on('data', (chunk: Buffer) => chunks.push(Buffer.from(chunk)))
|
|
227
|
-
response.once('end', () => {
|
|
228
|
-
const body = Buffer.concat(chunks).toString('utf8')
|
|
229
|
-
let errorCode = 'BridgeRequestFailed'
|
|
230
|
-
let errorMessage = response.statusMessage || 'Bridge rejected the Workflow watch connection'
|
|
231
|
-
let details: unknown
|
|
232
|
-
try {
|
|
233
|
-
const parsed = JSON.parse(body) as { error?: { code?: string; message?: string; details?: unknown } }
|
|
234
|
-
errorCode = parsed.error?.code || errorCode
|
|
235
|
-
errorMessage = parsed.error?.message || errorMessage
|
|
236
|
-
details = parsed.error?.details
|
|
237
|
-
} catch {
|
|
238
|
-
// Keep the HTTP status text when the response is not Bridge JSON.
|
|
239
|
-
}
|
|
240
|
-
failBeforeAccept(new BridgeError(errorCode, errorMessage, { status: response.statusCode || 503, details }))
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
socket.on('error', (cause) => {
|
|
245
|
-
const error = new BridgeError('BridgeUnavailable', 'Unable to connect to Tapicker Bridge Workflow watch', {
|
|
246
|
-
status: 503,
|
|
247
|
-
cause,
|
|
248
|
-
})
|
|
249
|
-
if (!acceptedClientId) failBeforeAccept(error)
|
|
250
|
-
else notifyClose(error)
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
socket.once('close', (code, reason) => {
|
|
254
|
-
cleanupAbortListener()
|
|
255
|
-
if (!acceptedClientId) {
|
|
256
|
-
failBeforeAccept(
|
|
257
|
-
new BridgeError('WorkflowWatchClosed', 'Bridge closed the Workflow watch connection before accepting it', {
|
|
258
|
-
status: 503,
|
|
259
|
-
details: { code, reason: reason.toString() },
|
|
260
|
-
}),
|
|
261
|
-
)
|
|
262
|
-
} else if (!intentionalClose) {
|
|
263
|
-
notifyClose(
|
|
264
|
-
new BridgeError('WorkflowWatchClosed', 'Bridge closed the Workflow watch connection', {
|
|
265
|
-
status: 503,
|
|
266
|
-
details: { code, reason: reason.toString() },
|
|
267
|
-
}),
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function waitForWorkflowStatus(options: WaitForWorkflowStatusOptions): Promise<WorkflowWatchResult> {
|
|
275
|
-
return new Promise((resolve, reject) => {
|
|
276
|
-
let settled = false
|
|
277
|
-
let latestStatus: WorkflowStatusEvent | undefined
|
|
278
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
279
|
-
let unsubscribe: () => void = () => {}
|
|
280
|
-
let abortListener: (() => void) | undefined
|
|
281
|
-
|
|
282
|
-
const cleanup = () => {
|
|
283
|
-
unsubscribe()
|
|
284
|
-
if (timer) clearTimeout(timer)
|
|
285
|
-
if (abortListener) options.signal?.removeEventListener('abort', abortListener)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const finish = (status: WorkflowStatusEvent, timedOut = false) => {
|
|
289
|
-
if (settled) return
|
|
290
|
-
settled = true
|
|
291
|
-
cleanup()
|
|
292
|
-
resolve({ ...status, timedOut })
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const fail = (error: unknown) => {
|
|
296
|
-
if (settled) return
|
|
297
|
-
settled = true
|
|
298
|
-
cleanup()
|
|
299
|
-
reject(error)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const publish = (status: WorkflowStatusEvent) => {
|
|
303
|
-
if (settled) return
|
|
304
|
-
if (status.clientId !== options.clientId || status.workflowId !== options.workflowId) return
|
|
305
|
-
if (latestStatus && sameStatus(latestStatus, status)) return
|
|
306
|
-
latestStatus = status
|
|
307
|
-
try {
|
|
308
|
-
options.onStatus?.(status)
|
|
309
|
-
} catch (error) {
|
|
310
|
-
fail(error)
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
if (terminalStates.has(status.state)) finish(status)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (options.signal?.aborted) {
|
|
317
|
-
fail(new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 }))
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
unsubscribe = options.subscribe(publish, fail)
|
|
322
|
-
if (settled) {
|
|
323
|
-
unsubscribe()
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
if (options.signal) {
|
|
327
|
-
abortListener = () =>
|
|
328
|
-
fail(new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 }))
|
|
329
|
-
options.signal.addEventListener('abort', abortListener, { once: true })
|
|
330
|
-
}
|
|
331
|
-
if (options.timeoutMs !== undefined) {
|
|
332
|
-
timer = setTimeout(() => {
|
|
333
|
-
if (latestStatus) finish(latestStatus, true)
|
|
334
|
-
else
|
|
335
|
-
fail(
|
|
336
|
-
new BridgeError('WorkflowWatchTimeout', 'Timed out before receiving Workflow status', {
|
|
337
|
-
status: 504,
|
|
338
|
-
details: { timeoutMs: options.timeoutMs },
|
|
339
|
-
}),
|
|
340
|
-
)
|
|
341
|
-
}, options.timeoutMs)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
void options
|
|
345
|
-
.readStatus()
|
|
346
|
-
.then((snapshot) => {
|
|
347
|
-
if (settled) return
|
|
348
|
-
const snapshotIsAtLeastAsNew =
|
|
349
|
-
latestStatus !== undefined && Date.parse(snapshot.updatedAt) >= Date.parse(latestStatus.updatedAt)
|
|
350
|
-
if (
|
|
351
|
-
!latestStatus ||
|
|
352
|
-
snapshotIsAtLeastAsNew ||
|
|
353
|
-
(terminalStates.has(snapshot.state) && !terminalStates.has(latestStatus.state))
|
|
354
|
-
) {
|
|
355
|
-
publish(snapshot)
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
.catch(fail)
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function sameStatus(left: WorkflowStatusEvent, right: WorkflowStatusEvent) {
|
|
363
|
-
return left.state === right.state && left.updatedAt === right.updatedAt
|
|
364
|
-
}
|