pi-freeflow 1.5.0 → 1.7.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 +62 -86
- package/package.json +2 -3
- package/src/catalog.ts +21 -12
- package/src/commands.ts +308 -146
- package/src/config.ts +24 -2
- package/src/deploy.ts +128 -185
- package/src/health.ts +2 -7
- package/src/index.ts +22 -4
- package/src/logger.ts +52 -24
- package/src/models.ts +139 -67
- package/src/probe.ts +49 -0
- package/src/proxy.ts +150 -57
- package/src/relay-state.ts +380 -69
- package/src/relay.ts +72 -26
- package/src/stream-pipe.ts +307 -271
- package/src/types.ts +4 -2
- package/src/update-checker.ts +21 -5
package/src/stream-pipe.ts
CHANGED
|
@@ -1,271 +1,307 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upstream SSE streaming pipeline with thinking sniffing and exception isolation.
|
|
3
|
-
*
|
|
4
|
-
* Prevents upstream stream aborts or timeouts from crashing the host process.
|
|
5
|
-
* Sniffs thinking and reasoning chunks for debug trace logging without payload mutation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import type * as http from "node:http";
|
|
10
|
-
import type { Readable } from "node:stream";
|
|
11
|
-
import { isDebugEnabled, log } from "./logger.ts";
|
|
12
|
-
import { markRelayFailure } from "./relay-state.ts";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
s.includes("
|
|
66
|
-
s.includes("
|
|
67
|
-
s.includes("
|
|
68
|
-
s.includes("
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
res.
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
!
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Upstream SSE streaming pipeline with thinking sniffing and exception isolation.
|
|
3
|
+
*
|
|
4
|
+
* Prevents upstream stream aborts or timeouts from crashing the host process.
|
|
5
|
+
* Sniffs thinking and reasoning chunks for debug trace logging without payload mutation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import type * as http from "node:http";
|
|
10
|
+
import type { Readable } from "node:stream";
|
|
11
|
+
import { isDebugEnabled, log } from "./logger.ts";
|
|
12
|
+
import { markRelayFailure } from "./relay-state.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A premature stream end counts as "substantial" only when BOTH thresholds
|
|
16
|
+
* are strictly exceeded: more than 50 chunks AND more than 100KB. Substantial
|
|
17
|
+
* truncations are reported as incomplete; smaller drops stay failed.
|
|
18
|
+
*/
|
|
19
|
+
export const SUBSTANTIAL_MIN_CHUNKS = 50;
|
|
20
|
+
export const SUBSTANTIAL_MIN_BYTES = 100 * 1024;
|
|
21
|
+
|
|
22
|
+
export function isSubstantial(chunks: number, bytes: number): boolean {
|
|
23
|
+
return chunks > SUBSTANTIAL_MIN_CHUNKS && bytes > SUBSTANTIAL_MIN_BYTES;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pipes an upstream readable stream to a client HTTP response.
|
|
27
|
+
*
|
|
28
|
+
* - Flushes HTTP headers immediately for low Time-to-First-Byte (TTFB).
|
|
29
|
+
* - Detects thinking/reasoning tokens in chunks for diagnostic logs.
|
|
30
|
+
* - Handles upstream errors (returns 502 if headers unsent) and ends response.
|
|
31
|
+
* - Cleans up resources and destroys upstream stream on client disconnect/abort.
|
|
32
|
+
*/
|
|
33
|
+
export function pipeUpstreamStream(
|
|
34
|
+
nodeStream: Readable,
|
|
35
|
+
res: http.ServerResponse,
|
|
36
|
+
req: http.IncomingMessage,
|
|
37
|
+
reqId?: string,
|
|
38
|
+
relayUrl?: string,
|
|
39
|
+
): void {
|
|
40
|
+
const rid = reqId || randomUUID().slice(0, 8);
|
|
41
|
+
let totalChunks = 0;
|
|
42
|
+
let totalBytes = 0;
|
|
43
|
+
let thinkingChunks = 0;
|
|
44
|
+
let thinkingBytes = 0;
|
|
45
|
+
let firstChunkAt: number | null = null;
|
|
46
|
+
const startAt = Date.now();
|
|
47
|
+
const isResponsesApi = req.url?.includes("/responses") ?? false;
|
|
48
|
+
let hasTerminalEvent = false;
|
|
49
|
+
// Abort-cause tracking: only genuine upstream-side truncation may penalize
|
|
50
|
+
// relay health. Client disconnects and clean upstream ends must not.
|
|
51
|
+
let upstreamEnded = false;
|
|
52
|
+
let clientAborted = false;
|
|
53
|
+
// Terminal-marker scan carry: holds the tail of the previously scanned
|
|
54
|
+
// view so a marker split across adjacent chunks ("[DO" | "NE]") is still
|
|
55
|
+
// recognized. Longest marker is "response.completed" (18 bytes); 32
|
|
56
|
+
// gives comfortable headroom.
|
|
57
|
+
let terminalScanCarry = Buffer.alloc(0);
|
|
58
|
+
|
|
59
|
+
const sniffThinking = (chunk: Buffer | string): boolean => {
|
|
60
|
+
const s =
|
|
61
|
+
typeof chunk === "string"
|
|
62
|
+
? chunk
|
|
63
|
+
: chunk.toString("utf8", 0, Math.min(chunk.length, 4000));
|
|
64
|
+
return (
|
|
65
|
+
s.includes("reasoning") ||
|
|
66
|
+
s.includes("thinking") ||
|
|
67
|
+
s.includes("<think>") ||
|
|
68
|
+
s.includes("reasoning_content") ||
|
|
69
|
+
s.includes('"type":"thinking"') ||
|
|
70
|
+
s.includes("thinking_delta")
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const checkTerminalEvent = (s: string): boolean => {
|
|
75
|
+
return (
|
|
76
|
+
s.includes("response.completed") ||
|
|
77
|
+
s.includes("response.done") ||
|
|
78
|
+
s.includes("response.failed") ||
|
|
79
|
+
s.includes("response.incomplete") ||
|
|
80
|
+
s.includes("[DONE]")
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
const ensureTerminalEvent = (
|
|
84
|
+
isError = false,
|
|
85
|
+
errorMsg?: string,
|
|
86
|
+
penalizeRelay = true,
|
|
87
|
+
) => {
|
|
88
|
+
if (hasTerminalEvent || res.writableEnded) return;
|
|
89
|
+
// Only genuine upstream-side truncation penalizes relay health.
|
|
90
|
+
// Client disconnects and clean upstream ends leave it untouched.
|
|
91
|
+
if (penalizeRelay && relayUrl && relayUrl !== "direct") {
|
|
92
|
+
markRelayFailure(relayUrl, 0, errorMsg || "stream truncated prematurely");
|
|
93
|
+
}
|
|
94
|
+
if (isResponsesApi && totalChunks > 0) {
|
|
95
|
+
try {
|
|
96
|
+
if (isError) {
|
|
97
|
+
res.write(
|
|
98
|
+
`\nevent: response.failed\ndata: {"type":"response.failed","response":{"status":"failed","error":{"code":"stream_error","message":${JSON.stringify(errorMsg || "Upstream stream disconnected unexpectedly")}}}}\n\n`,
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
res.write(
|
|
102
|
+
`\nevent: response.incomplete\ndata: {"type":"response.incomplete","response":{"status":"incomplete","incomplete_details":{"reason":"cancelled"}}}\n\n`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
hasTerminalEvent = true;
|
|
106
|
+
log(
|
|
107
|
+
"warn",
|
|
108
|
+
`injected synthetic response.${isError ? "failed" : "incomplete"} for prematurely truncated stream`,
|
|
109
|
+
{ totalChunks, totalBytes, isError, errorMsg },
|
|
110
|
+
rid,
|
|
111
|
+
);
|
|
112
|
+
} catch {}
|
|
113
|
+
} else if (!isResponsesApi && totalChunks > 0) {
|
|
114
|
+
try {
|
|
115
|
+
res.write("\ndata: [DONE]\n\n");
|
|
116
|
+
hasTerminalEvent = true;
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (typeof res.flushHeaders === "function") {
|
|
123
|
+
res.flushHeaders();
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
nodeStream.on("data", (chunk: Buffer | string) => {
|
|
127
|
+
try {
|
|
128
|
+
if (firstChunkAt === null) {
|
|
129
|
+
firstChunkAt = Date.now();
|
|
130
|
+
const ttfb = firstChunkAt - startAt;
|
|
131
|
+
log("debug", `stream first chunk in ${ttfb}ms`, undefined, rid);
|
|
132
|
+
}
|
|
133
|
+
totalChunks++;
|
|
134
|
+
const chunkSize =
|
|
135
|
+
typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
|
|
136
|
+
totalBytes += chunkSize;
|
|
137
|
+
|
|
138
|
+
if (sniffThinking(chunk)) {
|
|
139
|
+
thinkingChunks++;
|
|
140
|
+
thinkingBytes += chunkSize;
|
|
141
|
+
if (isDebugEnabled() && thinkingChunks <= 3) {
|
|
142
|
+
const preview =
|
|
143
|
+
typeof chunk === "string"
|
|
144
|
+
? chunk.slice(0, 600)
|
|
145
|
+
: chunk.toString("utf8", 0, 600);
|
|
146
|
+
log(
|
|
147
|
+
"debug",
|
|
148
|
+
`thinking chunk #${thinkingChunks}`,
|
|
149
|
+
{ preview: preview.slice(0, 400) },
|
|
150
|
+
rid,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Always scan the FULL chunk plus the small carry from the previous
|
|
155
|
+
// view. SSE chunks are KBs at most, so includes() over everything
|
|
156
|
+
// is negligible against correctness — the removed head/tail windows
|
|
157
|
+
// are exactly what let markers buried mid-chunk or split across
|
|
158
|
+
// adjacent chunks escape and cause duplicate synthetic terminals.
|
|
159
|
+
const buf =
|
|
160
|
+
typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
161
|
+
const scanBuf =
|
|
162
|
+
terminalScanCarry.length > 0
|
|
163
|
+
? Buffer.concat([terminalScanCarry, buf])
|
|
164
|
+
: buf;
|
|
165
|
+
if (!hasTerminalEvent && checkTerminalEvent(scanBuf.toString("utf8"))) {
|
|
166
|
+
hasTerminalEvent = true;
|
|
167
|
+
}
|
|
168
|
+
terminalScanCarry = Buffer.from(scanBuf.subarray(Math.max(0, scanBuf.length - 32)));
|
|
169
|
+
|
|
170
|
+
const canContinue = res.write(chunk);
|
|
171
|
+
if (canContinue === false && !nodeStream.destroyed) {
|
|
172
|
+
// Backpressure: pause the upstream source until the client
|
|
173
|
+
// socket drains, instead of buffering an unbounded amount into
|
|
174
|
+
// the response. 'drain' might never fire if the client closes —
|
|
175
|
+
// the close handlers below destroy the stream regardless of
|
|
176
|
+
// its paused state.
|
|
177
|
+
nodeStream.pause();
|
|
178
|
+
const onDrain = () => {
|
|
179
|
+
res.off("drain", onDrain);
|
|
180
|
+
if (!nodeStream.destroyed) nodeStream.resume();
|
|
181
|
+
};
|
|
182
|
+
res.once("drain", onDrain);
|
|
183
|
+
}
|
|
184
|
+
const maybeFlush = res as unknown as { flush?: () => void };
|
|
185
|
+
if (typeof maybeFlush.flush === "function") {
|
|
186
|
+
maybeFlush.flush();
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
nodeStream.on("error", (e: unknown) => {
|
|
192
|
+
const errorMsg = (e as Error)?.message || String(e);
|
|
193
|
+
// An abort (client disconnect or a proxy-internal header timeout marked
|
|
194
|
+
// FF_INTERNAL_ABORT) is not a relay fault — never penalize relay health,
|
|
195
|
+
// and behave like a client abort for the terminal marker. Only genuine
|
|
196
|
+
// upstream-side failures (socket errors, truncation) penalize.
|
|
197
|
+
// Stream errors are always Error subclasses (node/undici).
|
|
198
|
+
const streamErr = e as Error & { code?: string };
|
|
199
|
+
const isInternalAbort =
|
|
200
|
+
streamErr?.name === "AbortError" || streamErr?.code === "FF_INTERNAL_ABORT";
|
|
201
|
+
log(
|
|
202
|
+
isInternalAbort ? "warn" : "error",
|
|
203
|
+
isInternalAbort
|
|
204
|
+
? "upstream stream aborted (internal cancel/timeout)"
|
|
205
|
+
: "upstream stream error",
|
|
206
|
+
{ error: errorMsg, totalChunks, thinkingChunks, hasTerminalEvent },
|
|
207
|
+
rid,
|
|
208
|
+
);
|
|
209
|
+
try {
|
|
210
|
+
if (!res.headersSent) {
|
|
211
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
212
|
+
} else if (isInternalAbort) {
|
|
213
|
+
clientAborted = true;
|
|
214
|
+
ensureTerminalEvent(false, errorMsg || "stream interrupted", false);
|
|
215
|
+
} else {
|
|
216
|
+
ensureTerminalEvent(!isSubstantial(totalChunks, totalBytes), errorMsg, true);
|
|
217
|
+
}
|
|
218
|
+
if (!res.writableEnded) {
|
|
219
|
+
res.end();
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
nodeStream.on("end", () => {
|
|
225
|
+
upstreamEnded = true;
|
|
226
|
+
const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);
|
|
227
|
+
if (!hasTerminalEvent && totalChunks > 0) {
|
|
228
|
+
// Clean upstream end without a detectable marker: keep the host
|
|
229
|
+
// contract (synthetic terminal) but never blame the relay.
|
|
230
|
+
ensureTerminalEvent(
|
|
231
|
+
false,
|
|
232
|
+
"upstream ended without terminal marker",
|
|
233
|
+
false,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (thinkingChunks > 0) {
|
|
237
|
+
log(
|
|
238
|
+
"info",
|
|
239
|
+
`stream ended in ${elapsed}s — ${totalChunks} chunks (${(totalBytes / 1024).toFixed(1)}KB), thinking: ${thinkingChunks} chunks (${(thinkingBytes / 1024).toFixed(1)}KB)`,
|
|
240
|
+
undefined,
|
|
241
|
+
rid,
|
|
242
|
+
);
|
|
243
|
+
} else if (isDebugEnabled()) {
|
|
244
|
+
log(
|
|
245
|
+
"debug",
|
|
246
|
+
`stream ended in ${elapsed}s — ${totalChunks} chunks (${(totalBytes / 1024).toFixed(1)}KB), no thinking detected`,
|
|
247
|
+
undefined,
|
|
248
|
+
rid,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
if (!res.writableEnded) res.end();
|
|
253
|
+
} catch {}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
nodeStream.on("close", () => {
|
|
257
|
+
try {
|
|
258
|
+
if (!hasTerminalEvent && totalChunks > 0) {
|
|
259
|
+
if (clientAborted || upstreamEnded) {
|
|
260
|
+
// Client disconnect teardown or post-end cleanup — the relay
|
|
261
|
+
// is not at fault; still give the host a terminal event.
|
|
262
|
+
ensureTerminalEvent(false, "stream interrupted by client", false);
|
|
263
|
+
} else {
|
|
264
|
+
// Upstream socket died mid-stream with no error event.
|
|
265
|
+
// For muse-spark large payloads: raxtant 514KB failed but feoni 802KB
|
|
266
|
+
// succeeded with same 2.6MB in — so this is edge-specific, not pure
|
|
267
|
+
// provider token limit. Keep penalize=true to rotate failing relay,
|
|
268
|
+
// but inject incomplete (not failed) for substantial to avoid alarming
|
|
269
|
+
// stream_error. Small premature (<50 chunks) stays failed+penalize.
|
|
270
|
+
ensureTerminalEvent(!isSubstantial(totalChunks, totalBytes), "stream closed prematurely", true);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!res.writableEnded) res.end();
|
|
274
|
+
} catch {}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
req.on("aborted", () => {
|
|
278
|
+
clientAborted = true;
|
|
279
|
+
log("warn", "client aborted — destroying upstream", { totalChunks }, rid);
|
|
280
|
+
if (!nodeStream.destroyed) nodeStream.destroy();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
req.on("close", () => {
|
|
284
|
+
if (!upstreamEnded && !nodeStream.destroyed) {
|
|
285
|
+
clientAborted = true;
|
|
286
|
+
nodeStream.destroy();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
res.on("close", () => {
|
|
291
|
+
if (
|
|
292
|
+
!upstreamEnded &&
|
|
293
|
+
!res.writableEnded &&
|
|
294
|
+
!nodeStream.destroyed
|
|
295
|
+
) {
|
|
296
|
+
clientAborted = true;
|
|
297
|
+
nodeStream.destroy();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
res.on("error", () => {
|
|
302
|
+
if (!upstreamEnded && !nodeStream.destroyed) {
|
|
303
|
+
clientAborted = true;
|
|
304
|
+
nodeStream.destroy();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface KnownRelay {
|
|
|
38
38
|
url: string;
|
|
39
39
|
label?: string;
|
|
40
40
|
addedAt?: string;
|
|
41
|
+
/** Shared secret set by /freeflow deploy; proxy sends x-relay-auth only when present. */
|
|
42
|
+
auth?: string;
|
|
41
43
|
}
|
|
42
44
|
export type RelayMode = "auto" | "on" | "off";
|
|
43
45
|
|
|
@@ -92,9 +94,9 @@ export interface RateLimitStatus {
|
|
|
92
94
|
export interface ExtensionUIContext {
|
|
93
95
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
94
96
|
setStatus(key: string, status: string | undefined): void;
|
|
95
|
-
input(prompt: string,
|
|
97
|
+
input(prompt: string, placeholder?: string): Promise<string | undefined>;
|
|
96
98
|
select(prompt: string, options: string[]): Promise<string | undefined>;
|
|
97
|
-
confirm?(
|
|
99
|
+
confirm?(title: string, message?: string): Promise<boolean>;
|
|
98
100
|
/** Optional status-bar click handler (when host supports clickable widgets) */
|
|
99
101
|
onStatusClick?(handler: () => void | Promise<void>): void | (() => void);
|
|
100
102
|
/** Fallback/alternate names hosts may expose */
|