@schlessera/brain-ui-server 0.15.0 → 0.17.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 +4 -0
- package/dist/agent/backend.d.ts +31 -10
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +92 -28
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts +9 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +80 -18
- package/dist/app.js.map +1 -1
- package/dist/config/env-core.d.ts +73 -0
- package/dist/config/env-core.d.ts.map +1 -0
- package/dist/config/env-core.js +62 -0
- package/dist/config/env-core.js.map +1 -0
- package/dist/config/env.d.ts +27 -5
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +90 -10
- package/dist/config/env.js.map +1 -1
- package/dist/cron/scheduler.d.ts +20 -0
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +43 -8
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/client.d.ts +10 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +5 -14
- package/dist/db/client.js.map +1 -1
- package/dist/db/settings.d.ts +3 -2
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +8 -4
- package/dist/db/settings.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +8 -2
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +74 -10
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +10 -0
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +19 -3
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/observability/index.d.ts +73 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +74 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/loggers.d.ts +62 -0
- package/dist/observability/loggers.d.ts.map +1 -0
- package/dist/observability/loggers.js +109 -0
- package/dist/observability/loggers.js.map +1 -0
- package/dist/observability/meter.d.ts +39 -0
- package/dist/observability/meter.d.ts.map +1 -0
- package/dist/observability/meter.js +95 -0
- package/dist/observability/meter.js.map +1 -0
- package/dist/observability/types.d.ts +67 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +44 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +9 -1
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts +2 -0
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +7 -7
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/graph.d.ts +2 -0
- package/dist/routes/graph.d.ts.map +1 -1
- package/dist/routes/graph.js +7 -7
- package/dist/routes/graph.js.map +1 -1
- package/dist/routes/health.d.ts +7 -14
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +25 -6
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/models.d.ts +3 -0
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +5 -1
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/render.d.ts +2 -1
- package/dist/routes/render.d.ts.map +1 -1
- package/dist/routes/render.js +2 -2
- package/dist/routes/render.js.map +1 -1
- package/dist/routes/share.d.ts +3 -0
- package/dist/routes/share.d.ts.map +1 -1
- package/dist/routes/share.js +5 -5
- package/dist/routes/share.js.map +1 -1
- package/dist/share/staging.d.ts +3 -2
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +12 -4
- package/dist/share/staging.js.map +1 -1
- package/dist/voice/keyterm-builder.d.ts +4 -1
- package/dist/voice/keyterm-builder.d.ts.map +1 -1
- package/dist/voice/keyterm-builder.js +15 -8
- package/dist/voice/keyterm-builder.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +23 -3
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +10 -3
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +16 -4
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts +15 -0
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +66 -10
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts +22 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +36 -16
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +73 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +127 -1
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/rate-limit.d.ts +58 -0
- package/dist/ws/rate-limit.d.ts.map +1 -0
- package/dist/ws/rate-limit.js +62 -0
- package/dist/ws/rate-limit.js.map +1 -0
- package/dist/ws/run-session.d.ts +3 -0
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +49 -9
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/session-catalog.d.ts +2 -1
- package/dist/ws/session-catalog.d.ts.map +1 -1
- package/dist/ws/session-catalog.js +20 -3
- package/dist/ws/session-catalog.js.map +1 -1
- package/dist/ws/turns.d.ts +21 -0
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js +11 -1
- package/dist/ws/turns.js.map +1 -1
- package/package.json +9 -6
- package/src/agent/backend.ts +131 -35
- package/src/app.ts +97 -18
- package/src/config/env-core.ts +93 -0
- package/src/config/env.ts +116 -15
- package/src/cron/scheduler.ts +61 -16
- package/src/db/client.ts +16 -5
- package/src/db/settings.ts +9 -4
- package/src/index.ts +43 -2
- package/src/middleware/auth.ts +80 -15
- package/src/middleware/passkeys.ts +29 -5
- package/src/observability/index.ts +150 -0
- package/src/observability/loggers.ts +174 -0
- package/src/observability/meter.ts +190 -0
- package/src/observability/types.ts +101 -0
- package/src/routes/brain.ts +10 -1
- package/src/routes/files.ts +9 -8
- package/src/routes/graph.ts +9 -8
- package/src/routes/health.ts +29 -6
- package/src/routes/models.ts +8 -5
- package/src/routes/render.ts +3 -2
- package/src/routes/share.ts +17 -10
- package/src/share/staging.ts +18 -4
- package/src/voice/keyterm-builder.ts +19 -10
- package/src/ws/bridge.ts +22 -6
- package/src/ws/clients.ts +24 -6
- package/src/ws/connection.ts +73 -13
- package/src/ws/dispatch.ts +47 -16
- package/src/ws/host.ts +164 -1
- package/src/ws/rate-limit.ts +86 -0
- package/src/ws/run-session.ts +57 -14
- package/src/ws/session-catalog.ts +21 -3
- package/src/ws/turns.ts +27 -3
package/dist/ws/dispatch.js
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
|
+
import { PROTOCOL_REV_CLIENT_ECHO } from "@schlessera/brain-ui-sdk/protocol";
|
|
1
2
|
import { locationErrorText } from "./frames.js";
|
|
2
3
|
import { sendSessionHistory } from "./history.js";
|
|
3
4
|
import { validateAttachments } from "./attachments.js";
|
|
4
5
|
import { handleChatMessage } from "./run-session.js";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* a
|
|
9
|
-
*
|
|
7
|
+
* Does this reply's echoed turnId identify the turn that raised the request?
|
|
8
|
+
*
|
|
9
|
+
* A wrong id is always refused: a stale echo from before a reconnect or a
|
|
10
|
+
* follow-up would resolve a different turn's pending promise.
|
|
11
|
+
*
|
|
12
|
+
* A MISSING id depends on who is speaking. A client that declared rev 3
|
|
13
|
+
* promised to echo, so silence means the reply cannot be correlated and is
|
|
14
|
+
* refused. A client that declared nothing is rev 2, where the field is
|
|
15
|
+
* optional, and is still tolerated — that tolerance is the deprecation window,
|
|
16
|
+
* and it is what lets this be enforced at all without breaking clients that
|
|
17
|
+
* predate `client_hello`.
|
|
10
18
|
*/
|
|
11
|
-
function turnIdMatches(pending, echoed) {
|
|
12
|
-
|
|
19
|
+
export function turnIdMatches(pending, echoed, requireEcho) {
|
|
20
|
+
if (echoed === undefined)
|
|
21
|
+
return !requireEcho;
|
|
22
|
+
return echoed === pending.turnId;
|
|
13
23
|
}
|
|
14
|
-
export async function handleClientMessage(host, ws, msg) {
|
|
24
|
+
export async function handleClientMessage(host, ws, msg, connection = {}) {
|
|
15
25
|
const { coordinator, catalog } = host;
|
|
26
|
+
const requireEcho = (connection.protocolRev ?? 2) >= PROTOCOL_REV_CLIENT_ECHO;
|
|
16
27
|
switch (msg.type) {
|
|
28
|
+
case "client_hello": {
|
|
29
|
+
// Record what this connection speaks. Never rejected on version: a
|
|
30
|
+
// future client declaring rev 9 is simply held to the rules this host
|
|
31
|
+
// knows, and an unknown capability flag is ignored.
|
|
32
|
+
connection.protocolRev = msg.protocolRev;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
17
35
|
case "chat_message": {
|
|
18
36
|
const attachmentResult = validateAttachments(msg.attachments);
|
|
19
37
|
if (!attachmentResult.ok) {
|
|
@@ -36,7 +54,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
36
54
|
}
|
|
37
55
|
case "ask_user_response": {
|
|
38
56
|
const pending = coordinator.pendingAskUser.get(msg.requestId);
|
|
39
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
57
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
40
58
|
coordinator.pendingAskUser.delete(msg.requestId);
|
|
41
59
|
pending.resolve({ answers: msg.answers, annotations: msg.annotations });
|
|
42
60
|
}
|
|
@@ -44,7 +62,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
44
62
|
}
|
|
45
63
|
case "ask_user_cancel": {
|
|
46
64
|
const pending = coordinator.pendingAskUser.get(msg.requestId);
|
|
47
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
65
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
48
66
|
coordinator.pendingAskUser.delete(msg.requestId);
|
|
49
67
|
pending.reject(new Error(msg.reason || "User cancelled the question"));
|
|
50
68
|
}
|
|
@@ -52,7 +70,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
52
70
|
}
|
|
53
71
|
case "location_response": {
|
|
54
72
|
const pending = coordinator.pendingLocation.get(msg.requestId);
|
|
55
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
73
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
56
74
|
coordinator.pendingLocation.delete(msg.requestId);
|
|
57
75
|
pending.resolve({ coords: msg.coords, timestamp: msg.timestamp });
|
|
58
76
|
}
|
|
@@ -60,7 +78,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
60
78
|
}
|
|
61
79
|
case "location_error": {
|
|
62
80
|
const pending = coordinator.pendingLocation.get(msg.requestId);
|
|
63
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
81
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
64
82
|
coordinator.pendingLocation.delete(msg.requestId);
|
|
65
83
|
pending.reject(new Error(locationErrorText(msg.code, msg.message)));
|
|
66
84
|
}
|
|
@@ -68,7 +86,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
68
86
|
}
|
|
69
87
|
case "mask_response": {
|
|
70
88
|
const pending = coordinator.pendingMask.get(msg.requestId);
|
|
71
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
89
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
72
90
|
coordinator.pendingMask.delete(msg.requestId);
|
|
73
91
|
// Decoded here rather than in the tool: the boundary already validated
|
|
74
92
|
// the base64 and its size, so the backend gets bytes it can trust.
|
|
@@ -78,7 +96,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
78
96
|
}
|
|
79
97
|
case "mask_error": {
|
|
80
98
|
const pending = coordinator.pendingMask.get(msg.requestId);
|
|
81
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
99
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
82
100
|
coordinator.pendingMask.delete(msg.requestId);
|
|
83
101
|
pending.reject(new Error(msg.code === "cancelled"
|
|
84
102
|
? `The user did not mark an area${msg.message ? `: ${msg.message}` : ""}`
|
|
@@ -88,7 +106,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
88
106
|
}
|
|
89
107
|
case "tool_approval": {
|
|
90
108
|
const pending = coordinator.pendingApprovals.get(msg.toolUseId);
|
|
91
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
109
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
92
110
|
coordinator.pendingApprovals.delete(msg.toolUseId);
|
|
93
111
|
pending.resolve(msg.updatedInput
|
|
94
112
|
? { behavior: "allow", updatedInput: msg.updatedInput }
|
|
@@ -98,7 +116,7 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
98
116
|
}
|
|
99
117
|
case "tool_denial": {
|
|
100
118
|
const pending = coordinator.pendingApprovals.get(msg.toolUseId);
|
|
101
|
-
if (pending && turnIdMatches(pending, msg.turnId)) {
|
|
119
|
+
if (pending && turnIdMatches(pending, msg.turnId, requireEcho)) {
|
|
102
120
|
coordinator.pendingApprovals.delete(msg.toolUseId);
|
|
103
121
|
pending.resolve({ behavior: "deny", message: msg.message });
|
|
104
122
|
}
|
|
@@ -150,10 +168,12 @@ export async function handleClientMessage(host, ws, msg) {
|
|
|
150
168
|
});
|
|
151
169
|
}
|
|
152
170
|
catch (err) {
|
|
171
|
+
const message = err instanceof Error ? err.message : "Failed to load session";
|
|
172
|
+
host.reportTurnFailed("SESSION_LOAD_ERROR", { sessionId: msg.sessionId }, message);
|
|
153
173
|
host.sendMessage(ws, {
|
|
154
174
|
type: "error",
|
|
155
175
|
code: "SESSION_LOAD_ERROR",
|
|
156
|
-
message
|
|
176
|
+
message,
|
|
157
177
|
sessionId: msg.sessionId,
|
|
158
178
|
});
|
|
159
179
|
}
|
package/dist/ws/dispatch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/ws/dispatch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/ws/dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/ws/host.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { ClientSet, type WSContext } from "./clients.js";
|
|
|
3
3
|
import { TurnCoordinator } from "./turns.js";
|
|
4
4
|
import type { SessionCatalog } from "./session-catalog.js";
|
|
5
5
|
import type { BackendRegistry } from "../agent/backend.js";
|
|
6
|
+
import { type Observability } from "../observability/index.js";
|
|
7
|
+
import { FrameRateLimiter } from "./rate-limit.js";
|
|
6
8
|
/**
|
|
7
9
|
* Budget for the host-side follow-up queue of ONE session (backends without a
|
|
8
10
|
* native `followUp`; with one, messages go into the running turn and none of
|
|
@@ -41,7 +43,34 @@ export interface WsHostOptions {
|
|
|
41
43
|
* it dynamic; createApp passes the resolved config value.
|
|
42
44
|
*/
|
|
43
45
|
maxConcurrentSessions?: () => number;
|
|
46
|
+
/**
|
|
47
|
+
* Where this coordinator reports. Injected rather than reached for, so two
|
|
48
|
+
* apps in one process report separately and a test can assert on what the
|
|
49
|
+
* socket layer actually said. Defaults to silence: an embedder that never
|
|
50
|
+
* passes one gets no output, not a surprise stream on stdout.
|
|
51
|
+
*/
|
|
52
|
+
observability?: Observability;
|
|
53
|
+
/**
|
|
54
|
+
* Inbound frame metering policy, per connection. Omitted (or a rate of 0)
|
|
55
|
+
* means no metering — which is what a test wants, and what an embedder
|
|
56
|
+
* fronting the socket with its own limiter wants.
|
|
57
|
+
*/
|
|
58
|
+
wsRate?: {
|
|
59
|
+
ratePerSecond: number;
|
|
60
|
+
burst: number;
|
|
61
|
+
};
|
|
44
62
|
}
|
|
63
|
+
/** Identity of one turn, as it appears on a log record. */
|
|
64
|
+
export interface TurnLogContext {
|
|
65
|
+
sessionId?: string | null;
|
|
66
|
+
turnId?: string | null;
|
|
67
|
+
providerId?: string | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Log attributes for one turn. Null fields are omitted rather than stringified
|
|
71
|
+
* — a new session has no sessionId until `session_info` names it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function turnLogAttributes(turn: TurnLogContext): Record<string, string>;
|
|
45
74
|
/**
|
|
46
75
|
* Everything one WebSocket coordinator instance owns: turn state, the session
|
|
47
76
|
* catalog, the backend registry, the attached client sockets, branding copy,
|
|
@@ -56,7 +85,51 @@ export declare class WsHost {
|
|
|
56
85
|
appName: string;
|
|
57
86
|
turnTimeoutMs: number;
|
|
58
87
|
maxConcurrentSessions: () => number;
|
|
88
|
+
readonly observability: Observability;
|
|
89
|
+
readonly wsRate: {
|
|
90
|
+
ratePerSecond: number;
|
|
91
|
+
burst: number;
|
|
92
|
+
} | null;
|
|
93
|
+
/** Scoped instruments, resolved once — `[ws]` is the existing log prefix. */
|
|
94
|
+
readonly log: ReturnType<Observability["logger"]>;
|
|
95
|
+
private readonly framesDropped;
|
|
96
|
+
private readonly turnsStarted;
|
|
97
|
+
private readonly turnsCompleted;
|
|
98
|
+
private readonly turnsFailed;
|
|
99
|
+
private readonly wsErrors;
|
|
59
100
|
constructor(options: WsHostOptions);
|
|
101
|
+
/** A metering bucket for one new connection, or null when metering is off. */
|
|
102
|
+
newRateLimiter(): FrameRateLimiter | null;
|
|
103
|
+
/**
|
|
104
|
+
* Record a frame this server refused, and say so.
|
|
105
|
+
*
|
|
106
|
+
* Before this existed, every rejection answered the client and vanished:
|
|
107
|
+
* `parseClientMessage` failures were never logged, so the validation we
|
|
108
|
+
* already shipped had no observability at all. `reason` is a bounded token,
|
|
109
|
+
* never the frame body — the payload is caller-supplied and can be 12 MB.
|
|
110
|
+
*/
|
|
111
|
+
reportDroppedFrame(reason: string, detail?: string): void;
|
|
112
|
+
/** A turn began executing: counted, and logged with its correlation ids. */
|
|
113
|
+
reportTurnStarted(turn: TurnLogContext): void;
|
|
114
|
+
/** A turn's backend call resolved: counted, and logged with its duration. */
|
|
115
|
+
reportTurnCompleted(turn: TurnLogContext, durationMs: number): void;
|
|
116
|
+
/**
|
|
117
|
+
* Record a turn failure the client is being told about. Before this existed
|
|
118
|
+
* every such failure was an error FRAME only — visible on one phone screen,
|
|
119
|
+
* absent from the server's own record.
|
|
120
|
+
*
|
|
121
|
+
* `code` is the bounded error-frame code (it feeds a counter attribute);
|
|
122
|
+
* `error` is the thrown message and rides only on the log record — never a
|
|
123
|
+
* caller-supplied frame body, per the reportDroppedFrame model.
|
|
124
|
+
*/
|
|
125
|
+
reportTurnFailed(code: string, turn: TurnLogContext, error?: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* Record a socket that closed abnormally. This is the transport-error signal
|
|
128
|
+
* available on Bun: hono's Bun adapter dispatches only open/message/close
|
|
129
|
+
* (never WSEvents.onError, and Bun's ServerWebSocket has no error callback),
|
|
130
|
+
* so a transport failure surfaces as a close with an abnormal code.
|
|
131
|
+
*/
|
|
132
|
+
reportAbnormalClose(code: number): void;
|
|
60
133
|
/** Fan a frame out to every attached client (size-bounded per frame). */
|
|
61
134
|
sendToClients(msg: ServerMessage): void;
|
|
62
135
|
/** Send a frame to one specific socket (size-bounded). */
|
package/dist/ws/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/ws/host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,SAAS,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/ws/host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,SAAS,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAQnD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,QAAmB,CAAC;AACjD,eAAO,MAAM,eAAe,QAAmB,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,QAAQ,EAAE,eAAe,CAAC;IAC1B,+DAA+D;IAC/D,OAAO,EAAE,cAAc,CAAC;IACxB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;IACrC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM9E;AAED;;;;;GAKG;AACH,qBAAa,MAAM;IACjB,QAAQ,CAAC,WAAW,kBAAyB;IAC7C,QAAQ,CAAC,OAAO,YAAmB;IACnC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,MAAM,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAE5B;IACF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAE3B;IACF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAE7B;IACF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAE1B;IACF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAEvB;gBAEU,OAAO,EAAE,aAAa;IA8BlC,8EAA8E;IAC9E,cAAc,IAAI,gBAAgB,GAAG,IAAI;IAIzC;;;;;;;OAOG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IASzD,4EAA4E;IAC5E,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAS7C,6EAA6E;IAC7E,mBAAmB,CAAC,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IASnE;;;;;;;;OAQG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAe1E;;;;;OAKG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASvC,yEAAyE;IACzE,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAYvC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI;CAGrD"}
|
package/dist/ws/host.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ClientSet, sendTo } from "./clients.js";
|
|
2
2
|
import { TurnCoordinator } from "./turns.js";
|
|
3
|
+
import { createSilentObservability } from "../observability/index.js";
|
|
4
|
+
import { FrameRateLimiter } from "./rate-limit.js";
|
|
3
5
|
/** Host-side turn timeout. The backend no longer times out — the host owns it. */
|
|
4
6
|
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
5
7
|
/** Default cap on concurrently RUNNING sessions (MAX_CONCURRENT_SESSIONS). */
|
|
@@ -27,6 +29,17 @@ export const QUEUE_MAX_BYTES = 50 * 1024 * 1024;
|
|
|
27
29
|
* and would run the session for days. Not the limit anyone should hit.
|
|
28
30
|
*/
|
|
29
31
|
export const MAX_SESSION_QUEUE = 50;
|
|
32
|
+
/**
|
|
33
|
+
* Log attributes for one turn. Null fields are omitted rather than stringified
|
|
34
|
+
* — a new session has no sessionId until `session_info` names it.
|
|
35
|
+
*/
|
|
36
|
+
export function turnLogAttributes(turn) {
|
|
37
|
+
return {
|
|
38
|
+
...(turn.sessionId ? { "session.id": turn.sessionId } : {}),
|
|
39
|
+
...(turn.turnId ? { "turn.id": turn.turnId } : {}),
|
|
40
|
+
...(turn.providerId ? { profile: turn.providerId } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* Everything one WebSocket coordinator instance owns: turn state, the session
|
|
32
45
|
* catalog, the backend registry, the attached client sockets, branding copy,
|
|
@@ -41,6 +54,15 @@ export class WsHost {
|
|
|
41
54
|
appName;
|
|
42
55
|
turnTimeoutMs;
|
|
43
56
|
maxConcurrentSessions;
|
|
57
|
+
observability;
|
|
58
|
+
wsRate;
|
|
59
|
+
/** Scoped instruments, resolved once — `[ws]` is the existing log prefix. */
|
|
60
|
+
log;
|
|
61
|
+
framesDropped;
|
|
62
|
+
turnsStarted;
|
|
63
|
+
turnsCompleted;
|
|
64
|
+
turnsFailed;
|
|
65
|
+
wsErrors;
|
|
44
66
|
constructor(options) {
|
|
45
67
|
this.registry = options.registry;
|
|
46
68
|
this.catalog = options.catalog;
|
|
@@ -48,10 +70,114 @@ export class WsHost {
|
|
|
48
70
|
this.turnTimeoutMs = options.turnTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
49
71
|
this.maxConcurrentSessions =
|
|
50
72
|
options.maxConcurrentSessions ?? (() => DEFAULT_MAX_CONCURRENT_SESSIONS);
|
|
73
|
+
this.observability = options.observability ?? createSilentObservability();
|
|
74
|
+
this.wsRate =
|
|
75
|
+
options.wsRate && options.wsRate.ratePerSecond > 0 ? options.wsRate : null;
|
|
76
|
+
this.log = this.observability.logger("ws");
|
|
77
|
+
const meter = this.observability.meter("ws");
|
|
78
|
+
this.framesDropped = meter.createCounter("ws.frames.dropped", {
|
|
79
|
+
description: "Inbound frames refused before reaching a handler",
|
|
80
|
+
});
|
|
81
|
+
this.turnsStarted = meter.createCounter("turns.started", {
|
|
82
|
+
description: "Turns handed to a backend",
|
|
83
|
+
});
|
|
84
|
+
this.turnsCompleted = meter.createCounter("turns.completed", {
|
|
85
|
+
description: "Turns whose backend call resolved without throwing",
|
|
86
|
+
});
|
|
87
|
+
this.turnsFailed = meter.createCounter("turns.failed", {
|
|
88
|
+
description: "Turn failures surfaced to the client, by error code",
|
|
89
|
+
});
|
|
90
|
+
this.wsErrors = meter.createCounter("ws.errors", {
|
|
91
|
+
description: "Transport errors reported by the socket layer",
|
|
92
|
+
});
|
|
93
|
+
this.coordinator.log = this.log;
|
|
94
|
+
}
|
|
95
|
+
/** A metering bucket for one new connection, or null when metering is off. */
|
|
96
|
+
newRateLimiter() {
|
|
97
|
+
return this.wsRate ? new FrameRateLimiter(this.wsRate) : null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Record a frame this server refused, and say so.
|
|
101
|
+
*
|
|
102
|
+
* Before this existed, every rejection answered the client and vanished:
|
|
103
|
+
* `parseClientMessage` failures were never logged, so the validation we
|
|
104
|
+
* already shipped had no observability at all. `reason` is a bounded token,
|
|
105
|
+
* never the frame body — the payload is caller-supplied and can be 12 MB.
|
|
106
|
+
*/
|
|
107
|
+
reportDroppedFrame(reason, detail) {
|
|
108
|
+
this.framesDropped.add(1, { reason, direction: "inbound" });
|
|
109
|
+
this.log.emit({
|
|
110
|
+
severityText: "WARN",
|
|
111
|
+
body: "inbound frame rejected",
|
|
112
|
+
attributes: detail ? { reason, detail } : { reason },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** A turn began executing: counted, and logged with its correlation ids. */
|
|
116
|
+
reportTurnStarted(turn) {
|
|
117
|
+
this.turnsStarted.add(1);
|
|
118
|
+
this.log.emit({
|
|
119
|
+
severityText: "INFO",
|
|
120
|
+
body: "turn started",
|
|
121
|
+
attributes: turnLogAttributes(turn),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** A turn's backend call resolved: counted, and logged with its duration. */
|
|
125
|
+
reportTurnCompleted(turn, durationMs) {
|
|
126
|
+
this.turnsCompleted.add(1);
|
|
127
|
+
this.log.emit({
|
|
128
|
+
severityText: "INFO",
|
|
129
|
+
body: "turn completed",
|
|
130
|
+
attributes: { ...turnLogAttributes(turn), "duration.ms": durationMs },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Record a turn failure the client is being told about. Before this existed
|
|
135
|
+
* every such failure was an error FRAME only — visible on one phone screen,
|
|
136
|
+
* absent from the server's own record.
|
|
137
|
+
*
|
|
138
|
+
* `code` is the bounded error-frame code (it feeds a counter attribute);
|
|
139
|
+
* `error` is the thrown message and rides only on the log record — never a
|
|
140
|
+
* caller-supplied frame body, per the reportDroppedFrame model.
|
|
141
|
+
*/
|
|
142
|
+
reportTurnFailed(code, turn, error) {
|
|
143
|
+
this.turnsFailed.add(1, { code });
|
|
144
|
+
this.log.emit({
|
|
145
|
+
// A busy session is the client racing itself; everything else is a
|
|
146
|
+
// failure the operator should see.
|
|
147
|
+
severityText: code === "SESSION_BUSY" ? "WARN" : "ERROR",
|
|
148
|
+
body: "turn failed",
|
|
149
|
+
attributes: {
|
|
150
|
+
code,
|
|
151
|
+
...turnLogAttributes(turn),
|
|
152
|
+
...(error ? { error } : {}),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Record a socket that closed abnormally. This is the transport-error signal
|
|
158
|
+
* available on Bun: hono's Bun adapter dispatches only open/message/close
|
|
159
|
+
* (never WSEvents.onError, and Bun's ServerWebSocket has no error callback),
|
|
160
|
+
* so a transport failure surfaces as a close with an abnormal code.
|
|
161
|
+
*/
|
|
162
|
+
reportAbnormalClose(code) {
|
|
163
|
+
this.wsErrors.add(1, { "close.code": code });
|
|
164
|
+
this.log.emit({
|
|
165
|
+
severityText: "WARN",
|
|
166
|
+
body: "websocket closed abnormally",
|
|
167
|
+
attributes: { "close.code": code },
|
|
168
|
+
});
|
|
51
169
|
}
|
|
52
170
|
/** Fan a frame out to every attached client (size-bounded per frame). */
|
|
53
171
|
sendToClients(msg) {
|
|
54
|
-
this.clients.broadcast(msg)
|
|
172
|
+
this.clients.broadcast(msg, () => {
|
|
173
|
+
// Outbound counterpart of reportDroppedFrame: the peer never saw this
|
|
174
|
+
// frame. Counted only — a dead socket would otherwise WARN per frame
|
|
175
|
+
// until its onClose prunes it.
|
|
176
|
+
this.framesDropped.add(1, {
|
|
177
|
+
reason: "broadcast_send_failed",
|
|
178
|
+
direction: "outbound",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
55
181
|
}
|
|
56
182
|
/** Send a frame to one specific socket (size-bounded). */
|
|
57
183
|
sendMessage(ws, msg) {
|
package/dist/ws/host.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/ws/host.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAkB,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/ws/host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-connection inbound frame metering.
|
|
3
|
+
*
|
|
4
|
+
* Frames are already size-, cardinality- and depth-bounded, so the cheap CPU
|
|
5
|
+
* amplification is closed. What was left unmetered is VOLUME: a flood of
|
|
6
|
+
* individually valid frames behind the auth guard. That is a smaller threat
|
|
7
|
+
* than an unauthenticated one — it needs a session — but "the attacker had to
|
|
8
|
+
* log in first" is not a bound.
|
|
9
|
+
*
|
|
10
|
+
* A token bucket rather than a fixed window, because the traffic this protects
|
|
11
|
+
* is bursty by nature and a fixed window punishes the wrong thing. Opening the
|
|
12
|
+
* app fires a handful of frames at once (resume, environment, a first
|
|
13
|
+
* message); an approval storm during a busy turn is a dozen in a second. Both
|
|
14
|
+
* are legitimate and both fit in the burst. What does not fit is a sustained
|
|
15
|
+
* rate no human produces.
|
|
16
|
+
*
|
|
17
|
+
* Inbound traffic is naturally low-rate — deltas flow the other way — so the
|
|
18
|
+
* defaults are generous enough that no real client should ever see one of
|
|
19
|
+
* these, which is the property that makes it safe to turn on by default.
|
|
20
|
+
*/
|
|
21
|
+
/** Injected so tests need no real time. */
|
|
22
|
+
export interface Clock {
|
|
23
|
+
now(): number;
|
|
24
|
+
}
|
|
25
|
+
export interface RateLimitOptions {
|
|
26
|
+
/** Sustained frames per second. */
|
|
27
|
+
ratePerSecond: number;
|
|
28
|
+
/** Frames absorbable in one burst before the sustained rate applies. */
|
|
29
|
+
burst: number;
|
|
30
|
+
clock?: Clock;
|
|
31
|
+
}
|
|
32
|
+
export interface RateLimitDecision {
|
|
33
|
+
allowed: boolean;
|
|
34
|
+
/** Tokens left after this decision, for reporting. */
|
|
35
|
+
remaining: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One bucket per connection. Not shared, and not keyed by anything
|
|
39
|
+
* user-supplied: a map keyed by session or IP is itself an unbounded
|
|
40
|
+
* allocation an attacker controls, which is the classic way a rate limiter
|
|
41
|
+
* becomes the memory-exhaustion bug it was added to prevent. This lives on the
|
|
42
|
+
* socket and dies with it.
|
|
43
|
+
*/
|
|
44
|
+
export declare class FrameRateLimiter {
|
|
45
|
+
private tokens;
|
|
46
|
+
private lastRefill;
|
|
47
|
+
private readonly ratePerMs;
|
|
48
|
+
private readonly burst;
|
|
49
|
+
private readonly clock;
|
|
50
|
+
/** Frames refused since the connection opened. */
|
|
51
|
+
private rejected;
|
|
52
|
+
constructor(options: RateLimitOptions);
|
|
53
|
+
/** Take one token. False means this frame should be refused. */
|
|
54
|
+
take(): RateLimitDecision;
|
|
55
|
+
/** How many frames this connection has had refused. */
|
|
56
|
+
get rejectedCount(): number;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/ws/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,MAAM,CAAC;CACf;AAID,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,kDAAkD;IAClD,OAAO,CAAC,QAAQ,CAAK;gBAET,OAAO,EAAE,gBAAgB;IAQrC,gEAAgE;IAChE,IAAI,IAAI,iBAAiB;IAczB,uDAAuD;IACvD,IAAI,aAAa,IAAI,MAAM,CAE1B;CACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-connection inbound frame metering.
|
|
3
|
+
*
|
|
4
|
+
* Frames are already size-, cardinality- and depth-bounded, so the cheap CPU
|
|
5
|
+
* amplification is closed. What was left unmetered is VOLUME: a flood of
|
|
6
|
+
* individually valid frames behind the auth guard. That is a smaller threat
|
|
7
|
+
* than an unauthenticated one — it needs a session — but "the attacker had to
|
|
8
|
+
* log in first" is not a bound.
|
|
9
|
+
*
|
|
10
|
+
* A token bucket rather than a fixed window, because the traffic this protects
|
|
11
|
+
* is bursty by nature and a fixed window punishes the wrong thing. Opening the
|
|
12
|
+
* app fires a handful of frames at once (resume, environment, a first
|
|
13
|
+
* message); an approval storm during a busy turn is a dozen in a second. Both
|
|
14
|
+
* are legitimate and both fit in the burst. What does not fit is a sustained
|
|
15
|
+
* rate no human produces.
|
|
16
|
+
*
|
|
17
|
+
* Inbound traffic is naturally low-rate — deltas flow the other way — so the
|
|
18
|
+
* defaults are generous enough that no real client should ever see one of
|
|
19
|
+
* these, which is the property that makes it safe to turn on by default.
|
|
20
|
+
*/
|
|
21
|
+
const realClock = { now: () => Date.now() };
|
|
22
|
+
/**
|
|
23
|
+
* One bucket per connection. Not shared, and not keyed by anything
|
|
24
|
+
* user-supplied: a map keyed by session or IP is itself an unbounded
|
|
25
|
+
* allocation an attacker controls, which is the classic way a rate limiter
|
|
26
|
+
* becomes the memory-exhaustion bug it was added to prevent. This lives on the
|
|
27
|
+
* socket and dies with it.
|
|
28
|
+
*/
|
|
29
|
+
export class FrameRateLimiter {
|
|
30
|
+
tokens;
|
|
31
|
+
lastRefill;
|
|
32
|
+
ratePerMs;
|
|
33
|
+
burst;
|
|
34
|
+
clock;
|
|
35
|
+
/** Frames refused since the connection opened. */
|
|
36
|
+
rejected = 0;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.burst = Math.max(1, options.burst);
|
|
39
|
+
this.ratePerMs = Math.max(0, options.ratePerSecond) / 1000;
|
|
40
|
+
this.clock = options.clock ?? realClock;
|
|
41
|
+
this.tokens = this.burst;
|
|
42
|
+
this.lastRefill = this.clock.now();
|
|
43
|
+
}
|
|
44
|
+
/** Take one token. False means this frame should be refused. */
|
|
45
|
+
take() {
|
|
46
|
+
const now = this.clock.now();
|
|
47
|
+
const elapsed = Math.max(0, now - this.lastRefill);
|
|
48
|
+
this.lastRefill = now;
|
|
49
|
+
this.tokens = Math.min(this.burst, this.tokens + elapsed * this.ratePerMs);
|
|
50
|
+
if (this.tokens < 1) {
|
|
51
|
+
this.rejected++;
|
|
52
|
+
return { allowed: false, remaining: 0 };
|
|
53
|
+
}
|
|
54
|
+
this.tokens -= 1;
|
|
55
|
+
return { allowed: true, remaining: Math.floor(this.tokens) };
|
|
56
|
+
}
|
|
57
|
+
/** How many frames this connection has had refused. */
|
|
58
|
+
get rejectedCount() {
|
|
59
|
+
return this.rejected;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/ws/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,MAAM,SAAS,GAAU,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AAgBnD;;;;;;GAMG;AACH,MAAM,OAAO,gBAAgB;IACnB,MAAM,CAAS;IACf,UAAU,CAAS;IACV,SAAS,CAAS;IAClB,KAAK,CAAS;IACd,KAAK,CAAQ;IAC9B,kDAAkD;IAC1C,QAAQ,GAAG,CAAC,CAAC;IAErB,YAAY,OAAyB;QACnC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;QAC3D,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,SAAS,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,gEAAgE;IAChE,IAAI;QACF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;QACnD,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAE3E,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;QACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IAC/D,CAAC;IAED,uDAAuD;IACvD,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF"}
|
package/dist/ws/run-session.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare function runSession(host: WsHost, initial: {
|
|
|
12
12
|
attachments: ChatImageAttachment[];
|
|
13
13
|
providerId?: string;
|
|
14
14
|
client?: ClientEnvironment;
|
|
15
|
+
/** Client correlation id for a new conversation; echoed on session_info. */
|
|
16
|
+
draftId?: string;
|
|
15
17
|
}): Promise<void>;
|
|
16
18
|
/** Dispatch a chat_message: follow-up to a running session, or a new session. */
|
|
17
19
|
export declare function handleChatMessage(host: WsHost, ws: WSContext, msg: {
|
|
@@ -20,5 +22,6 @@ export declare function handleChatMessage(host: WsHost, ws: WSContext, msg: {
|
|
|
20
22
|
attachments: ChatImageAttachment[];
|
|
21
23
|
providerId?: string;
|
|
22
24
|
client?: ClientEnvironment;
|
|
25
|
+
draftId?: string;
|
|
23
26
|
}): Promise<void>;
|
|
24
27
|
//# sourceMappingURL=run-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-session.d.ts","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAM9C,OAAO,
|
|
1
|
+
{"version":3,"file":"run-session.d.ts","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAM9C,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,WAAW,CAAC;AAEnB;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,IAAI,CAAC,CAoIf;AAOD,iFAAiF;AACjF,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,SAAS,EACb,GAAG,EAAE;IACH,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,IAAI,CAAC,CAkGf"}
|