@stacknet/stacks 0.1.2 → 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/dist/{billing-BqscteyZ.d.cts → billing-eQZIWeNW.d.cts} +2 -0
- package/dist/{billing-BqscteyZ.d.ts → billing-eQZIWeNW.d.ts} +2 -0
- package/dist/clients/index.cjs +4 -4
- package/dist/clients/index.d.cts +4 -1
- package/dist/clients/index.d.ts +4 -1
- package/dist/clients/index.js +4 -4
- package/dist/{index-DVzKiF_0.d.cts → index-B_dUFmAg.d.cts} +31 -6
- package/dist/{index-DVzKiF_0.d.ts → index-B_dUFmAg.d.ts} +31 -6
- package/dist/index.cjs +12 -16
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +12 -16
- package/dist/proxy/index.cjs +2 -2
- package/dist/proxy/index.d.cts +1 -1
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.js +2 -2
- package/dist/streaming/index.cjs +8 -12
- package/dist/streaming/index.js +8 -12
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/clients/agents.ts +23 -6
- package/src/managers/task-manager.ts +25 -3
- package/src/proxy/forwarder.ts +105 -16
- package/src/proxy/route-handlers.ts +273 -116
- package/src/streaming/sse.ts +65 -40
- package/src/types/agent.ts +2 -0
package/src/streaming/sse.ts
CHANGED
|
@@ -11,6 +11,22 @@ export interface SSEEvent {
|
|
|
11
11
|
retry?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Hard cap on the in-memory buffer used by `parseSSEStream`. A malicious
|
|
15
|
+
* or malfunctioning upstream that never emits a newline would otherwise
|
|
16
|
+
* grow the buffer until the process OOMs. 1 MiB is far beyond any real
|
|
17
|
+
* SSE event. */
|
|
18
|
+
const MAX_SSE_BUFFER_BYTES = 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
/** SSE framing uses newlines as field separators; any `\r` or `\n` in
|
|
21
|
+
* `event`, `id`, or a string `data` value would let an untrusted caller
|
|
22
|
+
* inject additional SSE frames that the receiver would process as if
|
|
23
|
+
* they came from the server. Strip them. */
|
|
24
|
+
function stripCRLF(value: string): string {
|
|
25
|
+
// Replace CR/LF/LS/PS with a single space so the field remains
|
|
26
|
+
// structurally valid without carrying a frame separator.
|
|
27
|
+
return value.replace(/[\r\n\u2028\u2029]/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
/**
|
|
15
31
|
* Parse SSE stream from a Response
|
|
16
32
|
*/
|
|
@@ -32,6 +48,11 @@ export async function* parseSSEStream(
|
|
|
32
48
|
if (done) break;
|
|
33
49
|
|
|
34
50
|
buffer += decoder.decode(value, { stream: true });
|
|
51
|
+
if (buffer.length > MAX_SSE_BUFFER_BYTES) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`SSE buffer exceeded ${MAX_SSE_BUFFER_BYTES} bytes without an event delimiter`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
35
56
|
const lines = buffer.split('\n');
|
|
36
57
|
buffer = lines.pop() || '';
|
|
37
58
|
|
|
@@ -86,6 +107,43 @@ export async function* parseSSEStream(
|
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
109
|
|
|
110
|
+
/** Encode an SSE message safely — every field with CRLF stripped, data
|
|
111
|
+
* multi-line payloads split into repeated `data:` lines per the spec.
|
|
112
|
+
* Keeping this in one place lets both `createSSEResponse` and
|
|
113
|
+
* `SSEWriter.write` share exactly the same sanitization rules. */
|
|
114
|
+
function encodeEvent(event: SSEEvent): string {
|
|
115
|
+
let message = '';
|
|
116
|
+
|
|
117
|
+
if (event.event) {
|
|
118
|
+
message += `event: ${stripCRLF(event.event)}\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (event.id) {
|
|
122
|
+
message += `id: ${stripCRLF(event.id)}\n`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (event.retry !== undefined && Number.isFinite(event.retry)) {
|
|
126
|
+
// retry is numeric per the SSE spec — coerce defensively.
|
|
127
|
+
message += `retry: ${Math.floor(event.retry)}\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const dataString =
|
|
131
|
+
typeof event.data === 'string'
|
|
132
|
+
? event.data
|
|
133
|
+
: JSON.stringify(event.data);
|
|
134
|
+
|
|
135
|
+
// Per the SSE spec, data may span multiple lines — each line is a
|
|
136
|
+
// separate `data:` field. This also means an embedded newline in a
|
|
137
|
+
// string payload can't be used to smuggle a new frame: the blank
|
|
138
|
+
// line at the end of the event is produced by us, not the caller.
|
|
139
|
+
for (const dataLine of dataString.split(/\r?\n/)) {
|
|
140
|
+
message += `data: ${dataLine}\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
message += '\n';
|
|
144
|
+
return message;
|
|
145
|
+
}
|
|
146
|
+
|
|
89
147
|
/**
|
|
90
148
|
* Create an SSE Response from an async iterable
|
|
91
149
|
*/
|
|
@@ -99,28 +157,7 @@ export function createSSEResponse(
|
|
|
99
157
|
async start(controller) {
|
|
100
158
|
try {
|
|
101
159
|
for await (const event of stream) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (event.event) {
|
|
105
|
-
message += `event: ${event.event}\n`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (event.id) {
|
|
109
|
-
message += `id: ${event.id}\n`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (event.retry) {
|
|
113
|
-
message += `retry: ${event.retry}\n`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const data =
|
|
117
|
-
typeof event.data === 'string'
|
|
118
|
-
? event.data
|
|
119
|
-
: JSON.stringify(event.data);
|
|
120
|
-
|
|
121
|
-
message += `data: ${data}\n\n`;
|
|
122
|
-
|
|
123
|
-
controller.enqueue(encoder.encode(message));
|
|
160
|
+
controller.enqueue(encoder.encode(encodeEvent(event)));
|
|
124
161
|
}
|
|
125
162
|
} catch (error) {
|
|
126
163
|
console.error('SSE stream error:', error);
|
|
@@ -178,23 +215,7 @@ export class SSEWriter {
|
|
|
178
215
|
*/
|
|
179
216
|
write(event: SSEEvent): void {
|
|
180
217
|
if (!this.controller) return;
|
|
181
|
-
|
|
182
|
-
let message = '';
|
|
183
|
-
|
|
184
|
-
if (event.event) {
|
|
185
|
-
message += `event: ${event.event}\n`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (event.id) {
|
|
189
|
-
message += `id: ${event.id}\n`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const data =
|
|
193
|
-
typeof event.data === 'string' ? event.data : JSON.stringify(event.data);
|
|
194
|
-
|
|
195
|
-
message += `data: ${data}\n\n`;
|
|
196
|
-
|
|
197
|
-
this.controller.enqueue(this.encoder.encode(message));
|
|
218
|
+
this.controller.enqueue(this.encoder.encode(encodeEvent(event)));
|
|
198
219
|
}
|
|
199
220
|
|
|
200
221
|
/**
|
|
@@ -209,7 +230,11 @@ export class SSEWriter {
|
|
|
209
230
|
*/
|
|
210
231
|
writeComment(comment: string): void {
|
|
211
232
|
if (!this.controller) return;
|
|
212
|
-
|
|
233
|
+
// Comments are a single `:` line followed by a blank line. Strip any
|
|
234
|
+
// CRLF so a caller can't close out the comment and smuggle a real
|
|
235
|
+
// event after it.
|
|
236
|
+
const safe = stripCRLF(comment);
|
|
237
|
+
this.controller.enqueue(this.encoder.encode(`: ${safe}\n\n`));
|
|
213
238
|
}
|
|
214
239
|
|
|
215
240
|
/**
|
package/src/types/agent.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface AgentFromPromptInput {
|
|
|
98
98
|
|
|
99
99
|
export interface AgentsClientConfig {
|
|
100
100
|
baseUrl?: string;
|
|
101
|
+
/** JWT auth token for authenticated requests */
|
|
102
|
+
authToken?: string;
|
|
101
103
|
/**
|
|
102
104
|
* Use the coprocessor API (/cpx/agent/agents) instead of legacy (/agents).
|
|
103
105
|
* Defaults to true. Set to false for backwards compatibility.
|