@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.
Files changed (49) hide show
  1. package/README.md +15 -15
  2. package/dist/bridge/bridge-service.d.ts +1 -1
  3. package/dist/bridge/bridge-service.js +7 -7
  4. package/dist/bridge/control.js +1 -1
  5. package/dist/bridge/export-store.d.ts +35 -0
  6. package/dist/bridge/export-store.js +195 -0
  7. package/dist/bridge/extension-session.d.ts +9 -2
  8. package/dist/bridge/extension-session.js +56 -8
  9. package/dist/cli.js +563 -148
  10. package/dist/commands.d.ts +8 -0
  11. package/dist/commands.js +276 -40
  12. package/dist/constants.d.ts +2 -2
  13. package/dist/constants.js +4 -2
  14. package/dist/http-client.d.ts +24 -0
  15. package/dist/http-client.js +101 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +2 -2
  18. package/dist/mcp.d.ts +4 -3
  19. package/dist/mcp.js +221 -134
  20. package/dist/protocol/schema.d.ts +503 -34
  21. package/dist/protocol/schema.js +396 -15
  22. package/dist/server.d.ts +7 -4
  23. package/dist/server.js +70 -26
  24. package/dist/update-notifier-worker.d.ts +2 -0
  25. package/dist/update-notifier-worker.js +3 -0
  26. package/dist/update-notifier.d.ts +9 -0
  27. package/dist/update-notifier.js +178 -0
  28. package/dist/workflow-watch.d.ts +8 -7
  29. package/dist/workflow-watch.js +39 -29
  30. package/package.json +16 -6
  31. package/docs/cli.md +0 -112
  32. package/docs/http-api.md +0 -149
  33. package/docs/mcp.md +0 -90
  34. package/docs/operations.md +0 -85
  35. package/docs/workflow-watch.md +0 -86
  36. package/src/bridge/bridge-service.ts +0 -291
  37. package/src/bridge/control.ts +0 -281
  38. package/src/bridge/daemon.ts +0 -102
  39. package/src/bridge/extension-session.ts +0 -286
  40. package/src/bridge/pairing-state.ts +0 -92
  41. package/src/cli.ts +0 -1019
  42. package/src/commands.ts +0 -158
  43. package/src/constants.ts +0 -10
  44. package/src/errors.ts +0 -72
  45. package/src/index.ts +0 -35
  46. package/src/mcp.ts +0 -386
  47. package/src/protocol/schema.ts +0 -202
  48. package/src/server.ts +0 -623
  49. package/src/workflow-watch.ts +0 -364
@@ -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
- }