@browserlessapi/inferlo 0.6.0 → 0.8.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 +9 -5
- package/dist/ai/ai-sdk.d.ts +48 -0
- package/dist/ai/ai-sdk.js +235 -0
- package/dist/ai/ai-sdk.js.map +1 -0
- package/dist/ai/index.d.ts +37 -3
- package/dist/ai/index.js +96 -7
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/public.d.ts +5 -1
- package/dist/ai/public.js +2 -0
- package/dist/ai/public.js.map +1 -1
- package/dist/ai/ui.d.ts +47 -0
- package/dist/ai/ui.js +33 -0
- package/dist/ai/ui.js.map +1 -0
- package/dist/app.d.ts +55 -4
- package/dist/app.js +439 -39
- package/dist/app.js.map +1 -1
- package/dist/astro.d.ts +60 -0
- package/dist/astro.js +68 -0
- package/dist/astro.js.map +1 -0
- package/dist/audit.d.ts +8 -1
- package/dist/audit.js +9 -2
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/authjs.d.ts +70 -0
- package/dist/authjs.js +94 -0
- package/dist/authjs.js.map +1 -0
- package/dist/azure.d.ts +66 -0
- package/dist/azure.js +119 -0
- package/dist/azure.js.map +1 -0
- package/dist/better-auth.d.ts +89 -0
- package/dist/better-auth.js +75 -0
- package/dist/better-auth.js.map +1 -0
- package/dist/cache.d.ts +13 -9
- package/dist/cache.js +11 -9
- package/dist/cache.js.map +1 -1
- package/dist/cli/main.js +5 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/client-ip.d.ts +41 -1
- package/dist/client-ip.js +85 -1
- package/dist/client-ip.js.map +1 -1
- package/dist/cross-site.d.ts +14 -0
- package/dist/cross-site.js +56 -0
- package/dist/cross-site.js.map +1 -0
- package/dist/db/memory.js +2 -1
- package/dist/db/memory.js.map +1 -1
- package/dist/db/postgres.js +23 -6
- package/dist/db/postgres.js.map +1 -1
- package/dist/db/table.d.ts +37 -5
- package/dist/db/table.js +96 -15
- package/dist/db/table.js.map +1 -1
- package/dist/db/types.d.ts +2 -0
- package/dist/describe.d.ts +23 -5
- package/dist/describe.js +20 -2
- package/dist/describe.js.map +1 -1
- package/dist/disk-storage.d.ts +6 -0
- package/dist/disk-storage.js +96 -0
- package/dist/disk-storage.js.map +1 -0
- package/dist/events.d.ts +134 -0
- package/dist/events.js +196 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +23 -0
- package/dist/files.js +168 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +26 -8
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +56 -10
- package/dist/jobs.js +71 -13
- package/dist/jobs.js.map +1 -1
- package/dist/jwt.d.ts +83 -0
- package/dist/jwt.js +312 -0
- package/dist/jwt.js.map +1 -0
- package/dist/langchain.d.ts +58 -0
- package/dist/langchain.js +220 -0
- package/dist/langchain.js.map +1 -0
- package/dist/model.d.ts +23 -6
- package/dist/model.js +41 -7
- package/dist/model.js.map +1 -1
- package/dist/oidc.d.ts +70 -0
- package/dist/oidc.js +379 -0
- package/dist/oidc.js.map +1 -0
- package/dist/openapi.js +17 -2
- package/dist/openapi.js.map +1 -1
- package/dist/passport.d.ts +54 -0
- package/dist/passport.js +68 -0
- package/dist/passport.js.map +1 -0
- package/dist/postgres-js.d.ts +39 -0
- package/dist/postgres-js.js +71 -0
- package/dist/postgres-js.js.map +1 -0
- package/dist/rate-limit.d.ts +5 -3
- package/dist/rate-limit.js +3 -3
- package/dist/rate-limit.js.map +1 -1
- package/dist/redis-events.d.ts +27 -0
- package/dist/redis-events.js +163 -0
- package/dist/redis-events.js.map +1 -0
- package/dist/redis-jobs.d.ts +24 -0
- package/dist/redis-jobs.js +203 -0
- package/dist/redis-jobs.js.map +1 -0
- package/dist/redis-sockets.d.ts +24 -0
- package/dist/redis-sockets.js +75 -0
- package/dist/redis-sockets.js.map +1 -0
- package/dist/redis.d.ts +30 -0
- package/dist/redis.js +106 -0
- package/dist/redis.js.map +1 -0
- package/dist/response-body.d.ts +6 -0
- package/dist/response-body.js +7 -0
- package/dist/response-body.js.map +1 -0
- package/dist/route.d.ts +117 -3
- package/dist/route.js +27 -0
- package/dist/route.js.map +1 -1
- package/dist/schema.d.ts +23 -3
- package/dist/schema.js +70 -0
- package/dist/schema.js.map +1 -1
- package/dist/serve.d.ts +24 -1
- package/dist/serve.js +200 -8
- package/dist/serve.js.map +1 -1
- package/dist/service-client.d.ts +91 -0
- package/dist/service-client.js +177 -0
- package/dist/service-client.js.map +1 -0
- package/dist/sessions.d.ts +2 -0
- package/dist/sessions.js +36 -4
- package/dist/sessions.js.map +1 -1
- package/dist/sockets.d.ts +58 -0
- package/dist/sockets.js +111 -0
- package/dist/sockets.js.map +1 -0
- package/dist/storage.d.ts +47 -0
- package/dist/storage.js +123 -0
- package/dist/storage.js.map +1 -0
- package/dist/tracing.d.ts +7 -3
- package/dist/tracing.js +8 -2
- package/dist/tracing.js.map +1 -1
- package/dist/websocket.d.ts +81 -0
- package/dist/websocket.js +251 -0
- package/dist/websocket.js.map +1 -0
- package/llms.txt +2 -0
- package/package.json +77 -5
- package/templates/app/AGENTS.md +9 -5
- package/templates/app/test/notes.test.ts +2 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket plumbing shared by the app (src/app.ts) and the servers (src/serve.ts): the per-request upgrader a server
|
|
3
|
+
* registers, the record of upgraded requests, Deno's built-in upgrade, and the RFC 6455 frame reader and writer used
|
|
4
|
+
* on Node.js, where the runtime has no WebSocket server of its own. Web APIs only.
|
|
5
|
+
*/
|
|
6
|
+
/** One open connection, as the app sees it: send a text message, or close with a code and reason. */
|
|
7
|
+
export interface SocketTransport {
|
|
8
|
+
send(text: string): void;
|
|
9
|
+
close(code: number, reason: string): void;
|
|
10
|
+
}
|
|
11
|
+
/** What the app does with a connection's events. */
|
|
12
|
+
export interface SocketListener {
|
|
13
|
+
message(data: string | Uint8Array): void;
|
|
14
|
+
close(code: number, reason: string): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Accepts the WebSocket upgrade of one request: connects the app's listener to the new connection and returns the
|
|
18
|
+
* response to answer with (Deno), or undefined when the server answers the handshake itself (Node.js, Bun).
|
|
19
|
+
*/
|
|
20
|
+
export type Upgrader = (connect: (transport: SocketTransport) => SocketListener) => Promise<Response | undefined>;
|
|
21
|
+
/** Set by serve() for each request it can upgrade (Node.js upgrade requests, every Bun request). */
|
|
22
|
+
export declare const upgraders: WeakMap<Request, Upgrader>;
|
|
23
|
+
/** Requests the app upgraded, with Deno's upgrade response (null on Node.js and Bun, which answer it themselves). */
|
|
24
|
+
export declare const upgrades: WeakMap<Request, Response | null>;
|
|
25
|
+
/**
|
|
26
|
+
* On Deno, a request from Deno.serve (or deno serve), which passes its handler info with a remoteAddr to app.fetch,
|
|
27
|
+
* can be upgraded with Deno.upgradeWebSocket. Requests made any other way (app.request) cannot.
|
|
28
|
+
*/
|
|
29
|
+
export declare function denoUpgrader(req: Request, info: unknown): Upgrader | undefined;
|
|
30
|
+
type BunSocket = {
|
|
31
|
+
data: {
|
|
32
|
+
connect: (transport: SocketTransport) => SocketListener;
|
|
33
|
+
listener?: SocketListener;
|
|
34
|
+
};
|
|
35
|
+
send(text: string): void;
|
|
36
|
+
close(code?: number, reason?: string): void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The websocket handlers for Bun.serve, so a Bun server that runs app.fetch serves the app's WebSocket routes:
|
|
40
|
+
* Bun.serve({ fetch: app.fetch, websocket: bunWebSocket }). serve() and app.listen() on Bun use them already.
|
|
41
|
+
*/
|
|
42
|
+
export declare const bunWebSocket: {
|
|
43
|
+
open(ws: BunSocket): void;
|
|
44
|
+
message(ws: BunSocket, data: string | Uint8Array): void;
|
|
45
|
+
close(ws: BunSocket, code: number, reason: string): void;
|
|
46
|
+
};
|
|
47
|
+
/** On Bun, app.fetch(req, server) gets the Bun server, whose upgrade() hands the connection to bunWebSocket. */
|
|
48
|
+
export declare function bunUpgrader(req: Request, server: unknown): Upgrader | undefined;
|
|
49
|
+
/** The Sec-WebSocket-Accept value for a client's Sec-WebSocket-Key (RFC 6455 section 4.2.2). */
|
|
50
|
+
export declare function acceptKey(key: string): Promise<string>;
|
|
51
|
+
/** True for a well-formed Sec-WebSocket-Key: 16 random bytes in base64. */
|
|
52
|
+
export declare function validKey(key: string | null): key is string;
|
|
53
|
+
export declare const OP_CONTINUATION = 0;
|
|
54
|
+
export declare const OP_TEXT = 1;
|
|
55
|
+
export declare const OP_BINARY = 2;
|
|
56
|
+
export declare const OP_CLOSE = 8;
|
|
57
|
+
export declare const OP_PING = 9;
|
|
58
|
+
export declare const OP_PONG = 10;
|
|
59
|
+
/** A complete, unmasked server frame. */
|
|
60
|
+
export declare function encodeFrame(opcode: number, payload: Uint8Array): Uint8Array;
|
|
61
|
+
/** A close frame with a status code and a reason (cut to the 123 bytes a close frame has room for). */
|
|
62
|
+
export declare function encodeClose(code: number, reason: string): Uint8Array;
|
|
63
|
+
export interface FrameEvents {
|
|
64
|
+
/** A complete text (string) or binary (bytes) message. */
|
|
65
|
+
message(data: string | Uint8Array): void;
|
|
66
|
+
ping(payload: Uint8Array): void;
|
|
67
|
+
/** The client's close frame: its code (1005 when it sent none) and reason. */
|
|
68
|
+
close(code: number, reason: string): void;
|
|
69
|
+
/** The client broke the protocol: close the connection with this code. */
|
|
70
|
+
error(code: number, reason: string): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reads client frames (RFC 6455): masked, possibly fragmented, with ping, pong and close control frames in between.
|
|
74
|
+
* A message larger than maxMessageBytes is an error (1009), so a client cannot make the server buffer without end.
|
|
75
|
+
*/
|
|
76
|
+
export declare class FrameReader {
|
|
77
|
+
#private;
|
|
78
|
+
constructor(maxMessageBytes: number, events: FrameEvents);
|
|
79
|
+
push(chunk: Uint8Array): void;
|
|
80
|
+
}
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket plumbing shared by the app (src/app.ts) and the servers (src/serve.ts): the per-request upgrader a server
|
|
3
|
+
* registers, the record of upgraded requests, Deno's built-in upgrade, and the RFC 6455 frame reader and writer used
|
|
4
|
+
* on Node.js, where the runtime has no WebSocket server of its own. Web APIs only.
|
|
5
|
+
*/
|
|
6
|
+
import { InferloError } from "./errors.js";
|
|
7
|
+
/** Set by serve() for each request it can upgrade (Node.js upgrade requests, every Bun request). */
|
|
8
|
+
export const upgraders = new WeakMap();
|
|
9
|
+
/** Requests the app upgraded, with Deno's upgrade response (null on Node.js and Bun, which answer it themselves). */
|
|
10
|
+
export const upgrades = new WeakMap();
|
|
11
|
+
/**
|
|
12
|
+
* On Deno, a request from Deno.serve (or deno serve), which passes its handler info with a remoteAddr to app.fetch,
|
|
13
|
+
* can be upgraded with Deno.upgradeWebSocket. Requests made any other way (app.request) cannot.
|
|
14
|
+
*/
|
|
15
|
+
export function denoUpgrader(req, info) {
|
|
16
|
+
const deno = globalThis.Deno;
|
|
17
|
+
if (!deno?.upgradeWebSocket || typeof info !== "object" || info === null || !("remoteAddr" in info))
|
|
18
|
+
return undefined;
|
|
19
|
+
return async (connect) => {
|
|
20
|
+
const { socket, response } = deno.upgradeWebSocket(req);
|
|
21
|
+
const listener = connect({
|
|
22
|
+
send: (text) => {
|
|
23
|
+
if (socket.readyState === 1)
|
|
24
|
+
socket.send(text);
|
|
25
|
+
},
|
|
26
|
+
close: (code, reason) => socket.close(code, reason),
|
|
27
|
+
});
|
|
28
|
+
socket.onmessage = (event) => listener.message(typeof event.data === "string" ? event.data : new Uint8Array(event.data));
|
|
29
|
+
socket.onclose = (event) => listener.close(event.code, event.reason);
|
|
30
|
+
socket.onerror = () => { };
|
|
31
|
+
return response;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The websocket handlers for Bun.serve, so a Bun server that runs app.fetch serves the app's WebSocket routes:
|
|
36
|
+
* Bun.serve({ fetch: app.fetch, websocket: bunWebSocket }). serve() and app.listen() on Bun use them already.
|
|
37
|
+
*/
|
|
38
|
+
export const bunWebSocket = {
|
|
39
|
+
open(ws) {
|
|
40
|
+
ws.data.listener = ws.data.connect({ send: (text) => ws.send(text), close: (code, reason) => ws.close(code, reason) });
|
|
41
|
+
},
|
|
42
|
+
message(ws, data) {
|
|
43
|
+
ws.data.listener?.message(typeof data === "string" ? data : new Uint8Array(data));
|
|
44
|
+
},
|
|
45
|
+
close(ws, code, reason) {
|
|
46
|
+
ws.data.listener?.close(code, reason);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
/** On Bun, app.fetch(req, server) gets the Bun server, whose upgrade() hands the connection to bunWebSocket. */
|
|
50
|
+
export function bunUpgrader(req, server) {
|
|
51
|
+
if (!globalThis.Bun || typeof server?.upgrade !== "function")
|
|
52
|
+
return undefined;
|
|
53
|
+
return async (connect) => {
|
|
54
|
+
let upgraded;
|
|
55
|
+
try {
|
|
56
|
+
upgraded = server.upgrade(req, { data: { connect } });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
upgraded = false;
|
|
60
|
+
}
|
|
61
|
+
if (!upgraded) {
|
|
62
|
+
throw new InferloError("IL2011", "This Bun server cannot accept WebSocket connections.", { status: 501, fix: 'Pass the websocket handlers to Bun.serve: Bun.serve({ fetch: app.fetch, websocket: bunWebSocket }), with bunWebSocket from "@browserlessapi/inferlo".' });
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The Sec-WebSocket-Accept value for a client's Sec-WebSocket-Key (RFC 6455 section 4.2.2). */
|
|
68
|
+
export async function acceptKey(key) {
|
|
69
|
+
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
|
|
70
|
+
let binary = "";
|
|
71
|
+
for (const byte of new Uint8Array(digest))
|
|
72
|
+
binary += String.fromCharCode(byte);
|
|
73
|
+
return btoa(binary);
|
|
74
|
+
}
|
|
75
|
+
/** True for a well-formed Sec-WebSocket-Key: 16 random bytes in base64. */
|
|
76
|
+
export function validKey(key) {
|
|
77
|
+
return typeof key === "string" && /^[A-Za-z0-9+/]{22}==$/.test(key);
|
|
78
|
+
}
|
|
79
|
+
export const OP_CONTINUATION = 0x0;
|
|
80
|
+
export const OP_TEXT = 0x1;
|
|
81
|
+
export const OP_BINARY = 0x2;
|
|
82
|
+
export const OP_CLOSE = 0x8;
|
|
83
|
+
export const OP_PING = 0x9;
|
|
84
|
+
export const OP_PONG = 0xa;
|
|
85
|
+
/** A complete, unmasked server frame. */
|
|
86
|
+
export function encodeFrame(opcode, payload) {
|
|
87
|
+
const length = payload.byteLength;
|
|
88
|
+
const headerLength = length < 126 ? 2 : length < 65536 ? 4 : 10;
|
|
89
|
+
const out = new Uint8Array(headerLength + length);
|
|
90
|
+
out[0] = 0x80 | opcode;
|
|
91
|
+
if (length < 126)
|
|
92
|
+
out[1] = length;
|
|
93
|
+
else if (length < 65536) {
|
|
94
|
+
out[1] = 126;
|
|
95
|
+
out[2] = length >> 8;
|
|
96
|
+
out[3] = length & 0xff;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out[1] = 127;
|
|
100
|
+
new DataView(out.buffer).setBigUint64(2, BigInt(length));
|
|
101
|
+
}
|
|
102
|
+
out.set(payload, headerLength);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/** A close frame with a status code and a reason (cut to the 123 bytes a close frame has room for). */
|
|
106
|
+
export function encodeClose(code, reason) {
|
|
107
|
+
const text = new TextEncoder().encode(reason).subarray(0, 123);
|
|
108
|
+
const payload = new Uint8Array(2 + text.byteLength);
|
|
109
|
+
payload[0] = code >> 8;
|
|
110
|
+
payload[1] = code & 0xff;
|
|
111
|
+
payload.set(text, 2);
|
|
112
|
+
return encodeFrame(OP_CLOSE, payload);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Reads client frames (RFC 6455): masked, possibly fragmented, with ping, pong and close control frames in between.
|
|
116
|
+
* A message larger than maxMessageBytes is an error (1009), so a client cannot make the server buffer without end.
|
|
117
|
+
*/
|
|
118
|
+
export class FrameReader {
|
|
119
|
+
#buffer = new Uint8Array(0);
|
|
120
|
+
#parts = [];
|
|
121
|
+
#size = 0;
|
|
122
|
+
#opcode = -1;
|
|
123
|
+
#done = false;
|
|
124
|
+
#max;
|
|
125
|
+
#events;
|
|
126
|
+
#decoder = new TextDecoder("utf-8", { fatal: true });
|
|
127
|
+
constructor(maxMessageBytes, events) {
|
|
128
|
+
this.#max = maxMessageBytes;
|
|
129
|
+
this.#events = events;
|
|
130
|
+
}
|
|
131
|
+
push(chunk) {
|
|
132
|
+
if (this.#done)
|
|
133
|
+
return;
|
|
134
|
+
if (this.#buffer.byteLength === 0)
|
|
135
|
+
this.#buffer = chunk;
|
|
136
|
+
else {
|
|
137
|
+
const joined = new Uint8Array(this.#buffer.byteLength + chunk.byteLength);
|
|
138
|
+
joined.set(this.#buffer);
|
|
139
|
+
joined.set(chunk, this.#buffer.byteLength);
|
|
140
|
+
this.#buffer = joined;
|
|
141
|
+
}
|
|
142
|
+
while (!this.#done && this.#next())
|
|
143
|
+
;
|
|
144
|
+
}
|
|
145
|
+
#fail(code, reason) {
|
|
146
|
+
this.#done = true;
|
|
147
|
+
this.#events.error(code, reason);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/** Reads one frame from the buffer; false when more bytes are needed or reading stopped. */
|
|
151
|
+
#next() {
|
|
152
|
+
const b = this.#buffer;
|
|
153
|
+
if (b.byteLength < 2)
|
|
154
|
+
return false;
|
|
155
|
+
const fin = (b[0] & 0x80) !== 0;
|
|
156
|
+
const opcode = b[0] & 0x0f;
|
|
157
|
+
if ((b[0] & 0x70) !== 0)
|
|
158
|
+
return this.#fail(1002, "Extensions are not supported");
|
|
159
|
+
if ((b[1] & 0x80) === 0)
|
|
160
|
+
return this.#fail(1002, "Client frames must be masked");
|
|
161
|
+
let length = b[1] & 0x7f;
|
|
162
|
+
let offset = 2;
|
|
163
|
+
if (length === 126) {
|
|
164
|
+
if (b.byteLength < 4)
|
|
165
|
+
return false;
|
|
166
|
+
length = (b[2] << 8) | b[3];
|
|
167
|
+
offset = 4;
|
|
168
|
+
}
|
|
169
|
+
else if (length === 127) {
|
|
170
|
+
if (b.byteLength < 10)
|
|
171
|
+
return false;
|
|
172
|
+
const big = new DataView(b.buffer, b.byteOffset + 2, 8).getBigUint64(0);
|
|
173
|
+
if (big > BigInt(this.#max))
|
|
174
|
+
return this.#fail(1009, "Message too big");
|
|
175
|
+
length = Number(big);
|
|
176
|
+
offset = 10;
|
|
177
|
+
}
|
|
178
|
+
const control = opcode >= 0x8;
|
|
179
|
+
if (control && (!fin || length > 125))
|
|
180
|
+
return this.#fail(1002, "Invalid control frame");
|
|
181
|
+
if (![OP_CONTINUATION, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG].includes(opcode))
|
|
182
|
+
return this.#fail(1002, "Unknown opcode");
|
|
183
|
+
if (!control && this.#size + length > this.#max)
|
|
184
|
+
return this.#fail(1009, "Message too big");
|
|
185
|
+
if (b.byteLength < offset + 4 + length)
|
|
186
|
+
return false;
|
|
187
|
+
const mask = b.subarray(offset, offset + 4);
|
|
188
|
+
const payload = b.slice(offset + 4, offset + 4 + length);
|
|
189
|
+
for (let i = 0; i < payload.byteLength; i++)
|
|
190
|
+
payload[i] ^= mask[i & 3];
|
|
191
|
+
this.#buffer = b.subarray(offset + 4 + length);
|
|
192
|
+
if (opcode === OP_PING)
|
|
193
|
+
this.#events.ping(payload);
|
|
194
|
+
else if (opcode === OP_PONG) {
|
|
195
|
+
// Answers to pings; nothing to do.
|
|
196
|
+
}
|
|
197
|
+
else if (opcode === OP_CLOSE) {
|
|
198
|
+
if (payload.byteLength === 1)
|
|
199
|
+
return this.#fail(1002, "Invalid close frame");
|
|
200
|
+
const code = payload.byteLength >= 2 ? (payload[0] << 8) | payload[1] : 1005;
|
|
201
|
+
let reason = "";
|
|
202
|
+
try {
|
|
203
|
+
reason = this.#decoder.decode(payload.subarray(2));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return this.#fail(1007, "Close reason is not UTF-8");
|
|
207
|
+
}
|
|
208
|
+
this.#done = true;
|
|
209
|
+
this.#events.close(code, reason);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
if (opcode === OP_CONTINUATION ? this.#opcode === -1 : this.#opcode !== -1)
|
|
214
|
+
return this.#fail(1002, "Unexpected frame order");
|
|
215
|
+
if (opcode !== OP_CONTINUATION)
|
|
216
|
+
this.#opcode = opcode;
|
|
217
|
+
this.#parts.push(payload);
|
|
218
|
+
this.#size += length;
|
|
219
|
+
if (fin) {
|
|
220
|
+
const data = this.#parts.length === 1 ? this.#parts[0] : concat(this.#parts, this.#size);
|
|
221
|
+
const kind = this.#opcode;
|
|
222
|
+
this.#parts = [];
|
|
223
|
+
this.#size = 0;
|
|
224
|
+
this.#opcode = -1;
|
|
225
|
+
if (kind === OP_TEXT) {
|
|
226
|
+
let text;
|
|
227
|
+
try {
|
|
228
|
+
text = this.#decoder.decode(data);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return this.#fail(1007, "Text message is not UTF-8");
|
|
232
|
+
}
|
|
233
|
+
this.#events.message(text);
|
|
234
|
+
}
|
|
235
|
+
else
|
|
236
|
+
this.#events.message(data);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function concat(parts, size) {
|
|
243
|
+
const out = new Uint8Array(size);
|
|
244
|
+
let at = 0;
|
|
245
|
+
for (const part of parts) {
|
|
246
|
+
out.set(part, at);
|
|
247
|
+
at += part.byteLength;
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAoB3C,oGAAoG;AACpG,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,OAAO,EAAqB,CAAC;AAE1D,qHAAqH;AACrH,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA4B,CAAC;AAWhE;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY,EAAE,IAAa;IACtD,MAAM,IAAI,GAAI,UAAyG,CAAC,IAAI,CAAC;IAC7H,IAAI,CAAC,IAAI,EAAE,gBAAgB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,YAAY,IAAI,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACtH,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,gBAAiB,CAAC,GAAG,CAAC,CAAC;QACzD,MAAM,QAAQ,GAAG,OAAO,CAAC;YACvB,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;gBACb,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjD,CAAC;YACD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;SACpD,CAAC,CAAC;QACH,MAAM,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,IAAmB,CAAC,CAAC,CAAC;QACxI,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACrE,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;QAC1B,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAID;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,IAAI,CAAC,EAAa;QAChB,EAAE,CAAC,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACzH,CAAC;IACD,OAAO,CAAC,EAAa,EAAE,IAAyB;QAC9C,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACpF,CAAC;IACD,KAAK,CAAC,EAAa,EAAE,IAAY,EAAE,MAAc;QAC/C,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;CACF,CAAC;AAEF,gHAAgH;AAChH,MAAM,UAAU,WAAW,CAAC,GAAY,EAAE,MAAe;IACvD,IAAI,CAAE,UAAgC,CAAC,GAAG,IAAI,OAAQ,MAAuC,EAAE,OAAO,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IACxI,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,IAAI,QAAiB,CAAC;QACtB,IAAI,CAAC;YACH,QAAQ,GAAI,MAA8D,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;QACjH,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,sDAAsD,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,uJAAuJ,EAAE,CAAC,CAAC;QAC1Q,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,GAAG,sCAAsC,CAAC,CAAC,CAAC;IAC3H,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,IAAI,UAAU,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC/E,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,GAAkB;IACzC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AACnC,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC;AAC3B,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAC;AAC5B,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC;AAC3B,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC;AAE3B,yCAAyC;AACzC,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,OAAmB;IAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAClC,MAAM,YAAY,GAAG,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,YAAY,GAAG,MAAM,CAAC,CAAC;IAClD,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC;IACvB,IAAI,MAAM,GAAG,GAAG;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC;SAC7B,IAAI,MAAM,GAAG,KAAK,EAAE,CAAC;QACxB,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;QACb,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC;QACrB,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACzB,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;QACb,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC/B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,MAAc;IACtD,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IACpD,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;IACvB,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACrB,OAAO,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACxC,CAAC;AAYD;;;GAGG;AACH,MAAM,OAAO,WAAW;IACtB,OAAO,GAAe,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,GAAiB,EAAE,CAAC;IAC1B,KAAK,GAAG,CAAC,CAAC;IACV,OAAO,GAAG,CAAC,CAAC,CAAC;IACb,KAAK,GAAG,KAAK,CAAC;IACL,IAAI,CAAS;IACb,OAAO,CAAc;IACrB,QAAQ,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9D,YAAY,eAAuB,EAAE,MAAmB;QACtD,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,IAAI,CAAC,KAAiB;QACpB,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,CAAC;YAAE,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;aACnD,CAAC;YACJ,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;YAC1E,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzB,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YAC3C,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACxB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE;YAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,MAAc;QAChC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACjC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,4FAA4F;IAC5F,KAAK;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QACvB,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACnC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,CAAC,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,8BAA8B,CAAC,CAAC;QAClF,IAAI,CAAC,CAAC,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,8BAA8B,CAAC,CAAC;QAClF,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC;QAC1B,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;YACnC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;YAC9B,MAAM,GAAG,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YAC1B,IAAI,CAAC,CAAC,UAAU,GAAG,EAAE;gBAAE,OAAO,KAAK,CAAC;YACpC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACxE,IAAI,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;YACxE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YACrB,MAAM,GAAG,EAAE,CAAC;QACd,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC;QAC9B,IAAI,OAAO,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC;QACxF,IAAI,CAAC,CAAC,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;QACnI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAC5F,IAAI,CAAC,CAAC,UAAU,GAAG,MAAM,GAAG,CAAC,GAAG,MAAM;YAAE,OAAO,KAAK,CAAC;QACrD,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;QACzD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE;YAAE,OAAO,CAAC,CAAC,CAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;QACzE,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;QAE/C,IAAI,MAAM,KAAK,OAAO;YAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;aAC9C,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YAC5B,mCAAmC;QACrC,CAAC;aAAM,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,OAAO,CAAC,UAAU,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;YAC7E,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAC/E,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC;YACvD,CAAC;YACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;aAAM,CAAC;YACN,IAAI,MAAM,KAAK,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC;YAC9H,IAAI,MAAM,KAAK,eAAe;gBAAE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;YACtD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;YACrB,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC1F,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;gBAC1B,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;gBACjB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;gBACf,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;gBAClB,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;oBACrB,IAAI,IAAY,CAAC;oBACjB,IAAI,CAAC;wBACH,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;oBACpC,CAAC;oBAAC,MAAM,CAAC;wBACP,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC;oBACvD,CAAC;oBACD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAC7B,CAAC;;oBAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAED,SAAS,MAAM,CAAC,KAAmB,EAAE,IAAY;IAC/C,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAClB,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC;IACxB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/llms.txt
CHANGED
|
@@ -11,6 +11,7 @@ Key rules: routes are private unless `auth: "public"`; validate input with `t.*`
|
|
|
11
11
|
- [Schemas](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/schema.md): t.* types, modifiers, validation, JSON Schema
|
|
12
12
|
- [Models and database](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/database.md): model(), queries, adapters, db push, transactions
|
|
13
13
|
- [Routes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/routes.md): options, handler context, return values, errors, middleware, deprecated routes (Deprecation and Sunset headers), API versions with apiVersion("v1", routes) and an OpenAPI document per version
|
|
14
|
+
- [WebSockets](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/websockets.md): route.ws(path, { message, auth, rateLimit }).handle(fn) with the same auth, permissions and rate limits as HTTP routes, JSON messages validated and handled in order, ctx.socket, origin checks, on Node.js, Bun and Deno
|
|
14
15
|
- [Auth](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/auth.md): auth.resolve, roles, permissions() with can() and authorize() and the route permission option, bearer API keys, passwords
|
|
15
16
|
- [AI](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/ai.md): providers, aliases and fallback, structured output, streaming, embeddings, usage and budgets
|
|
16
17
|
- [Modules](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/modules.md): module("billing", { imports, models, routes, jobs, provides }) splits a large app into feature modules with explicit dependencies, createApp({ modules }), checked at startup and shown in describe
|
|
@@ -25,6 +26,7 @@ Key rules: routes are private unless `auth: "public"`; validate input with `t.*`
|
|
|
25
26
|
- [Security](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/security.md): secure defaults and security headers (createApp({ secureHeaders }))
|
|
26
27
|
- [Observability](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/observability.md): request ids (X-Request-Id, ctx.requestId), request logs and JSON logs with createApp({ logging: { format: "json", requests: true } }), your own logger, Prometheus metrics with createApp({ metrics: { token } })
|
|
27
28
|
- [Deployment](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/deploy.md): Node, Bun, Deno, edge, health and readiness endpoints, graceful shutdown with app.close()
|
|
29
|
+
- [Performance](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/performance.md): reproducible throughput and latency benchmark against Fastify, Hono, Express and NestJS (npm run bench:perf) and the latest results
|
|
28
30
|
- [Support policy](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/support.md): semantic versioning, which versions get fixes, LTS after 1.0, deprecation rules and supported runtime, database and framework versions
|
|
29
31
|
- [Error codes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/errors.md): every ILxxxx code with its fix
|
|
30
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserlessapi/inferlo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The enterprise-ready TypeScript backend framework, with AI models built in: routing, validation, PostgreSQL and migrations, auth, rate limiting, jobs, OpenAPI and MCP, plus Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible models.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,22 @@
|
|
|
43
43
|
"types": "./dist/ai/public.d.ts",
|
|
44
44
|
"default": "./dist/ai/public.js"
|
|
45
45
|
},
|
|
46
|
+
"./astro": {
|
|
47
|
+
"types": "./dist/astro.d.ts",
|
|
48
|
+
"default": "./dist/astro.js"
|
|
49
|
+
},
|
|
50
|
+
"./authjs": {
|
|
51
|
+
"types": "./dist/authjs.d.ts",
|
|
52
|
+
"default": "./dist/authjs.js"
|
|
53
|
+
},
|
|
54
|
+
"./azure": {
|
|
55
|
+
"types": "./dist/azure.d.ts",
|
|
56
|
+
"default": "./dist/azure.js"
|
|
57
|
+
},
|
|
58
|
+
"./better-auth": {
|
|
59
|
+
"types": "./dist/better-auth.d.ts",
|
|
60
|
+
"default": "./dist/better-auth.js"
|
|
61
|
+
},
|
|
46
62
|
"./elysia": {
|
|
47
63
|
"types": "./dist/elysia.d.ts",
|
|
48
64
|
"default": "./dist/elysia.js"
|
|
@@ -79,6 +95,10 @@
|
|
|
79
95
|
"types": "./dist/nextjs.d.ts",
|
|
80
96
|
"default": "./dist/nextjs.js"
|
|
81
97
|
},
|
|
98
|
+
"./passport": {
|
|
99
|
+
"types": "./dist/passport.d.ts",
|
|
100
|
+
"default": "./dist/passport.js"
|
|
101
|
+
},
|
|
82
102
|
"./nestjs": {
|
|
83
103
|
"types": "./dist/nestjs.d.ts",
|
|
84
104
|
"default": "./dist/nestjs.js"
|
|
@@ -103,9 +123,17 @@
|
|
|
103
123
|
"types": "./dist/db/postgres.d.ts",
|
|
104
124
|
"default": "./dist/db/postgres.js"
|
|
105
125
|
},
|
|
126
|
+
"./postgres-js": {
|
|
127
|
+
"types": "./dist/postgres-js.d.ts",
|
|
128
|
+
"default": "./dist/postgres-js.js"
|
|
129
|
+
},
|
|
106
130
|
"./testing": {
|
|
107
131
|
"types": "./dist/testing.d.ts",
|
|
108
132
|
"default": "./dist/testing.js"
|
|
133
|
+
},
|
|
134
|
+
"./langchain": {
|
|
135
|
+
"types": "./dist/langchain.d.ts",
|
|
136
|
+
"default": "./dist/langchain.js"
|
|
109
137
|
}
|
|
110
138
|
},
|
|
111
139
|
"bin": {
|
|
@@ -121,32 +149,53 @@
|
|
|
121
149
|
"scripts": {
|
|
122
150
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli/main.js",
|
|
123
151
|
"prepare": "npm run build",
|
|
124
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk && tsc --noEmit -p examples/with-hono && tsc --noEmit -p examples/with-express && tsc --noEmit -p examples/with-fastify && tsc --noEmit -p examples/with-elysia && tsc --noEmit -p examples/with-nestjs && tsc --noEmit -p examples/with-trpc && tsc --noEmit -p examples/with-adonisjs && tsc --noEmit -p examples/with-tsed && tsc --noEmit -p examples/with-encore && tsc --noEmit -p examples/with-koa && tsc --noEmit -p examples/with-nextjs && tsc --noEmit -p examples/with-nitro && tsc --noEmit -p examples/with-sveltekit && tsc --noEmit -p examples/with-react-router && tsc --noEmit -p examples/with-lambda && tsc --noEmit -p examples/with-hapi",
|
|
152
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk && tsc --noEmit -p examples/with-hono && tsc --noEmit -p examples/with-express && tsc --noEmit -p examples/with-fastify && tsc --noEmit -p examples/with-elysia && tsc --noEmit -p examples/with-nestjs && tsc --noEmit -p examples/with-trpc && tsc --noEmit -p examples/with-adonisjs && tsc --noEmit -p examples/with-tsed && tsc --noEmit -p examples/with-encore && tsc --noEmit -p examples/with-koa && tsc --noEmit -p examples/with-nextjs && tsc --noEmit -p examples/with-nitro && tsc --noEmit -p examples/with-sveltekit && tsc --noEmit -p examples/with-react-router && tsc --noEmit -p examples/with-lambda && tsc --noEmit -p examples/with-hapi && tsc --noEmit -p examples/with-azure && tsc --noEmit -p examples/with-cloud-run-functions && tsc --noEmit -p examples/with-cloudflare-workers && tsc --noEmit -p examples/with-vercel && tsc --noEmit -p examples/with-netlify && tsc --noEmit -p examples/with-astro && tsc --noEmit -p examples/with-drizzle && tsc --noEmit -p examples/with-typeorm && tsc --noEmit -p examples/with-kysely && tsc --noEmit -p examples/with-sequelize && tsc --noEmit -p examples/with-knex && tsc --noEmit -p examples/with-postgres-js && tsc --noEmit -p examples/with-mikro-orm && tsc --noEmit -p examples/with-better-auth && node examples/with-prisma/generate.ts && tsc --noEmit -p examples/with-prisma",
|
|
125
153
|
"test": "node --test --test-reporter=spec \"test/**/*.test.ts\" \"examples/*/test/*.test.ts\"",
|
|
126
154
|
"test:bun": "bun test test/",
|
|
155
|
+
"test:deno": "deno test -A --no-check --ignore=test/tsed.test.ts test/ && deno test -A --no-check --ignore=examples/with-tsed examples/ && deno test -A --no-check --unsafe-proto test/tsed.test.ts examples/with-tsed/",
|
|
127
156
|
"llms": "node scripts/build-llms.ts",
|
|
128
157
|
"check": "npm run typecheck && npm test && node scripts/build-llms.ts --check",
|
|
129
|
-
"bench": "node bench/run.ts"
|
|
158
|
+
"bench": "node bench/run.ts",
|
|
159
|
+
"bench:perf": "node bench/perf/run.ts",
|
|
160
|
+
"bench:perf:check": "node bench/perf/check.ts"
|
|
130
161
|
},
|
|
131
162
|
"peerDependencies": {
|
|
163
|
+
"@langchain/core": ">=0.3.0",
|
|
132
164
|
"@opentelemetry/api": ">=1.3.0",
|
|
133
|
-
"pg": ">=8"
|
|
165
|
+
"pg": ">=8",
|
|
166
|
+
"postgres": ">=3.4.0"
|
|
134
167
|
},
|
|
135
168
|
"peerDependenciesMeta": {
|
|
169
|
+
"@langchain/core": {
|
|
170
|
+
"optional": true
|
|
171
|
+
},
|
|
136
172
|
"@opentelemetry/api": {
|
|
137
173
|
"optional": true
|
|
138
174
|
},
|
|
139
175
|
"pg": {
|
|
140
176
|
"optional": true
|
|
177
|
+
},
|
|
178
|
+
"postgres": {
|
|
179
|
+
"optional": true
|
|
141
180
|
}
|
|
142
181
|
},
|
|
143
182
|
"devDependencies": {
|
|
144
183
|
"@adonisjs/core": "^6.21.0",
|
|
184
|
+
"@ai-sdk/openai-compatible": "^3.0.85",
|
|
185
|
+
"@auth/core": "^0.41.3",
|
|
186
|
+
"@azure/functions": "^4.17.0",
|
|
187
|
+
"@google-cloud/functions-framework": "^5.0.5",
|
|
145
188
|
"@hapi/hapi": "^21.4.13",
|
|
189
|
+
"@hono/node-server": "^2.1.6",
|
|
190
|
+
"@langchain/core": "^1.3.5",
|
|
191
|
+
"@mikro-orm/core": "^7.2.8",
|
|
192
|
+
"@mikro-orm/postgresql": "^7.2.8",
|
|
146
193
|
"@nestjs/common": "^12.3.3",
|
|
147
194
|
"@nestjs/core": "^12.3.3",
|
|
148
195
|
"@nestjs/platform-express": "^12.3.3",
|
|
149
196
|
"@opentelemetry/api": "^1.10.0",
|
|
197
|
+
"@prisma/adapter-pg": "^7.10.1",
|
|
198
|
+
"@prisma/client": "^7.10.1",
|
|
150
199
|
"@sveltejs/kit": "^2.70.3",
|
|
151
200
|
"@trpc/server": "^11.24.0",
|
|
152
201
|
"@tsed/core": "^8.45.0",
|
|
@@ -155,24 +204,47 @@
|
|
|
155
204
|
"@tsed/platform-http": "^8.45.0",
|
|
156
205
|
"@tsed/platform-params": "^8.45.0",
|
|
157
206
|
"@tsed/schema": "^8.45.0",
|
|
207
|
+
"@types/autocannon": "^7.12.7",
|
|
158
208
|
"@types/express": "^5.0.6",
|
|
209
|
+
"@types/express-session": "^1.19.0",
|
|
159
210
|
"@types/koa": "^3.0.3",
|
|
160
211
|
"@types/node": "^22.18.0",
|
|
212
|
+
"@types/oidc-provider": "^9.13.1",
|
|
213
|
+
"@types/passport": "^1.0.17",
|
|
214
|
+
"@types/passport-local": "^1.0.38",
|
|
161
215
|
"@types/pg": "^8.15.0",
|
|
216
|
+
"ai": "^7.0.163",
|
|
217
|
+
"astro": "^7.4.7",
|
|
218
|
+
"autocannon": "^8.0.0",
|
|
219
|
+
"better-auth": "^1.7.18",
|
|
220
|
+
"drizzle-orm": "^0.45.4",
|
|
162
221
|
"elysia": "^1.4.30",
|
|
163
222
|
"encore.dev": "^1.58.8",
|
|
164
223
|
"express": "^5.3.0",
|
|
224
|
+
"express-session": "^1.19.0",
|
|
165
225
|
"fastify": "^5.12.8",
|
|
166
226
|
"h3": "^2.0.8",
|
|
167
227
|
"hono": "^4.13.16",
|
|
228
|
+
"knex": "^3.3.0",
|
|
168
229
|
"koa": "^3.3.0",
|
|
230
|
+
"kysely": "^0.29.8",
|
|
231
|
+
"miniflare": "^4.20260730.0",
|
|
169
232
|
"next": "^16.4.5",
|
|
233
|
+
"oidc-provider": "^9.13.3",
|
|
234
|
+
"passport": "^0.7.0",
|
|
235
|
+
"passport-local": "^1.0.0",
|
|
170
236
|
"pg": "^8.16.0",
|
|
237
|
+
"postgres": "^4.1.0",
|
|
238
|
+
"prisma": "^7.10.1",
|
|
171
239
|
"react": "^19.3.1",
|
|
172
240
|
"react-router": "^8.5.1",
|
|
241
|
+
"redis": "^6.3.1",
|
|
173
242
|
"reflect-metadata": "^0.2.2",
|
|
174
243
|
"rxjs": "^7.8.2",
|
|
175
|
-
"
|
|
244
|
+
"sequelize": "^6.37.8",
|
|
245
|
+
"typeorm": "^1.1.2",
|
|
246
|
+
"typescript": "^5.9.3",
|
|
247
|
+
"zod": "^4.6.7"
|
|
176
248
|
},
|
|
177
249
|
"publishConfig": {
|
|
178
250
|
"access": "public"
|
package/templates/app/AGENTS.md
CHANGED
|
@@ -30,24 +30,28 @@ Do not finish until `inferlo check` passes.
|
|
|
30
30
|
## Rules
|
|
31
31
|
|
|
32
32
|
1. **One way to do each thing.** Routes: `route.get/post/put/patch/delete(path, options).handle(fn)`. Validation: `t.*` schemas. Database: `db.<table>.<method>`. AI: `ai.generate` / `ai.stream` / `ai.embed`.
|
|
33
|
-
2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks. Browser sign-in uses cookie sessions: `export const session = sessions({ secret: settings.SESSION_SECRET, user: ({ userId }, { db }) => ... })`, `createApp({ auth: session })`, `await session.signIn(ctx, user.id)` in the login route and `session.signOut(ctx)` in logout; browsers send the CSRF token in `X-CSRF-Token` or a `_csrf` form field on POST, PUT, PATCH and DELETE (403 IL3005 without it).
|
|
34
|
-
3. **Validate every input with a schema.** `body`, `query` and `params` options. Reuse model schemas: `Ticket.pick("title", "body")`, `Ticket.omit("ownerId")`, `.partial()` for PATCH.
|
|
33
|
+
2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks. Browser sign-in uses cookie sessions: `export const session = sessions({ secret: settings.SESSION_SECRET, user: ({ userId }, { db }) => ... })`, `createApp({ auth: session })`, `await session.signIn(ctx, user.id)` in the login route and `session.signOut(ctx)` in logout; browsers send the CSRF token in `X-CSRF-Token` or a `_csrf` form field on POST, PUT, PATCH and DELETE (403 IL3005 without it). Company single sign-on (Okta, Entra ID, Google Workspace, Keycloak) for browser apps is `export const sso = oidc({ issuer, clientId, clientSecret, session, userId: (claims, { db }) => ... })` with `routes: [..., sso]`, ending in that session (sign out with a POST form to `sso.logoutPath` with the `_csrf` field, which also signs out at the provider; Google's `hd` and Entra ID's `domain_hint` go in `authorizationParams`); return `null` from `userId` for unknown users and trust `claims.email` only when `claims.email_verified` is true. APIs whose users sign in with an identity provider (Auth0, Okta, Entra ID, Keycloak, Clerk) check its access tokens with `auth: jwt({ issuer, audience, user: (claims) => ({ id: claims.sub, roles: [] }) })`; never decode or verify JWTs by hand. Apps that sign in with Better Auth keep it and use `const session = betterAuthSession(auth, { user: ({ user }) => ({ id: user.id, email: user.email, roles: [] }) })` from `@browserlessapi/inferlo/better-auth` with `createApp({ auth: session, middleware: [session.middleware] })`; never write a second sign-in system next to it. Apps on Auth.js do the same with `authJsSession(Auth, authConfig)` from `@browserlessapi/inferlo/authjs` (add `id: token.sub` to the session in `callbacks.session`). Express apps on Passport.js mount the app with `toNodeHandler(app)` after `passport.session()` and use `auth: passportSession({ user: (u) => ({ id: u.id, email: u.email, roles: [] }) })` from `@browserlessapi/inferlo/passport`.
|
|
34
|
+
3. **Validate every input with a schema.** `body`, `query` and `params` options. Reuse model schemas: `Ticket.pick("title", "body")`, `Ticket.omit("ownerId")`, `.partial()` for PATCH. File uploads: `body: t.object({ avatar: t.file({ maxBytes: 2_000_000, types: ["image/png", "image/jpeg"] }) })` gives the handler a `File` (clients send `FormData`); several files are `t.array(t.file()).max(10)`. Never put `t.file()` in a model: keep the file with `ctx.storage` (`` await storage.put(`avatars/${user.id}`, body.avatar) ``, needs `createApp({ storage: diskStorage({ dir: "./uploads" }) })`, `memoryStorage()` in tests) and its key in a `t.string()`; build keys from ids, never from the client's file name.
|
|
35
35
|
4. **Check ownership.** Fetch with `db.x.get(id)` (throws 404) and compare `ownerId` to `user.id`; throw `notFound()` when it does not match, so other users' ids are not revealed. When more than one rule decides access (owners, admins, teams), define named permissions once in `src/permissions.ts` with `export const access = permissions({ "notes.update": ({ user, note }) => note.ownerId === user.id || (user.roles ?? []).includes("admin") })`, pass `permissions: access` to `createApp`, and call `await access.authorize("notes.update", { user, note })` in routes and jobs (403 IL3004 when denied). Permissions that need only the user go on the route: `{ permission: "reports.view" }`; add `permissions: typeof access` to `Register` (with `import type`) so permission names are type-checked. When changes must be on record (who changed what), add `AuditLog` to the models and call `await audit("notes.update", { model: "notes", before: note, after: updated })` after the change, in routes and jobs (jobs pass `userId`).
|
|
36
36
|
5. **Related rows:** a `t.ref` field named `<name>Id` is a relation: `db.notes.findMany({ include: { owner: true } })` adds `note.owner` with one extra query, and `db.users.findMany({ include: { notes: true } })` adds `user.notes` (the rows that point to each user). Never load related rows in a loop. For lists clients page through, use `db.notes.paginate({ where, orderBy: { createdAt: "desc" }, cursor: query.cursor, limit: query.limit })`, which returns `{ items, nextCursor }`.
|
|
37
|
-
6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders.
|
|
37
|
+
6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders. Code that takes a pg client (Drizzle, Kysely, plain `pg`, and Knex queries with `.connection(client)`) gets it from `db.$pgClient()`, and inside `db.$transaction(async (tx) => ...)` from `tx.$pgClient()`, so it runs in the same transaction. Apps on postgres.js run Inferlo on their `sql` instance with `postgres({ pool: postgresJsPool(sql) })` from `@browserlessapi/inferlo/postgres-js` (never a second pool), and inside a transaction use `const { sql } = await tx.$pgClient<PostgresJsClient<ReservedSql>>()`. Prisma, TypeORM, Sequelize and MikroORM cannot join an Inferlo transaction: for a write across a Prisma, TypeORM, Sequelize or MikroORM table and an Inferlo model, use `db.$transaction` and write the other table with `tx.$sql`. Apps on MikroORM must set `schemaGenerator: { skipTables: [...models.map((m) => m.name), /^inferlo_/, "audit_log"] }`, or MikroORM's schema update drops Inferlo's tables.
|
|
38
38
|
7. **Model ids, timestamps:** every model already has `id`, `createdAt`, `updatedAt`. Never define them or send them as input.
|
|
39
39
|
8. **New field?** Add it to the model in `src/models.ts`, then run `npx inferlo db push` in development, or `npx inferlo db migrate <name>` to write a migration file for production (applied with `npx inferlo db migrate --apply`). Use `.optional()` or `.default()` for new fields on existing tables. Never edit `migrations/snapshot.json`, and never edit a migration file that has been applied (`--apply` warns with IL4018 and does not run the change): put changes in a new migration. Removing a model or field needs `npx inferlo db migrate <name> --allow-drop` (it deletes that data); never use it for a rename. Changing a field's type or default, making it required or optional, or changing an enum's values needs `--allow-change`; give a newly required field a `.default()` so existing NULL rows get a value, and before removing an enum value add an `UPDATE` in the new migration file (before its `ADD CONSTRAINT`) that moves rows off it (IL4021).
|
|
40
40
|
9. **New resource?** `npx inferlo add resource <plural_name>` generates the model and CRUD routes. Then edit them.
|
|
41
|
-
10. **AI calls** use aliases from `src/app.ts` (`model: "fast"`), never hard-coded provider model ids in routes. Use `output: schema` for structured data and `for: user.id` so usage is tracked per user. To let the model act, pass routes as tools: `ai.generate({ prompt, tools: [createNote, getNote], for: user.id })`. Each tool call runs as the current user with that route's auth and validation. For work that is not a route (an external API, a calculation), make a function tool: `tool("getWeather", { description, input: t.object({ ... }) }).handle(async ({ input }) => ...)`. To send images: `ai.generate({ prompt, images: [{ url }] })` or `[{ base64, mediaType: "image/png" }]`. Give routes that call AI models, send email or check passwords a `rateLimit: { max, per, by }` (IL2005 is the 429 it returns).
|
|
41
|
+
10. **AI calls** use aliases from `src/app.ts` (`model: "fast"`), never hard-coded provider model ids in routes. Use `output: schema` for structured data and `for: user.id` so usage is tracked per user. A provider without a built-in adapter (Mistral, Bedrock, Azure, Vertex, Groq, ...) is added through its Vercel AI SDK package: `providers: { mistral: aiSdk(createMistral({ apiKey: settings.MISTRAL_API_KEY })) }`, then aliases name `"mistral:<model id>"`. When the app uses LangChain agents, give them Inferlo's model and tools: `toLangChainModel(ai, { model: "fast", for: user.id })` and `toLangChainTools([getWeather, createNote], { ai })` from `@browserlessapi/inferlo/langchain`, never a second set of provider keys. To let the model act, pass routes as tools: `ai.generate({ prompt, tools: [createNote, getNote], for: user.id })`. Each tool call runs as the current user with that route's auth and validation. For work that is not a route (an external API, a calculation), make a function tool: `tool("getWeather", { description, input: t.object({ ... }) }).handle(async ({ input }) => ...)`. To send images: `ai.generate({ prompt, images: [{ url }] })` or `[{ base64, mediaType: "image/png" }]`. Give routes that call AI models, send email or check passwords a `rateLimit: { max, per, by }` (IL2005 is the 429 it returns). Streaming to a chat UI built with the Vercel AI SDK (`useChat`): `route.post("/chat", { body: uiChatBody }).handle(({ body, ai, user }) => ai.stream({ model: "fast", messages: fromUIMessages(body.messages), for: user.id }).toResponse({ format: "ai-sdk" }))`.
|
|
42
42
|
11. **Errors:** throw `notFound()`, `forbidden()`, `badRequest()` or `httpError(status, message)`. Every Inferlo error has a code (ILxxxx), a fix and a docs link: read the fix before changing code.
|
|
43
43
|
12. **Imports:** relative imports end in `.ts` (`import { Note } from "../models.ts"`).
|
|
44
44
|
13. **Configuration:** read environment variables only through `settings` from `src/config.ts`. A new variable is a new field in its `t.object({ ... })` schema (`.default(...)` or `.optional()` unless the app cannot start without it), and API keys, passwords and connection URLs go in `secrets` so they are never logged. Never read `process.env` in routes.
|
|
45
45
|
14. **Expose to AI agents:** add `tool: true` to a route to make it an MCP tool at `/mcp`. Same auth applies.
|
|
46
|
-
15. **Slow work after the response** (email, slow AI calls) is a background job: `export const x = job("send-email", { input: t.object({ ... }) }).handle(async ({ input, db, ai }) => { ... })` in `src/jobs.ts`, registered with `createApp({ jobs: [jobs] })`, queued with `await ctx.jobs.enqueue(x, { ... })`. Pass ids in the input, not rows. In tests, run queued jobs with `await app.jobs.runDue()`. Jobs run in the server process (`app.jobs.start()` in `src/server.ts`); with `createApp({ jobQueue: { store: "database" } })` they are kept in Postgres and `npm run worker` (`inferlo worker`) runs them in separate processes. Scheduled work is a cron next to its job: `export const nightly = cron("0 3 * * *", cleanupJob)` (UTC; the job's input must be `t.object({})`).
|
|
46
|
+
15. **Slow work after the response** (email, slow AI calls) is a background job: `export const x = job("send-email", { input: t.object({ ... }) }).handle(async ({ input, db, ai }) => { ... })` in `src/jobs.ts`, registered with `createApp({ jobs: [jobs] })`, queued with `await ctx.jobs.enqueue(x, { ... })`. Pass ids in the input, not rows. In tests, run queued jobs with `await app.jobs.runDue()`. Jobs run in the server process (`app.jobs.start()` in `src/server.ts`); with `createApp({ jobQueue: { store: "database" } })` they are kept in Postgres (or with `createApp({ redis, jobQueue: { store: "redis" } })` in Redis) and `npm run worker` (`inferlo worker`) runs them in separate processes. Scheduled work is a cron next to its job: `export const nightly = cron("0 3 * * *", cleanupJob)` (UTC; the job's input must be `t.object({})`). When other parts of the app react to something that happened, publish an event instead of enqueuing each job: `export const orderPaid = event("order.paid", { payload: t.object({ ... }) })` in `src/events.ts`, jobs subscribed with `on(orderPaid, sendReceipt)` (the job's input is `orderPaid.payload`) in `src/jobs.ts`, and `await ctx.events.publish(orderPaid, { ... })` after the change is saved. To send events to other services, give each service a `name` and set `createApp({ name, redis, events: { transport: "redis" } })`; subscribers use the same `on(event, job)`.
|
|
47
47
|
16. **Security headers and health:** every response already gets security headers (CSP, HSTS over HTTPS, nosniff, frame and referrer policies) and `GET /health` and `GET /ready` are built in (`health: true` in `src/app.ts`). Change a header with `createApp({ secureHeaders: { "Content-Security-Policy": "..." } })` instead of setting it in every route, never turn them all off without a proxy that adds them, and never add your own `/health` route. On SIGTERM, `src/server.ts` calls `app.close()`, which finishes in-flight requests and jobs first. Prometheus metrics (`GET /metrics`: requests, jobs, AI calls, tokens and cost) are served when `METRICS_TOKEN` is set; never add your own `/metrics` route.
|
|
48
48
|
17. **Large apps: feature modules.** When the app grows to several features, group each feature's models, routes and jobs in `src/<feature>/module.ts` with `export const billing = module("billing", { imports: [users], models: [...], routes: [routes], jobs: [jobs], provides: { createInvoice } })` and register it with `createApp({ modules: [users, billing] })`. A module that uses another module's models or `provides` lists it in `imports` (IL6023 and IL6024 tell you when one is missing). Call other modules only through their `provides`; never import their internal files. Keep every model in `Register` in `src/models.ts`.
|
|
49
49
|
18. **Replacing a route:** do not delete a route clients use. Add the new one and mark the old one `deprecated: { since: "YYYY-MM-DD", sunset: "YYYY-MM-DD", link: "https://..." }` in its options; it then sends the standard Deprecation and Sunset headers and is marked in OpenAPI and MCP. Remove it after the sunset date. For a breaking change to many routes, publish a new API version instead: `createApp({ routes: [apiVersion("v1", [notesV1]), apiVersion("v2", [notes])] })` serves them under `/v1` and `/v2`, each with its own OpenAPI document (`/v1/openapi.json`); write route paths without the version prefix.
|
|
50
50
|
19. **Caching:** for results that are slow or expensive to compute, use the built-in cache instead of a module-level `Map`: `` return cache.getOrSet(`notes:stats:${user.id}`, { ttlMs: 60_000, tags: ["notes"] }, () => computeStats(db, user.id)) `` in routes and jobs (`ctx.cache`), and `await cache.invalidate("notes")` after notes change. `ttlMs` is required; put the user id in the key of anything that differs per user; values come back as JSON (dates as strings). To cache a whole GET route instead, add the route option `cache: { ttlMs: 60_000, tags: ["notes"] }` (one response per user by default; `by: "public"` only for data that is the same for everyone) and invalidate the tag where the data changes. With several instances, use `createApp({ cache: { store: "database" } })`.
|
|
51
|
+
20. **Calling another Inferlo service:** `const tickets = serviceClient({ baseUrl: settings.TICKETS_URL, token: settings.TICKETS_TOKEN })`, then in a handler `await tickets.call(getTicket, { params: { id }, ctx })` with the other service's route imported, so the call is typed. Catch `ServiceCallError` and check `error.remoteStatus` for answers you expect (404). Never build these URLs with `fetch` by hand. When the app uses `tracing: { api: otel }`, pass the same `tracing` to `serviceClient` so calls join the trace.
|
|
52
|
+
21. **Data of many organisations (multi-tenant):** give the signed-in user a `tenantId` (the organisation's id) and make tables whose rows belong to one organisation tenant-scoped: `model("projects", { ... }, { tenant: true })`. `ctx.db` then filters every query by and stamps every insert with the user's tenant, so never add `tenantId` to a `where`, a body schema or an insert. Jobs queued (or events published) by a request run for the request's tenant: their `ctx.db` is scoped the same way, so do not pass the tenant id in the job input. In scripts use `app.db.$tenant(tenantId).projects`, and `app.jobs.enqueue(job, input, { tenantId })`. For limits and caches shared by an organisation use `rateLimit: { max, per, by: "tenant" }` and `cache: { ttlMs, by: "tenant" }`; `ctx.audit` records the tenant itself.
|
|
53
|
+
22. **Live updates over WebSockets:** `export const chat = route.ws("/chat", { message: t.object({ text: t.string().max(2000) }), rateLimit: { max: 60, per: "minute" } }).handle(async ({ message, user, db, socket }) => ...)` in a routes file, registered like other routes. Return a value to reply with JSON, or call `socket.send(data)` for several messages; stream a model's answer with `await socket.stream(ai.stream({ model: "fast", prompt, for: user.id }))`. Auth works as for HTTP routes (browsers authenticate with cookie sessions); never add a second WebSocket server or library. For live updates, ``socket.join(`project:${id}`)`` in the WebSocket handler (after checking the user may see it) and ``await sockets.broadcast(`project:${id}`, data)`` from any route, WebSocket handler or job; rooms are per tenant, and with more than one instance add `sockets: { transport: "redis", subscriber: await redis.duplicate().connect() }` next to `redis` in createApp. Test with `await app.listen(0)` and `new WebSocket(url)`.
|
|
54
|
+
23. **Response schemas:** give routes whose answer clients rely on a `response` schema (`route.get("/me", { response: t.object({ id: t.uuid(), email: t.email(), name: t.string() }) })`) and return exactly those fields, never a whole row. Outside production Inferlo checks answers against it and logs IL2016 for a missing, wrong or extra field; tests use `createApp({ validateResponses: "error" })` so a mismatch fails.
|
|
51
55
|
|
|
52
56
|
## Testing
|
|
53
57
|
|
|
@@ -11,6 +11,8 @@ function makeApp() {
|
|
|
11
11
|
routes: [notes],
|
|
12
12
|
db: memory(),
|
|
13
13
|
auth: { resolve: () => null },
|
|
14
|
+
// An answer that does not match its route's response schema fails the test (500 IL2016).
|
|
15
|
+
validateResponses: "error",
|
|
14
16
|
ai: { providers: { mock: mockProvider({ respond: () => '{"summary":"A short note."}' }) }, models: { fast: "mock:test" } },
|
|
15
17
|
});
|
|
16
18
|
}
|