@poppykit/client 0.0.0-stage → 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/API.md +25 -0
- package/AUTH.md +51 -0
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +50 -0
- package/DELIVERY.md +113 -0
- package/INTEGRATIONS.md +87 -0
- package/LICENSE +21 -0
- package/PROVIDER.md +99 -0
- package/README.md +123 -2
- package/RELEASING.md +193 -0
- package/SECURITY.md +13 -0
- package/STORAGE.md +99 -0
- package/dist/auth/challenge.d.ts +19 -0
- package/dist/auth/challenge.js +68 -0
- package/dist/auth/client-auth.d.ts +297 -0
- package/dist/auth/client-auth.js +1583 -0
- package/dist/auth/ownership.d.ts +9 -0
- package/dist/auth/ownership.js +23 -0
- package/dist/auth/sqlite-store.d.ts +14 -0
- package/dist/auth/sqlite-store.js +123 -0
- package/dist/auth/store.d.ts +19 -0
- package/dist/auth/store.js +1 -0
- package/dist/conversation-store.d.ts +15 -0
- package/dist/conversation-store.js +1 -0
- package/dist/conversation.d.ts +118 -0
- package/dist/conversation.js +404 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +7 -0
- package/dist/integrations.d.ts +30 -0
- package/dist/integrations.js +58 -0
- package/dist/memory.d.ts +15 -0
- package/dist/memory.js +32 -0
- package/dist/network/discovery.d.ts +101 -0
- package/dist/network/discovery.js +276 -0
- package/dist/network/transport.d.ts +31 -0
- package/dist/network/transport.js +305 -0
- package/dist/protocol/sse.d.ts +15 -0
- package/dist/protocol/sse.js +90 -0
- package/dist/protocol/wire.d.ts +128 -0
- package/dist/protocol/wire.js +278 -0
- package/dist/provider-sqlite.d.ts +8 -0
- package/dist/provider-sqlite.js +30 -0
- package/dist/provider.d.ts +121 -0
- package/dist/provider.js +342 -0
- package/dist/sqlite-init.d.ts +2 -0
- package/dist/sqlite-init.js +17 -0
- package/dist/sqlite.d.ts +20 -0
- package/dist/sqlite.js +80 -0
- package/examples/quickstart.mjs +19 -0
- package/package.json +72 -4
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { request as httpsRequest, Agent } from 'node:https';
|
|
2
|
+
import { connect, rootCertificates } from 'node:tls';
|
|
3
|
+
import { lookup } from 'node:dns';
|
|
4
|
+
import { BlockList, isIP } from 'node:net';
|
|
5
|
+
export class TransportError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code) {
|
|
8
|
+
super(`Transport ${code}`);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = 'TransportError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function secureUrl(value) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new TransportError('invalid_url');
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hash)
|
|
22
|
+
throw new TransportError('invalid_url');
|
|
23
|
+
return url;
|
|
24
|
+
}
|
|
25
|
+
function normalizedAddress(value) { return value.replace(/^\[|\]$/g, '').toLowerCase(); }
|
|
26
|
+
const blockedV4 = new BlockList();
|
|
27
|
+
for (const [network, prefix] of [
|
|
28
|
+
['0.0.0.0', 8], // "this" network
|
|
29
|
+
['10.0.0.0', 8], // private
|
|
30
|
+
['100.64.0.0', 10], // carrier-grade NAT
|
|
31
|
+
['127.0.0.0', 8], // loopback
|
|
32
|
+
['169.254.0.0', 16], // link-local
|
|
33
|
+
['172.16.0.0', 12], // private
|
|
34
|
+
['192.0.0.0', 24], // IETF protocol assignments
|
|
35
|
+
['192.0.2.0', 24], // documentation
|
|
36
|
+
['192.88.99.0', 24], // 6to4 relay anycast
|
|
37
|
+
['192.168.0.0', 16], // private
|
|
38
|
+
['198.18.0.0', 15], // benchmarking
|
|
39
|
+
['198.51.100.0', 24], // documentation
|
|
40
|
+
['203.0.113.0', 24], // documentation
|
|
41
|
+
['224.0.0.0', 3], // multicast and reserved (224.0.0.0 to 255.255.255.255)
|
|
42
|
+
])
|
|
43
|
+
blockedV4.addSubnet(network, prefix, 'ipv4');
|
|
44
|
+
// IPv6 is allow-listed: only global unicast 2000::/3 minus special-purpose ranges.
|
|
45
|
+
const blockedV6 = new BlockList();
|
|
46
|
+
for (const [network, prefix] of [
|
|
47
|
+
['2001::', 23], // IETF protocol assignments (Teredo etc.)
|
|
48
|
+
['2001:db8::', 32], // documentation
|
|
49
|
+
['2002::', 16], // 6to4
|
|
50
|
+
['3fff::', 20], // documentation
|
|
51
|
+
])
|
|
52
|
+
blockedV6.addSubnet(network, prefix, 'ipv6');
|
|
53
|
+
const globalV6 = new BlockList();
|
|
54
|
+
globalV6.addSubnet('2000::', 3, 'ipv6');
|
|
55
|
+
export function isPublicAddress(value) {
|
|
56
|
+
const address = normalizedAddress(value);
|
|
57
|
+
if (isIP(address) === 4)
|
|
58
|
+
return !blockedV4.check(address, 'ipv4');
|
|
59
|
+
// Zoned addresses (fe80::1%eth0) fail closed.
|
|
60
|
+
if (isIP(address) !== 6 || address.includes('%'))
|
|
61
|
+
return false;
|
|
62
|
+
return globalV6.check(address, 'ipv6') && !blockedV6.check(address, 'ipv6');
|
|
63
|
+
}
|
|
64
|
+
export function createNodeTransport(options = {}) {
|
|
65
|
+
const limits = { timeoutMs: options.timeoutMs ?? 15000, maxResponseBytes: options.maxResponseBytes ?? 1048576, maxRequestBytes: options.maxRequestBytes ?? 1048576, maxRedirects: options.maxRedirects ?? 5, maxPools: options.maxPools ?? 8, maxSockets: options.maxSockets ?? 2, maxRequests: options.maxRequests ?? 32, maxStreamBytes: options.maxStreamBytes ?? 16777216, maxStreamMs: options.maxStreamMs ?? 300000 };
|
|
66
|
+
for (const [name, value] of Object.entries(limits))
|
|
67
|
+
if (!Number.isSafeInteger(value) || value < (name === 'maxRedirects' ? 0 : 1) || (name.endsWith('Ms') && value > 2147483647))
|
|
68
|
+
throw new TypeError('Invalid transport limit');
|
|
69
|
+
const pools = new Map();
|
|
70
|
+
let active = 0, closed = false;
|
|
71
|
+
const connecting = new Set();
|
|
72
|
+
const shutdown = new AbortController();
|
|
73
|
+
const allowed = (hostname, address) => isPublicAddress(address) || !!options.localExceptions?.some(entry => entry.hostname.toLowerCase() === hostname.toLowerCase() && entry.addresses.some(ip => normalizedAddress(ip) === normalizedAddress(address)));
|
|
74
|
+
async function execute(request, settings = {}, streaming = false) {
|
|
75
|
+
if (closed)
|
|
76
|
+
throw new TransportError('closed');
|
|
77
|
+
if (active >= limits.maxRequests)
|
|
78
|
+
throw new TransportError('overloaded');
|
|
79
|
+
const discovery = settings.mode === 'discovery';
|
|
80
|
+
const timeoutMs = streaming ? limits.maxStreamMs : (discovery ? Math.min(limits.timeoutMs, 5000) : limits.timeoutMs) + (settings.waitMs ?? 0);
|
|
81
|
+
if (!Number.isSafeInteger(settings.waitMs ?? 0) || (settings.waitMs ?? 0) < 0 || timeoutMs > 2147483647)
|
|
82
|
+
throw new TypeError('Invalid transport wait');
|
|
83
|
+
active++;
|
|
84
|
+
const controller = new AbortController(), signal = AbortSignal.any([request.signal, controller.signal, shutdown.signal]);
|
|
85
|
+
const timer = setTimeout(() => controller.abort(new TransportError('timeout')), timeoutMs);
|
|
86
|
+
let transferred = false, finished = false;
|
|
87
|
+
const finish = () => { if (!finished) {
|
|
88
|
+
finished = true;
|
|
89
|
+
active--;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
} };
|
|
92
|
+
try {
|
|
93
|
+
signal.throwIfAborted();
|
|
94
|
+
let body;
|
|
95
|
+
if (request.body) {
|
|
96
|
+
const reader = request.body.getReader(), chunks = [];
|
|
97
|
+
let length = 0;
|
|
98
|
+
const abort = () => { void reader.cancel(signal.reason); };
|
|
99
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
100
|
+
try {
|
|
101
|
+
for (;;) {
|
|
102
|
+
signal.throwIfAborted();
|
|
103
|
+
const item = await reader.read();
|
|
104
|
+
if (item.done)
|
|
105
|
+
break;
|
|
106
|
+
length += item.value.length;
|
|
107
|
+
if (length > limits.maxRequestBytes)
|
|
108
|
+
throw new TransportError('body_limit');
|
|
109
|
+
chunks.push(item.value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
signal.removeEventListener('abort', abort);
|
|
114
|
+
await reader.cancel().catch(() => { });
|
|
115
|
+
reader.releaseLock();
|
|
116
|
+
}
|
|
117
|
+
body = Buffer.concat(chunks);
|
|
118
|
+
}
|
|
119
|
+
let url = secureUrl(request.url);
|
|
120
|
+
if (request.headers.has('host') && request.headers.get('host') !== url.host)
|
|
121
|
+
throw new TransportError('invalid_url');
|
|
122
|
+
if (discovery && (request.method !== 'GET' || body || ['authorization', 'dpop', 'cookie', 'proxy-authorization', 'host'].some(key => request.headers.has(key))))
|
|
123
|
+
throw new TransportError('invalid_url');
|
|
124
|
+
for (let redirects = 0;; redirects++) {
|
|
125
|
+
signal.throwIfAborted();
|
|
126
|
+
const hostname = normalizedAddress(url.hostname);
|
|
127
|
+
const addresses = await new Promise((resolve, reject) => {
|
|
128
|
+
const abort = () => reject(signal.reason);
|
|
129
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
130
|
+
const done = (error, found) => { signal.removeEventListener('abort', abort); error ? reject(new TransportError('network_error')) : resolve(found); };
|
|
131
|
+
if (isIP(hostname))
|
|
132
|
+
done(null, [{ address: hostname, family: isIP(hostname) }]);
|
|
133
|
+
else
|
|
134
|
+
lookup(hostname, { all: true, verbatim: true }, done);
|
|
135
|
+
});
|
|
136
|
+
signal.throwIfAborted();
|
|
137
|
+
const approved = addresses.filter(item => allowed(hostname, item.address)).sort((a, b) => a.address.localeCompare(b.address));
|
|
138
|
+
if (!approved.length)
|
|
139
|
+
throw new TransportError('blocked_address');
|
|
140
|
+
const mode = discovery ? 'discovery' : streaming ? 'stream' : 'endpoint';
|
|
141
|
+
const poolKey = JSON.stringify([url.origin, mode, approved, options.ca]);
|
|
142
|
+
// Recheck DNS/policy before every assignment, including free sockets. Retire incompatible pools.
|
|
143
|
+
for (const [key, pool] of pools)
|
|
144
|
+
if (pool.origin === url.origin && pool.mode === mode && key !== poolKey) {
|
|
145
|
+
pool.agent.destroy();
|
|
146
|
+
pools.delete(key);
|
|
147
|
+
}
|
|
148
|
+
let pool = pools.get(poolKey);
|
|
149
|
+
if (!pool) {
|
|
150
|
+
if (pools.size >= limits.maxPools) {
|
|
151
|
+
const idle = [...pools].find(([, value]) => value.active === 0);
|
|
152
|
+
if (!idle)
|
|
153
|
+
throw new TransportError('overloaded');
|
|
154
|
+
idle[1].agent.destroy();
|
|
155
|
+
pools.delete(idle[0]);
|
|
156
|
+
}
|
|
157
|
+
const agent = new Agent({ keepAlive: true, maxSockets: limits.maxSockets, maxTotalSockets: limits.maxSockets, maxFreeSockets: 1, timeout: limits.timeoutMs });
|
|
158
|
+
agent.createConnection = ((connectionOptions, callback) => {
|
|
159
|
+
const selected = approved[0];
|
|
160
|
+
const socket = connect({ ...connectionOptions, host: selected.address, servername: isIP(hostname) ? undefined : hostname, rejectUnauthorized: true, minVersion: 'TLSv1.2', ca: options.ca ? [...rootCertificates, options.ca] : undefined });
|
|
161
|
+
const connectSignal = connectionOptions.transportSignal;
|
|
162
|
+
const abortConnect = () => settle(new TransportError('network_error'));
|
|
163
|
+
connecting.add(socket);
|
|
164
|
+
socket.setTimeout?.(limits.timeoutMs, () => socket.destroy(new TransportError('timeout')));
|
|
165
|
+
let settled = false;
|
|
166
|
+
const settle = (error) => { if (settled)
|
|
167
|
+
return; settled = true; connectSignal?.removeEventListener('abort', abortConnect); connecting.delete(socket); socket.setTimeout?.(0); if (error) {
|
|
168
|
+
socket.destroy();
|
|
169
|
+
callback(error);
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
callback(null, socket); };
|
|
173
|
+
connectSignal?.addEventListener('abort', abortConnect, { once: true });
|
|
174
|
+
if (connectSignal?.aborted)
|
|
175
|
+
queueMicrotask(abortConnect);
|
|
176
|
+
socket.once('error', error => settle(error instanceof TransportError ? error : new TransportError('network_error')));
|
|
177
|
+
socket.once('secureConnect', () => settle(socket.remoteAddress && approved.some(item => normalizedAddress(item.address) === normalizedAddress(socket.remoteAddress)) && allowed(hostname, socket.remoteAddress) ? null : new TransportError('blocked_address')));
|
|
178
|
+
return undefined;
|
|
179
|
+
});
|
|
180
|
+
agent.reuseSocket = (duplex, outgoing) => {
|
|
181
|
+
const socket = duplex;
|
|
182
|
+
if (!socket.remoteAddress || !approved.some(item => normalizedAddress(item.address) === normalizedAddress(socket.remoteAddress)) || !allowed(hostname, socket.remoteAddress)) {
|
|
183
|
+
const error = new TransportError('blocked_address');
|
|
184
|
+
socket.destroy(error);
|
|
185
|
+
outgoing.destroy(error);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
Agent.prototype.reuseSocket.call(agent, socket, outgoing);
|
|
189
|
+
};
|
|
190
|
+
pool = { agent, active: 0, origin: url.origin, mode };
|
|
191
|
+
pools.set(poolKey, pool);
|
|
192
|
+
}
|
|
193
|
+
const selectedPool = pool;
|
|
194
|
+
// No hidden Agent queue: callers retry overload, after fresh DNS and policy validation.
|
|
195
|
+
if (selectedPool.active >= limits.maxSockets)
|
|
196
|
+
throw new TransportError('overloaded');
|
|
197
|
+
selectedPool.active++;
|
|
198
|
+
let releaseCalled = false;
|
|
199
|
+
const release = () => { if (!releaseCalled) {
|
|
200
|
+
releaseCalled = true;
|
|
201
|
+
selectedPool.active--;
|
|
202
|
+
} };
|
|
203
|
+
const response = await new Promise((resolve, reject) => {
|
|
204
|
+
const failed = (error) => {
|
|
205
|
+
const retired = () => { release(); reject(error); }, socket = outgoing.socket;
|
|
206
|
+
// A destroyed socket can still count against the Agent until its close event.
|
|
207
|
+
if (socket && !socket.closed)
|
|
208
|
+
socket.once('close', retired);
|
|
209
|
+
else if (!outgoing.closed)
|
|
210
|
+
outgoing.once('close', retired);
|
|
211
|
+
else
|
|
212
|
+
retired();
|
|
213
|
+
};
|
|
214
|
+
const outgoing = httpsRequest(url, { method: request.method, headers: Object.fromEntries(request.headers), agent: selectedPool.agent, signal, ...{ transportSignal: signal } }, incoming => {
|
|
215
|
+
const headers = new Headers();
|
|
216
|
+
for (let i = 0; i < incoming.rawHeaders.length; i += 2)
|
|
217
|
+
headers.append(incoming.rawHeaders[i], incoming.rawHeaders[i + 1]);
|
|
218
|
+
const status = incoming.statusCode ?? 502;
|
|
219
|
+
if (status < 200 || status > 599) {
|
|
220
|
+
incoming.destroy();
|
|
221
|
+
reject(new TransportError('network_error'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const noBody = [204, 205, 304].includes(status) || request.method === 'HEAD';
|
|
225
|
+
if (streaming && !noBody) {
|
|
226
|
+
incoming.pause();
|
|
227
|
+
let bytes = 0;
|
|
228
|
+
const iterator = incoming[Symbol.asyncIterator]();
|
|
229
|
+
const retire = () => new Promise(done => { const socket = incoming.socket; if (socket && !socket.closed)
|
|
230
|
+
socket.once('close', done);
|
|
231
|
+
else
|
|
232
|
+
done(); incoming.destroy(); });
|
|
233
|
+
const ended = () => { const socket = incoming.socket, done = () => { release(); finish(); }; if (socket?.destroyed && !socket.closed)
|
|
234
|
+
socket.once('close', done);
|
|
235
|
+
else
|
|
236
|
+
done(); };
|
|
237
|
+
const stream = new ReadableStream({
|
|
238
|
+
async pull(target) { try {
|
|
239
|
+
const item = await iterator.next();
|
|
240
|
+
if (item.done) {
|
|
241
|
+
target.close();
|
|
242
|
+
release();
|
|
243
|
+
finish();
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
bytes += item.value.length;
|
|
247
|
+
if (bytes > limits.maxStreamBytes)
|
|
248
|
+
throw new TransportError('body_limit');
|
|
249
|
+
target.enqueue(item.value);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
await retire();
|
|
254
|
+
release();
|
|
255
|
+
finish();
|
|
256
|
+
target.error(error);
|
|
257
|
+
} },
|
|
258
|
+
async cancel() { await retire(); await iterator.return?.(); release(); finish(); },
|
|
259
|
+
}, { highWaterMark: 0 });
|
|
260
|
+
incoming.once('close', ended);
|
|
261
|
+
transferred = true;
|
|
262
|
+
const result = new Response(stream, { status, headers });
|
|
263
|
+
Object.defineProperty(result, 'url', { value: url.href });
|
|
264
|
+
resolve(result);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const chunks = [];
|
|
268
|
+
let size = 0;
|
|
269
|
+
incoming.on('data', (chunk) => { size += chunk.length; if (size > (discovery ? Math.min(limits.maxResponseBytes, 102400) : limits.maxResponseBytes))
|
|
270
|
+
incoming.destroy(new TransportError('body_limit'));
|
|
271
|
+
else
|
|
272
|
+
chunks.push(chunk); });
|
|
273
|
+
incoming.on('error', failed);
|
|
274
|
+
incoming.on('end', () => { const result = new Response(noBody ? null : Buffer.concat(chunks), { status, headers }); Object.defineProperty(result, 'url', { value: url.href }); release(); resolve(result); });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
outgoing.on('error', failed);
|
|
278
|
+
outgoing.end(body);
|
|
279
|
+
}).catch(error => { release(); throw error; });
|
|
280
|
+
if (!discovery || ![301, 302, 303, 307, 308].includes(response.status))
|
|
281
|
+
return response;
|
|
282
|
+
if (redirects >= limits.maxRedirects)
|
|
283
|
+
throw new TransportError('redirect_limit');
|
|
284
|
+
const location = response.headers.get('location');
|
|
285
|
+
if (!location)
|
|
286
|
+
return response;
|
|
287
|
+
url = secureUrl(new URL(location, url).href);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (closed)
|
|
292
|
+
throw new TransportError('closed');
|
|
293
|
+
if (error instanceof TransportError)
|
|
294
|
+
throw error;
|
|
295
|
+
throw new TransportError(controller.signal.aborted ? 'timeout' : 'network_error');
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (!transferred)
|
|
299
|
+
finish();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { fetch: (request, settings) => execute(request, settings), stream: request => execute(request, {}, true), close() { closed = true; shutdown.abort(new TransportError('closed')); for (const socket of connecting)
|
|
303
|
+
socket.destroy(); connecting.clear(); for (const pool of pools.values())
|
|
304
|
+
pool.agent.destroy(); pools.clear(); } };
|
|
305
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConversationEvent } from './wire.js';
|
|
2
|
+
export type StreamFrame = {
|
|
3
|
+
kind: 'event';
|
|
4
|
+
event: ConversationEvent;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'delta';
|
|
7
|
+
messageId: string;
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
/** One bounded frame at a time. The caller controls reads by advancing the iterator. */
|
|
11
|
+
export declare function parseEventStream(body: ReadableStream<Uint8Array>, options?: {
|
|
12
|
+
maxFrameBytes?: number;
|
|
13
|
+
maxStreamBytes?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}): AsyncGenerator<StreamFrame>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { id, object, parseEvent, parseJson, string, ValidationError } from './wire.js';
|
|
2
|
+
/** One bounded frame at a time. The caller controls reads by advancing the iterator. */
|
|
3
|
+
export async function* parseEventStream(body, options = {}) {
|
|
4
|
+
const maxFrame = options.maxFrameBytes ?? 1048576, maxStream = options.maxStreamBytes ?? 16777216;
|
|
5
|
+
if (![maxFrame, maxStream].every(value => Number.isSafeInteger(value) && value > 0))
|
|
6
|
+
throw new TypeError('Invalid stream budget');
|
|
7
|
+
const reader = body.getReader(), decoder = new TextDecoder('utf-8', { fatal: true });
|
|
8
|
+
let buffer = '', bytes = 0, frameBytes = 0, data = [], eventType = '', eventId;
|
|
9
|
+
const abort = () => { void reader.cancel(options.signal?.reason); };
|
|
10
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
11
|
+
function frame() {
|
|
12
|
+
if (!data.length)
|
|
13
|
+
return;
|
|
14
|
+
const value = parseJson(data.join('\n'));
|
|
15
|
+
if (eventType === 'text-delta') {
|
|
16
|
+
if (eventId !== undefined)
|
|
17
|
+
throw new ValidationError('sse.delta.id');
|
|
18
|
+
const delta = object(value);
|
|
19
|
+
return { kind: 'delta', messageId: id(delta.message_id, 'message_id'), text: string(delta.text, 'text', false) };
|
|
20
|
+
}
|
|
21
|
+
const event = parseEvent(value);
|
|
22
|
+
if (eventId !== event.id)
|
|
23
|
+
throw new ValidationError('sse.id');
|
|
24
|
+
return { kind: 'event', event };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
for (;;) {
|
|
28
|
+
options.signal?.throwIfAborted();
|
|
29
|
+
const item = await reader.read();
|
|
30
|
+
options.signal?.throwIfAborted();
|
|
31
|
+
if (item.done) {
|
|
32
|
+
buffer += decoder.decode();
|
|
33
|
+
if (buffer.endsWith('\r'))
|
|
34
|
+
buffer += '\n';
|
|
35
|
+
else
|
|
36
|
+
break;
|
|
37
|
+
} // Discard an unterminated frame, but CR alone is a valid line ending.
|
|
38
|
+
bytes += item.value?.byteLength ?? 0;
|
|
39
|
+
if (bytes > maxStream)
|
|
40
|
+
throw new ValidationError('sse.stream.size');
|
|
41
|
+
if (!item.done)
|
|
42
|
+
buffer += decoder.decode(item.value, { stream: true });
|
|
43
|
+
for (;;) {
|
|
44
|
+
const end = buffer.search(/[\r\n]/);
|
|
45
|
+
if (end < 0)
|
|
46
|
+
break;
|
|
47
|
+
if (buffer[end] === '\r' && end === buffer.length - 1)
|
|
48
|
+
break;
|
|
49
|
+
const line = buffer.slice(0, end), width = buffer[end] === '\r' && buffer[end + 1] === '\n' ? 2 : 1;
|
|
50
|
+
buffer = buffer.slice(end + width);
|
|
51
|
+
frameBytes += Buffer.byteLength(line) + width;
|
|
52
|
+
if (frameBytes > maxFrame)
|
|
53
|
+
throw new ValidationError('sse.frame.size');
|
|
54
|
+
if (!line) {
|
|
55
|
+
const result = frame();
|
|
56
|
+
data = [];
|
|
57
|
+
eventType = '';
|
|
58
|
+
eventId = undefined;
|
|
59
|
+
frameBytes = 0;
|
|
60
|
+
if (result)
|
|
61
|
+
yield result;
|
|
62
|
+
}
|
|
63
|
+
else if (!line.startsWith(':')) {
|
|
64
|
+
const colon = line.indexOf(':'), field = colon < 0 ? line : line.slice(0, colon);
|
|
65
|
+
let value = colon < 0 ? '' : line.slice(colon + 1);
|
|
66
|
+
if (value.startsWith(' '))
|
|
67
|
+
value = value.slice(1);
|
|
68
|
+
if (field === 'data')
|
|
69
|
+
data.push(value);
|
|
70
|
+
else if (field === 'event')
|
|
71
|
+
eventType = value;
|
|
72
|
+
else if (field === 'id') {
|
|
73
|
+
if (value.includes('\0'))
|
|
74
|
+
throw new ValidationError('sse.id');
|
|
75
|
+
eventId = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (frameBytes + Buffer.byteLength(buffer) > maxFrame)
|
|
80
|
+
throw new ValidationError('sse.frame.size');
|
|
81
|
+
if (item.done)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
options.signal?.removeEventListener('abort', abort);
|
|
87
|
+
await reader.cancel().catch(() => { });
|
|
88
|
+
reader.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
2
|
+
[key: string]: Json;
|
|
3
|
+
};
|
|
4
|
+
export type JsonObject = {
|
|
5
|
+
[key: string]: Json;
|
|
6
|
+
};
|
|
7
|
+
export type Sender = 'agent' | 'human';
|
|
8
|
+
export type Status = 'working' | 'idle' | 'queued' | 'closed';
|
|
9
|
+
export interface Message {
|
|
10
|
+
id: string;
|
|
11
|
+
sender: Sender;
|
|
12
|
+
text?: string;
|
|
13
|
+
data?: JsonObject;
|
|
14
|
+
context?: JsonObject;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface RecordedMessage extends Message {
|
|
18
|
+
role: 'user' | 'company';
|
|
19
|
+
}
|
|
20
|
+
export interface ConversationRequest {
|
|
21
|
+
message: Message;
|
|
22
|
+
parent_conversation_id?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface ConversationState {
|
|
26
|
+
conversation_id: string;
|
|
27
|
+
status: Status;
|
|
28
|
+
responder: Sender;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface ConversationEvent {
|
|
32
|
+
id: string;
|
|
33
|
+
type: string;
|
|
34
|
+
created_at: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
export type KnownEvent = (ConversationEvent & {
|
|
38
|
+
type: 'message';
|
|
39
|
+
message: RecordedMessage;
|
|
40
|
+
}) | (ConversationEvent & {
|
|
41
|
+
type: 'state';
|
|
42
|
+
status: Status;
|
|
43
|
+
responder: Sender;
|
|
44
|
+
}) | (ConversationEvent & {
|
|
45
|
+
type: 'authorization';
|
|
46
|
+
error: 'sign_in_required';
|
|
47
|
+
scope?: string;
|
|
48
|
+
}) | (ConversationEvent & {
|
|
49
|
+
type: 'authorization';
|
|
50
|
+
error: 'insufficient_scope';
|
|
51
|
+
scope: string;
|
|
52
|
+
}) | (ConversationEvent & {
|
|
53
|
+
type: 'user_requested';
|
|
54
|
+
reason: string;
|
|
55
|
+
}) | (ConversationEvent & {
|
|
56
|
+
type: 'direct_opened' | 'direct_closed';
|
|
57
|
+
conversation_id: string;
|
|
58
|
+
});
|
|
59
|
+
export interface EventPage extends ConversationState {
|
|
60
|
+
events: ConversationEvent[];
|
|
61
|
+
cursor: string | null;
|
|
62
|
+
has_more: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface TokenResponse {
|
|
65
|
+
access_token: string;
|
|
66
|
+
token_type: 'DPoP' | 'Bearer';
|
|
67
|
+
expires_in: number;
|
|
68
|
+
scope: string;
|
|
69
|
+
session_id: string;
|
|
70
|
+
signed_in: boolean;
|
|
71
|
+
refresh_token?: string;
|
|
72
|
+
refresh_token_expires_in?: number;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
export interface ProtocolErrorBody {
|
|
76
|
+
error: string;
|
|
77
|
+
error_description?: string;
|
|
78
|
+
error_uri?: string;
|
|
79
|
+
extension?: string;
|
|
80
|
+
conversation_id?: string;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
export declare class ValidationError extends Error {
|
|
84
|
+
readonly field: string;
|
|
85
|
+
constructor(field: string);
|
|
86
|
+
}
|
|
87
|
+
export declare function validateJson(value: unknown): void;
|
|
88
|
+
export declare function object(value: unknown, field?: string): Record<string, unknown>;
|
|
89
|
+
export declare function string(value: unknown, field: string, nonempty?: boolean): string;
|
|
90
|
+
export declare function member(value: unknown, values: readonly unknown[], field: string): void;
|
|
91
|
+
export declare function id(value: unknown, field: string): string;
|
|
92
|
+
export declare function parseMessage(value: unknown, options?: {
|
|
93
|
+
recorded?: boolean;
|
|
94
|
+
company?: boolean;
|
|
95
|
+
}): Message | RecordedMessage;
|
|
96
|
+
export declare function parseConversationRequest(value: unknown): ConversationRequest;
|
|
97
|
+
export declare function parseConversationState(value: unknown): ConversationState;
|
|
98
|
+
export declare function parseEvent(value: unknown): ConversationEvent;
|
|
99
|
+
export declare function parseEventPage(value: unknown, requestCursor?: string): EventPage;
|
|
100
|
+
export declare function parseTokenResponse(value: unknown): TokenResponse;
|
|
101
|
+
export declare function parseProtocolError(value: unknown): ProtocolErrorBody;
|
|
102
|
+
export declare function parseReadOptions(url: URL, maxWaitSeconds: number): {
|
|
103
|
+
cursor?: string;
|
|
104
|
+
waitSeconds: number;
|
|
105
|
+
};
|
|
106
|
+
export declare function retryAfterMilliseconds(value: string | null, nowMs: number): number | undefined;
|
|
107
|
+
export interface AssertionClaims {
|
|
108
|
+
iss: string;
|
|
109
|
+
sub: string;
|
|
110
|
+
aud: string;
|
|
111
|
+
iat: number;
|
|
112
|
+
exp: number;
|
|
113
|
+
jti: string;
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
export interface DpopClaims {
|
|
117
|
+
jti: string;
|
|
118
|
+
htm: string;
|
|
119
|
+
htu: string;
|
|
120
|
+
iat: number;
|
|
121
|
+
ath?: string;
|
|
122
|
+
nonce?: string;
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
}
|
|
125
|
+
export declare function parseAssertionClaims(value: unknown): AssertionClaims;
|
|
126
|
+
export declare function parseDpopClaims(value: unknown): DpopClaims;
|
|
127
|
+
export declare function canonical(value: unknown): string;
|
|
128
|
+
export declare function parseJson(text: string): unknown;
|