backeer-opencode 0.8.1 → 0.10.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/CHANGELOG.md +25 -0
- package/dist/backeer-permission.js +263 -303
- package/docs/live-control.md +51 -12
- package/package.json +1 -1
- package/src/delivery-tracker.js +4 -4
- package/src/diagnostics.js +3 -3
- package/src/event-publishers.js +33 -34
- package/src/feedback-session-store.js +3 -14
- package/src/http.js +14 -16
- package/src/lifecycle-controller.js +5 -5
- package/src/lifecycle-publishers.js +8 -9
- package/src/live-bridge-client.js +3 -4
- package/src/live-bridge-config.js +10 -6
- package/src/live-bridge-events.js +6 -9
- package/src/live-bridge.js +58 -77
- package/src/live-control-client.js +84 -38
- package/src/live-control-mcp.js +10 -10
- package/src/live-diagnostics.js +42 -0
- package/src/live-discovery.js +11 -3
- package/src/live-errors.js +4 -0
- package/src/live-managed-mcp.js +16 -10
- package/src/live-profiles.js +6 -6
- package/src/live-question-tools.js +5 -5
- package/src/live-questions.js +4 -5
- package/src/live-session-manager.js +101 -15
- package/src/live-session-scope.js +3 -4
- package/src/opencode-v2-endpoint.js +8 -2
- package/src/permission-controller.js +4 -4
- package/src/plugin.js +5 -1
- package/src/reporter.js +4 -8
- package/src/session-messages.js +3 -3
- package/src/sse.js +7 -7
- package/src/updates.js +1 -1
- package/src/utilities.js +3 -6
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
const ID = /^[A-Za-z0-9_-]
|
|
1
|
+
const ID = /^[A-Za-z0-9_-]+$/
|
|
2
2
|
export const validBridgeID = (value) => typeof value === "string" && ID.test(value)
|
|
3
3
|
|
|
4
4
|
export function createBridgeSanitizer(secrets = []) {
|
|
5
5
|
const known = secrets.filter((value) => typeof value === "string" && value.length > 0)
|
|
6
6
|
const assistants = new Map()
|
|
7
|
-
function
|
|
8
|
-
if (typeof value !== "string") return
|
|
9
|
-
let result = value
|
|
7
|
+
function text(value) {
|
|
8
|
+
if (typeof value !== "string") return ""
|
|
9
|
+
let result = value
|
|
10
10
|
for (const secret of known) result = result.split(secret).join("[REDACTED]")
|
|
11
11
|
result = result
|
|
12
12
|
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]")
|
|
13
13
|
.replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]")
|
|
14
14
|
.replace(/\b(?:sk|bgi)_[A-Za-z0-9_-]{12,}|\bsk-[A-Za-z0-9_-]{12,}/g, "[REDACTED]")
|
|
15
15
|
.replace(/((?:api[_-]?key|access[_-]?token|secret|password|authorization)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]")
|
|
16
|
-
return
|
|
16
|
+
return result
|
|
17
17
|
}
|
|
18
|
-
function text(value, max) { return clipped(value, max).value }
|
|
19
18
|
function content(key, value) {
|
|
20
|
-
|
|
21
|
-
return { [key]: result.value, ...(result.truncated ? { truncated: true } : {}) }
|
|
19
|
+
return { [key]: text(value) }
|
|
22
20
|
}
|
|
23
21
|
function sanitize(event) {
|
|
24
22
|
const p = event?.properties ?? event?.data ?? {}
|
|
@@ -30,7 +28,6 @@ export function createBridgeSanitizer(secrets = []) {
|
|
|
30
28
|
if (!validBridgeID(info?.id)) return undefined
|
|
31
29
|
if (info.role === "assistant") {
|
|
32
30
|
assistants.set(info.id, sessionID)
|
|
33
|
-
if (assistants.size > 256) assistants.delete(assistants.keys().next().value)
|
|
34
31
|
}
|
|
35
32
|
return info.role === "assistant" ? { ...base, data: {
|
|
36
33
|
messageID: info.id, completed: !!info.time?.completed, failed: !!info.error,
|
package/src/live-bridge.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createServer } from "node:http"
|
|
2
|
-
import { randomUUID, timingSafeEqual } from "node:crypto"
|
|
2
|
+
import { randomUUID, timingSafeEqual, createHash } from "node:crypto"
|
|
3
|
+
import { resolve } from "node:path"
|
|
3
4
|
import { createV1LiveClient } from "./live-bridge-client.js"
|
|
4
5
|
import { createLiveConnectionNotifier, LIVE_HEARTBEAT_MS, LIVE_RECONNECT_MS } from "./live-connection.js"
|
|
5
6
|
import { createBridgeSanitizer, validBridgeID } from "./live-bridge-events.js"
|
|
6
7
|
import { questionSnapshot, resolveQuestionAnswers } from "./live-questions.js"
|
|
7
8
|
import { PLUGIN_VERSION } from "./updates.js"
|
|
8
9
|
|
|
9
|
-
const LIMITS = { events: 256, bytes: 2_000_000, clients: 8, commands: 1024, commandTTL: 86_400_000, body: 32_768, requests: 16 }
|
|
10
10
|
function failure(status, code) { return Object.assign(new Error(code), { status, code }) }
|
|
11
11
|
function json(response, status, value) {
|
|
12
12
|
if (!response.destroyed && !response.headersSent) {
|
|
@@ -16,10 +16,7 @@ function json(response, status, value) {
|
|
|
16
16
|
}
|
|
17
17
|
async function readBody(request) {
|
|
18
18
|
const chunks = []
|
|
19
|
-
let bytes = 0
|
|
20
19
|
for await (const chunk of request) {
|
|
21
|
-
bytes += chunk.length
|
|
22
|
-
if (bytes > LIMITS.body) throw failure(413, "body_too_large")
|
|
23
20
|
chunks.push(chunk)
|
|
24
21
|
}
|
|
25
22
|
try { return JSON.parse(Buffer.concat(chunks).toString("utf8")) } catch { throw failure(400, "invalid_json") }
|
|
@@ -27,7 +24,10 @@ async function readBody(request) {
|
|
|
27
24
|
|
|
28
25
|
export async function createLiveBridge({ config, client, directory, reporter }) {
|
|
29
26
|
const options = config.liveBridge
|
|
30
|
-
if (!options?.enabled
|
|
27
|
+
if (!options?.enabled || (options.projectDirectory !== undefined &&
|
|
28
|
+
resolve(options.projectDirectory) !== resolve(directory))) {
|
|
29
|
+
return { handle() {}, async dispose() {}, address: undefined }
|
|
30
|
+
}
|
|
31
31
|
const token = options.token
|
|
32
32
|
if (typeof token !== "string" || token.length < 32 || token === config.token) {
|
|
33
33
|
throw new Error("Live bridge requires a separate credential of at least 32 characters")
|
|
@@ -37,41 +37,46 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
37
37
|
const sanitizer = createBridgeSanitizer([token, config.token])
|
|
38
38
|
const epoch = randomUUID()
|
|
39
39
|
const events = [], streams = new Set(), commands = new Map(), states = new Map()
|
|
40
|
-
let sequence = 0,
|
|
41
|
-
|
|
42
|
-
const lookupWaiters = new Set()
|
|
40
|
+
let sequence = 0, disposed = false, disposeTask
|
|
41
|
+
const lookupWaiters = new Set(), lookups = new Map()
|
|
43
42
|
const cursor = () => `${epoch}:${sequence}`
|
|
44
43
|
|
|
45
|
-
function lookup(id) {
|
|
44
|
+
function lookup(id, signal) {
|
|
46
45
|
if (disposed) return Promise.reject(failure(503, "bridge_closed"))
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
let entry = lookups.get(id)
|
|
47
|
+
if (!entry) {
|
|
48
|
+
entry = { waiters: new Set() }
|
|
49
|
+
lookups.set(id, entry)
|
|
50
|
+
const settled = (ok, value) => {
|
|
51
|
+
lookups.delete(id)
|
|
52
|
+
for (const finish of [...entry.waiters]) finish(ok, value)
|
|
53
|
+
}
|
|
54
|
+
// Retrying readers share one in-flight native lookup for the exact ID.
|
|
55
|
+
// Aborted HTTP readers are removable subscribers, not retained Promise callbacks.
|
|
56
|
+
Promise.resolve().then(() => api.get(id)).then(value => settled(true, value), error => settled(false, error))
|
|
57
|
+
}
|
|
49
58
|
return new Promise((resolve, reject) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (finished) return
|
|
53
|
-
finished = true
|
|
54
|
-
clearTimeout(timer)
|
|
59
|
+
const finish = (ok, value) => {
|
|
60
|
+
if (!entry.waiters.delete(finish)) return
|
|
55
61
|
lookupWaiters.delete(cancel)
|
|
56
|
-
|
|
62
|
+
signal?.removeEventListener("abort", aborted)
|
|
63
|
+
if (ok) resolve(value)
|
|
64
|
+
else reject(value)
|
|
57
65
|
}
|
|
58
|
-
const cancel = () => finish(
|
|
59
|
-
const
|
|
66
|
+
const cancel = () => finish(false, failure(503, "bridge_closed"))
|
|
67
|
+
const aborted = () => finish(false, failure(499, "request_cancelled"))
|
|
68
|
+
entry.waiters.add(finish)
|
|
60
69
|
lookupWaiters.add(cancel)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Promise.resolve().then(() => api.get(id)).then(
|
|
64
|
-
(value) => { lookups--; finish(resolve, value) },
|
|
65
|
-
(error) => { lookups--; finish(reject, error) },
|
|
66
|
-
)
|
|
70
|
+
signal?.addEventListener("abort", aborted, { once: true })
|
|
71
|
+
if (signal?.aborted) aborted()
|
|
67
72
|
})
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
async function session(id) {
|
|
75
|
+
async function session(id, signal) {
|
|
71
76
|
if (!validBridgeID(id) || id === "global") throw failure(400, "invalid_session")
|
|
72
77
|
let info
|
|
73
|
-
try { info = await lookup(id) } catch (error) {
|
|
74
|
-
if (["
|
|
78
|
+
try { info = await lookup(id, signal) } catch (error) {
|
|
79
|
+
if (["bridge_closed", "request_cancelled"].includes(error.code)) throw error
|
|
75
80
|
throw failure(404, "session_not_found")
|
|
76
81
|
}
|
|
77
82
|
if (info?.id !== id || typeof info.time?.archived === "number") {
|
|
@@ -91,18 +96,12 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
91
96
|
const removed = streams.delete(stream)
|
|
92
97
|
clearInterval(stream.heartbeat)
|
|
93
98
|
stream.queue.length = 0
|
|
94
|
-
stream.queuedBytes = 0
|
|
95
99
|
if (removed && !streams.size && !disposed) connectionNotifications.update(false)
|
|
96
100
|
}
|
|
97
101
|
function write(stream, value) {
|
|
98
102
|
if (!streams.has(stream)) return
|
|
99
103
|
if (stream.blocked) {
|
|
100
104
|
stream.queue.push(value)
|
|
101
|
-
stream.queuedBytes += Buffer.byteLength(value)
|
|
102
|
-
if (stream.queuedBytes > LIMITS.bytes) {
|
|
103
|
-
removeStream(stream)
|
|
104
|
-
stream.response.destroy()
|
|
105
|
-
}
|
|
106
105
|
return
|
|
107
106
|
}
|
|
108
107
|
if (!stream.response.write(value)) {
|
|
@@ -120,17 +119,9 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
120
119
|
if (safe.type === "session.status") states.set(safe.sessionID, safe.data.status)
|
|
121
120
|
if (safe.type === "session.idle") states.set(safe.sessionID, "idle")
|
|
122
121
|
if (safe.type === "session.deleted") states.delete(safe.sessionID)
|
|
123
|
-
if (states.size > 256) states.delete(states.keys().next().value)
|
|
124
122
|
const record = { id: `${epoch}:${++sequence}`, ...safe, timestamp: Date.now() }
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
record.data = { truncated: true }
|
|
128
|
-
frame = `id: ${record.id}\nevent: backeer.event\ndata: ${JSON.stringify(record)}\n\n`
|
|
129
|
-
}
|
|
130
|
-
const size = Buffer.byteLength(frame)
|
|
131
|
-
events.push({ sequence, sessionID: record.sessionID, frame, size })
|
|
132
|
-
bytes += size
|
|
133
|
-
while (events.length > LIMITS.events || bytes > LIMITS.bytes) bytes -= events.shift().size
|
|
123
|
+
const frame = `id: ${record.id}\nevent: backeer.event\ndata: ${JSON.stringify(record)}\n\n`
|
|
124
|
+
events.push({ sequence, sessionID: record.sessionID, frame })
|
|
134
125
|
for (const stream of streams) if (!stream.sessionID || stream.sessionID === record.sessionID) write(stream, frame)
|
|
135
126
|
}
|
|
136
127
|
function authorized(request) {
|
|
@@ -141,7 +132,7 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
141
132
|
}
|
|
142
133
|
function parseCursor(value) {
|
|
143
134
|
if (!value) return { after: 0 }
|
|
144
|
-
const match = /^([a-f0-9-]{36}):(\d
|
|
135
|
+
const match = /^([a-f0-9-]{36}):(\d+)$/.exec(value)
|
|
145
136
|
if (!match) throw failure(400, "invalid_cursor")
|
|
146
137
|
if (match[1] !== epoch) return { gap: "stream_restarted" }
|
|
147
138
|
const after = Number(match[2])
|
|
@@ -150,9 +141,6 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
150
141
|
return { after }
|
|
151
142
|
}
|
|
152
143
|
function cachedCommand(requestId, sessionID, signature) {
|
|
153
|
-
for (const [id, command] of commands) {
|
|
154
|
-
if (command.settled && Date.now() - command.created >= LIMITS.commandTTL) commands.delete(id)
|
|
155
|
-
}
|
|
156
144
|
const existing = commands.get(requestId)
|
|
157
145
|
if (existing && (existing.sessionID !== sessionID || existing.signature !== signature)) {
|
|
158
146
|
throw failure(409, "request_id_conflict")
|
|
@@ -162,17 +150,16 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
162
150
|
async function execute(requestId, sessionID, signature, operation, questionId) {
|
|
163
151
|
let command = cachedCommand(requestId, sessionID, signature)
|
|
164
152
|
if (!command) {
|
|
165
|
-
if (commands.size >= LIMITS.commands) throw failure(429, "command_window_full")
|
|
166
153
|
// Another chat/mobile/native UI may answer too: OpenCode is authoritative.
|
|
167
154
|
// Reserve locally before calling it so concurrent request IDs cannot submit twice.
|
|
168
155
|
if (questionId && [...commands.values()].some(item => item.sessionID === sessionID && item.questionId === questionId)) {
|
|
169
156
|
throw failure(409, "question_reply_already_submitted")
|
|
170
157
|
}
|
|
171
|
-
command = { sessionID, signature, questionId,
|
|
158
|
+
command = { sessionID, signature, questionId,
|
|
172
159
|
result: { requestId, sessionID, ...(questionId ? { questionId } : {}),
|
|
173
160
|
status: questionId ? "answered" : "accepted" } }
|
|
174
161
|
commands.set(requestId, command)
|
|
175
|
-
command.task = Promise.resolve().then(operation).
|
|
162
|
+
command.task = Promise.resolve().then(operation).then(() => undefined)
|
|
176
163
|
}
|
|
177
164
|
await command.task
|
|
178
165
|
return command.result
|
|
@@ -182,7 +169,6 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
182
169
|
const pending = await api.questions(sessionID)
|
|
183
170
|
if (!Array.isArray(pending)) throw failure(502, "invalid_question_list")
|
|
184
171
|
const own = pending.filter(item => item.sessionID === sessionID)
|
|
185
|
-
if (own.length > 16) throw failure(413, "too_many_questions")
|
|
186
172
|
return own
|
|
187
173
|
}
|
|
188
174
|
async function route(request, response) {
|
|
@@ -191,10 +177,9 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
191
177
|
request.headers["x-backeer-project"] !== encodeURIComponent(directory)) {
|
|
192
178
|
return json(response, 409, { error: "project_mismatch" })
|
|
193
179
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
timeout.unref()
|
|
180
|
+
const controller = new AbortController()
|
|
181
|
+
const closed = () => controller.abort()
|
|
182
|
+
response.once("close", closed)
|
|
198
183
|
try {
|
|
199
184
|
const url = new URL(request.url, "http://127.0.0.1")
|
|
200
185
|
if (url.username || url.password) throw failure(400, "invalid_url")
|
|
@@ -203,7 +188,7 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
203
188
|
const values = Array.isArray(result) ? result : result?.data
|
|
204
189
|
if (!Array.isArray(values)) throw failure(502, "invalid_session_list")
|
|
205
190
|
const own = []
|
|
206
|
-
for (const info of values
|
|
191
|
+
for (const info of values) {
|
|
207
192
|
if (!validBridgeID(info?.id) || typeof info.time?.archived === "number") continue
|
|
208
193
|
if (api.ownsSession ? await api.ownsSession(info) : (info.location?.directory ?? info.directory) === directory) own.push(info)
|
|
209
194
|
}
|
|
@@ -220,11 +205,9 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
220
205
|
}
|
|
221
206
|
if (request.method === "GET" && url.pathname === "/v1/events") {
|
|
222
207
|
const selected = url.searchParams.get("sessionID") || undefined
|
|
223
|
-
if (selected) await session(selected)
|
|
208
|
+
if (selected) await session(selected, controller.signal)
|
|
224
209
|
const parsed = parseCursor(url.searchParams.get("after") ?? request.headers["last-event-id"])
|
|
225
210
|
if (response.headersSent) return
|
|
226
|
-
if (streams.size >= LIMITS.clients) throw failure(429, "too_many_streams")
|
|
227
|
-
clearTimeout(timeout)
|
|
228
211
|
response.writeHead(200, {
|
|
229
212
|
"content-type": "text/event-stream", "cache-control": "no-cache, no-transform",
|
|
230
213
|
"x-accel-buffering": "no", connection: "keep-alive",
|
|
@@ -234,7 +217,7 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
234
217
|
return
|
|
235
218
|
}
|
|
236
219
|
const stream = { response, sessionID: selected, heartbeat: undefined,
|
|
237
|
-
blocked: false, queue: []
|
|
220
|
+
blocked: false, queue: [] }
|
|
238
221
|
streams.add(stream)
|
|
239
222
|
connectionNotifications.update(true)
|
|
240
223
|
request.socket.setTimeout(0)
|
|
@@ -244,7 +227,6 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
244
227
|
stream.blocked = false
|
|
245
228
|
while (streams.has(stream) && !stream.blocked && stream.queue.length) {
|
|
246
229
|
const next = stream.queue.shift()
|
|
247
|
-
stream.queuedBytes -= Buffer.byteLength(next)
|
|
248
230
|
write(stream, next)
|
|
249
231
|
}
|
|
250
232
|
})
|
|
@@ -259,13 +241,12 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
259
241
|
}
|
|
260
242
|
return
|
|
261
243
|
}
|
|
262
|
-
const match = /^\/v1\/sessions\/([A-Za-z0-9_-]
|
|
244
|
+
const match = /^\/v1\/sessions\/([A-Za-z0-9_-]+)(\/instructions|\/questions(?:\/([A-Za-z0-9_-]+)\/answers)?)?$/.exec(url.pathname)
|
|
263
245
|
if (!match) throw failure(404, "route_not_found")
|
|
264
|
-
if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1]))
|
|
246
|
+
if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1], controller.signal))
|
|
265
247
|
if (request.method === "GET" && match[2] === "/questions") {
|
|
266
|
-
await session(match[1])
|
|
248
|
+
await session(match[1], controller.signal)
|
|
267
249
|
const result = { questions: (await questions(match[1])).map(item => questionSnapshot(item, sanitizer.text)) }
|
|
268
|
-
if (Buffer.byteLength(JSON.stringify(result)) > 240_000) throw failure(413, "questions_too_large")
|
|
269
250
|
return json(response, 200, result)
|
|
270
251
|
}
|
|
271
252
|
if (request.method !== "POST" || !match[2]) throw failure(405, "method_not_allowed")
|
|
@@ -274,8 +255,8 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
274
255
|
if (match[3]) {
|
|
275
256
|
if (!body || !validBridgeID(body.requestId) || !/^[a-f0-9]{64}$/.test(body.revision) ||
|
|
276
257
|
Object.keys(body).some(key => !["requestId", "revision", "answers"].includes(key))) throw failure(400, "invalid_question_answer")
|
|
277
|
-
await session(match[1])
|
|
278
|
-
const signature = JSON.stringify({ questionId: match[3], revision: body.revision, answers: body.answers })
|
|
258
|
+
await session(match[1], controller.signal)
|
|
259
|
+
const signature = createHash("sha256").update(JSON.stringify({ questionId: match[3], revision: body.revision, answers: body.answers })).digest("hex")
|
|
279
260
|
const existing = cachedCommand(body.requestId, match[1], signature)
|
|
280
261
|
if (existing) { await existing.task; return json(response, 200, existing.result) }
|
|
281
262
|
const pending = (await questions(match[1])).find(item => item.id === match[3])
|
|
@@ -289,25 +270,24 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
289
270
|
}
|
|
290
271
|
if (match[2] !== "/instructions") throw failure(405, "method_not_allowed")
|
|
291
272
|
if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" ||
|
|
292
|
-
!body.prompt.trim() ||
|
|
273
|
+
!body.prompt.trim() ||
|
|
293
274
|
Object.keys(body).some((key) => !["requestId", "prompt"].includes(key))) throw failure(400, "invalid_instruction")
|
|
294
|
-
await session(match[1])
|
|
275
|
+
await session(match[1], controller.signal)
|
|
295
276
|
if (response.headersSent || disposed) return
|
|
296
|
-
const signature = JSON.stringify({ prompt: body.prompt })
|
|
277
|
+
const signature = createHash("sha256").update(JSON.stringify({ prompt: body.prompt })).digest("hex")
|
|
297
278
|
const existing = cachedCommand(body.requestId, match[1], signature)
|
|
298
279
|
const result = await execute(body.requestId, match[1], signature, () => api.prompt(match[1], body.prompt))
|
|
299
280
|
return json(response, existing ? 200 : 202, result)
|
|
300
281
|
} catch (error) {
|
|
301
282
|
json(response, error.status ?? 502, { error: error.code ?? "opencode_request_failed" })
|
|
302
283
|
} finally {
|
|
303
|
-
|
|
304
|
-
clearTimeout(timeout)
|
|
284
|
+
response.removeListener("close", closed)
|
|
305
285
|
}
|
|
306
286
|
}
|
|
307
287
|
const server = createServer((request, response) => { void route(request, response) })
|
|
308
|
-
server.requestTimeout =
|
|
309
|
-
server.headersTimeout =
|
|
310
|
-
server.keepAliveTimeout =
|
|
288
|
+
server.requestTimeout = 0
|
|
289
|
+
server.headersTimeout = 0
|
|
290
|
+
server.keepAliveTimeout = 0
|
|
311
291
|
await new Promise((resolve, reject) => {
|
|
312
292
|
server.once("error", reject)
|
|
313
293
|
server.listen(options.port ?? 43871, "127.0.0.1", () => { server.off("error", reject); resolve() })
|
|
@@ -325,6 +305,7 @@ export async function createLiveBridge({ config, client, directory, reporter })
|
|
|
325
305
|
events.length = 0
|
|
326
306
|
states.clear()
|
|
327
307
|
commands.clear()
|
|
308
|
+
lookups.clear()
|
|
328
309
|
sanitizer.clear()
|
|
329
310
|
api.dispose?.()
|
|
330
311
|
})
|