responses-proxy 0.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 +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- package/package.json +39 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decoder for the AWS `application/vnd.amazon.eventstream` binary framing used by
|
|
3
|
+
* CodeWhisperer / Amazon Q streaming responses (the same framing Bedrock and
|
|
4
|
+
* Transcribe use). Each message is:
|
|
5
|
+
*
|
|
6
|
+
* prelude (12 bytes):
|
|
7
|
+
* total length uint32 (big-endian) total bytes of the whole message
|
|
8
|
+
* headers length uint32 (big-endian) bytes of the header section
|
|
9
|
+
* prelude crc uint32 (big-endian) CRC32 of the first 8 bytes
|
|
10
|
+
* headers (variable, `headers length` bytes)
|
|
11
|
+
* payload (variable, total - 12 - headersLength - 4 bytes)
|
|
12
|
+
* message crc uint32 (big-endian) CRC32 of every byte except itself
|
|
13
|
+
*
|
|
14
|
+
* Each header is: name length (uint8), name (utf8), value type (uint8), value.
|
|
15
|
+
*
|
|
16
|
+
* The decoder is incremental: feed it chunks and it returns whatever complete
|
|
17
|
+
* messages are now available, buffering any partial trailing bytes.
|
|
18
|
+
*/
|
|
19
|
+
const PRELUDE_LENGTH = 12;
|
|
20
|
+
const MESSAGE_CRC_LENGTH = 4;
|
|
21
|
+
const MIN_MESSAGE_LENGTH = PRELUDE_LENGTH + MESSAGE_CRC_LENGTH;
|
|
22
|
+
export class EventStreamParseError extends Error {
|
|
23
|
+
}
|
|
24
|
+
const CRC32_TABLE = buildCrc32Table();
|
|
25
|
+
function buildCrc32Table() {
|
|
26
|
+
const table = new Uint32Array(256);
|
|
27
|
+
for (let n = 0; n < 256; n += 1) {
|
|
28
|
+
let c = n;
|
|
29
|
+
for (let k = 0; k < 8; k += 1) {
|
|
30
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
31
|
+
}
|
|
32
|
+
table[n] = c >>> 0;
|
|
33
|
+
}
|
|
34
|
+
return table;
|
|
35
|
+
}
|
|
36
|
+
/** Standard IEEE CRC32 over a byte range, returned as an unsigned 32-bit int. */
|
|
37
|
+
export function crc32(buffer, start = 0, end = buffer.length) {
|
|
38
|
+
let crc = 0xffffffff;
|
|
39
|
+
for (let i = start; i < end; i += 1) {
|
|
40
|
+
crc = CRC32_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8);
|
|
41
|
+
}
|
|
42
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Incremental decoder. Push raw bytes as they arrive from the upstream stream and
|
|
46
|
+
* drain complete messages. Relies on the length prefix (not the CRC) to frame
|
|
47
|
+
* messages, matching the proven 9router/AWS-SDK behavior; a bad prelude CRC is
|
|
48
|
+
* tolerated rather than fatal so a single checksum quirk can't kill a valid stream.
|
|
49
|
+
*/
|
|
50
|
+
export class EventStreamParser {
|
|
51
|
+
buffer = Buffer.alloc(0);
|
|
52
|
+
push(chunk) {
|
|
53
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
54
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, incoming]) : incoming;
|
|
55
|
+
return this.drain();
|
|
56
|
+
}
|
|
57
|
+
/** Number of unparsed bytes still buffered (a partial trailing message). */
|
|
58
|
+
pendingBytes() {
|
|
59
|
+
return this.buffer.length;
|
|
60
|
+
}
|
|
61
|
+
drain() {
|
|
62
|
+
const messages = [];
|
|
63
|
+
while (this.buffer.length >= PRELUDE_LENGTH) {
|
|
64
|
+
const totalLength = this.buffer.readUInt32BE(0);
|
|
65
|
+
if (totalLength < MIN_MESSAGE_LENGTH) {
|
|
66
|
+
throw new EventStreamParseError(`event-stream message length ${totalLength} is below the minimum ${MIN_MESSAGE_LENGTH}`);
|
|
67
|
+
}
|
|
68
|
+
if (this.buffer.length < totalLength) {
|
|
69
|
+
// Wait for the rest of this message to arrive.
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
const frame = this.buffer.subarray(0, totalLength);
|
|
73
|
+
messages.push(decodeMessage(frame));
|
|
74
|
+
this.buffer = this.buffer.subarray(totalLength);
|
|
75
|
+
}
|
|
76
|
+
return messages;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Decode every complete message in a fully buffered response body. */
|
|
80
|
+
export function parseEventStream(buffer) {
|
|
81
|
+
const parser = new EventStreamParser();
|
|
82
|
+
const messages = parser.push(buffer);
|
|
83
|
+
if (parser.pendingBytes() > 0) {
|
|
84
|
+
throw new EventStreamParseError(`event-stream buffer ended with ${parser.pendingBytes()} trailing bytes (incomplete message)`);
|
|
85
|
+
}
|
|
86
|
+
return messages;
|
|
87
|
+
}
|
|
88
|
+
function decodeMessage(frame) {
|
|
89
|
+
const totalLength = frame.readUInt32BE(0);
|
|
90
|
+
const headersLength = frame.readUInt32BE(4);
|
|
91
|
+
// Note: bytes 8-11 hold the prelude CRC32. 9router (and the AWS SDK in practice)
|
|
92
|
+
// frame purely by the length prefix and do not reject on CRC mismatch, so we skip
|
|
93
|
+
// validation here to stay byte-compatible with the proven client.
|
|
94
|
+
const payloadStart = PRELUDE_LENGTH + headersLength;
|
|
95
|
+
const payloadEnd = totalLength - MESSAGE_CRC_LENGTH;
|
|
96
|
+
if (payloadStart > payloadEnd) {
|
|
97
|
+
throw new EventStreamParseError(`event-stream headers length ${headersLength} overflows message of ${totalLength} bytes`);
|
|
98
|
+
}
|
|
99
|
+
const headers = decodeHeaders(frame.subarray(PRELUDE_LENGTH, payloadStart));
|
|
100
|
+
const payload = Buffer.from(frame.subarray(payloadStart, payloadEnd));
|
|
101
|
+
return { headers, payload };
|
|
102
|
+
}
|
|
103
|
+
function decodeHeaders(section) {
|
|
104
|
+
const headers = {};
|
|
105
|
+
let offset = 0;
|
|
106
|
+
while (offset < section.length) {
|
|
107
|
+
const nameLength = section.readUInt8(offset);
|
|
108
|
+
offset += 1;
|
|
109
|
+
const name = section.toString("utf8", offset, offset + nameLength);
|
|
110
|
+
offset += nameLength;
|
|
111
|
+
const valueType = section.readUInt8(offset);
|
|
112
|
+
offset += 1;
|
|
113
|
+
switch (valueType) {
|
|
114
|
+
case 0: // true
|
|
115
|
+
headers[name] = true;
|
|
116
|
+
break;
|
|
117
|
+
case 1: // false
|
|
118
|
+
headers[name] = false;
|
|
119
|
+
break;
|
|
120
|
+
case 2: // byte
|
|
121
|
+
headers[name] = section.readInt8(offset);
|
|
122
|
+
offset += 1;
|
|
123
|
+
break;
|
|
124
|
+
case 3: // short
|
|
125
|
+
headers[name] = section.readInt16BE(offset);
|
|
126
|
+
offset += 2;
|
|
127
|
+
break;
|
|
128
|
+
case 4: // integer
|
|
129
|
+
headers[name] = section.readInt32BE(offset);
|
|
130
|
+
offset += 4;
|
|
131
|
+
break;
|
|
132
|
+
case 5: // long
|
|
133
|
+
headers[name] = Number(section.readBigInt64BE(offset));
|
|
134
|
+
offset += 8;
|
|
135
|
+
break;
|
|
136
|
+
case 6: {
|
|
137
|
+
// byte array
|
|
138
|
+
const len = section.readUInt16BE(offset);
|
|
139
|
+
offset += 2;
|
|
140
|
+
headers[name] = Buffer.from(section.subarray(offset, offset + len));
|
|
141
|
+
offset += len;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 7: {
|
|
145
|
+
// string
|
|
146
|
+
const len = section.readUInt16BE(offset);
|
|
147
|
+
offset += 2;
|
|
148
|
+
headers[name] = section.toString("utf8", offset, offset + len);
|
|
149
|
+
offset += len;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 8: // timestamp (epoch millis)
|
|
153
|
+
headers[name] = Number(section.readBigInt64BE(offset));
|
|
154
|
+
offset += 8;
|
|
155
|
+
break;
|
|
156
|
+
case 9: // uuid
|
|
157
|
+
headers[name] = Buffer.from(section.subarray(offset, offset + 16)).toString("hex");
|
|
158
|
+
offset += 16;
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
throw new EventStreamParseError(`unknown event-stream header value type ${valueType}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return headers;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Encode a single event-stream message. Used by tests to build fixtures, but also
|
|
168
|
+
* generally useful. Header values are emitted as strings (type 7) which matches
|
|
169
|
+
* how CodeWhisperer tags `:event-type`, `:content-type`, and `:message-type`.
|
|
170
|
+
*/
|
|
171
|
+
export function encodeEventStreamMessage(headers, payload) {
|
|
172
|
+
const headerChunks = [];
|
|
173
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
174
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
175
|
+
const valueBuf = Buffer.from(value, "utf8");
|
|
176
|
+
const head = Buffer.alloc(1 + nameBuf.length + 1 + 2);
|
|
177
|
+
let o = 0;
|
|
178
|
+
head.writeUInt8(nameBuf.length, o);
|
|
179
|
+
o += 1;
|
|
180
|
+
nameBuf.copy(head, o);
|
|
181
|
+
o += nameBuf.length;
|
|
182
|
+
head.writeUInt8(7, o); // string type
|
|
183
|
+
o += 1;
|
|
184
|
+
head.writeUInt16BE(valueBuf.length, o);
|
|
185
|
+
headerChunks.push(Buffer.concat([head, valueBuf]));
|
|
186
|
+
}
|
|
187
|
+
const headerSection = Buffer.concat(headerChunks);
|
|
188
|
+
const totalLength = PRELUDE_LENGTH + headerSection.length + payload.length + MESSAGE_CRC_LENGTH;
|
|
189
|
+
const message = Buffer.alloc(totalLength);
|
|
190
|
+
message.writeUInt32BE(totalLength, 0);
|
|
191
|
+
message.writeUInt32BE(headerSection.length, 4);
|
|
192
|
+
const preludeCrc = crc32(message, 0, 8);
|
|
193
|
+
message.writeUInt32BE(preludeCrc, 8);
|
|
194
|
+
headerSection.copy(message, PRELUDE_LENGTH);
|
|
195
|
+
payload.copy(message, PRELUDE_LENGTH + headerSection.length);
|
|
196
|
+
const messageCrc = crc32(message, 0, totalLength - MESSAGE_CRC_LENGTH);
|
|
197
|
+
message.writeUInt32BE(messageCrc, totalLength - MESSAGE_CRC_LENGTH);
|
|
198
|
+
return message;
|
|
199
|
+
}
|
|
200
|
+
/** Parse a header-tagged JSON payload, returning undefined on non-JSON payloads. */
|
|
201
|
+
export function decodeJsonPayload(message) {
|
|
202
|
+
if (message.payload.length === 0) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(message.payload.toString("utf8"));
|
|
207
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
208
|
+
? parsed
|
|
209
|
+
: undefined;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Convenience accessor for the `:event-type` header CodeWhisperer sets on each frame. */
|
|
216
|
+
export function eventType(message) {
|
|
217
|
+
const value = message.headers[":event-type"];
|
|
218
|
+
return typeof value === "string" ? value : undefined;
|
|
219
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { EventStreamParser, EventStreamParseError, crc32, decodeJsonPayload, encodeEventStreamMessage, eventType, parseEventStream, } from "./kiro-eventstream.js";
|
|
4
|
+
function assistantFrame(content) {
|
|
5
|
+
return encodeEventStreamMessage({
|
|
6
|
+
":event-type": "assistantResponseEvent",
|
|
7
|
+
":content-type": "application/json",
|
|
8
|
+
":message-type": "event",
|
|
9
|
+
}, Buffer.from(JSON.stringify({ content }), "utf8"));
|
|
10
|
+
}
|
|
11
|
+
test("crc32 matches the known check value for 'The quick brown fox...'", () => {
|
|
12
|
+
// IEEE CRC32 of this canonical string is 0x414fa339.
|
|
13
|
+
const value = crc32(Buffer.from("The quick brown fox jumps over the lazy dog", "utf8"));
|
|
14
|
+
assert.equal(value, 0x414fa339);
|
|
15
|
+
});
|
|
16
|
+
test("round-trips a single encoded message", () => {
|
|
17
|
+
const frame = assistantFrame("hello");
|
|
18
|
+
const messages = parseEventStream(frame);
|
|
19
|
+
assert.equal(messages.length, 1);
|
|
20
|
+
assert.equal(eventType(messages[0]), "assistantResponseEvent");
|
|
21
|
+
assert.equal(messages[0].headers[":content-type"], "application/json");
|
|
22
|
+
assert.deepEqual(decodeJsonPayload(messages[0]), { content: "hello" });
|
|
23
|
+
});
|
|
24
|
+
test("parses multiple concatenated messages", () => {
|
|
25
|
+
const frames = Buffer.concat([
|
|
26
|
+
assistantFrame("alpha"),
|
|
27
|
+
assistantFrame("beta"),
|
|
28
|
+
assistantFrame("gamma"),
|
|
29
|
+
]);
|
|
30
|
+
const messages = parseEventStream(frames);
|
|
31
|
+
assert.deepEqual(messages.map((m) => decodeJsonPayload(m)?.content), ["alpha", "beta", "gamma"]);
|
|
32
|
+
});
|
|
33
|
+
test("incremental parser buffers partial messages across chunks", () => {
|
|
34
|
+
const frame = assistantFrame("streamed");
|
|
35
|
+
const parser = new EventStreamParser();
|
|
36
|
+
// Feed the message one byte at a time; nothing should complete until the last byte.
|
|
37
|
+
let completed = 0;
|
|
38
|
+
for (let i = 0; i < frame.length; i += 1) {
|
|
39
|
+
const out = parser.push(frame.subarray(i, i + 1));
|
|
40
|
+
completed += out.length;
|
|
41
|
+
if (i < frame.length - 1) {
|
|
42
|
+
assert.equal(out.length, 0, `unexpected completion at byte ${i}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
assert.equal(completed, 1);
|
|
46
|
+
assert.equal(parser.pendingBytes(), 0);
|
|
47
|
+
});
|
|
48
|
+
test("incremental parser handles a chunk that spans a message boundary", () => {
|
|
49
|
+
const a = assistantFrame("first");
|
|
50
|
+
const b = assistantFrame("second");
|
|
51
|
+
const combined = Buffer.concat([a, b]);
|
|
52
|
+
const parser = new EventStreamParser();
|
|
53
|
+
const firstHalf = combined.subarray(0, a.length + 5);
|
|
54
|
+
const secondHalf = combined.subarray(a.length + 5);
|
|
55
|
+
const out1 = parser.push(firstHalf);
|
|
56
|
+
assert.equal(out1.length, 1);
|
|
57
|
+
assert.equal(decodeJsonPayload(out1[0])?.content, "first");
|
|
58
|
+
const out2 = parser.push(secondHalf);
|
|
59
|
+
assert.equal(out2.length, 1);
|
|
60
|
+
assert.equal(decodeJsonPayload(out2[0])?.content, "second");
|
|
61
|
+
});
|
|
62
|
+
test("tolerates a corrupted prelude CRC (frames by length, not checksum)", () => {
|
|
63
|
+
const frame = assistantFrame("crc-tolerant");
|
|
64
|
+
// Corrupt only the prelude CRC bytes (offset 8-11), leaving the lengths intact.
|
|
65
|
+
// 9router frames purely by the length prefix, so this must still decode.
|
|
66
|
+
frame.writeUInt32BE((frame.readUInt32BE(8) ^ 0xffffffff) >>> 0, 8);
|
|
67
|
+
const messages = parseEventStream(frame);
|
|
68
|
+
assert.equal(messages.length, 1);
|
|
69
|
+
assert.deepEqual(decodeJsonPayload(messages[0]), { content: "crc-tolerant" });
|
|
70
|
+
});
|
|
71
|
+
test("parseEventStream rejects a trailing incomplete message", () => {
|
|
72
|
+
const frame = assistantFrame("partial");
|
|
73
|
+
assert.throws(() => parseEventStream(frame.subarray(0, frame.length - 3)), (error) => error instanceof EventStreamParseError);
|
|
74
|
+
});
|
|
75
|
+
test("decodeJsonPayload returns undefined for non-JSON payloads", () => {
|
|
76
|
+
const frame = encodeEventStreamMessage({ ":event-type": "raw" }, Buffer.from("not json", "utf8"));
|
|
77
|
+
const [message] = parseEventStream(frame);
|
|
78
|
+
assert.equal(decodeJsonPayload(message), undefined);
|
|
79
|
+
});
|