agenthooksprotocol 0.0.0-bootstrap.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/LICENSE +202 -0
- package/README.md +29 -0
- package/dist/src/client/auth.d.ts +147 -0
- package/dist/src/client/auth.d.ts.map +1 -0
- package/dist/src/client/auth.js +629 -0
- package/dist/src/client/auth.js.map +1 -0
- package/dist/src/client/composition.d.ts +42 -0
- package/dist/src/client/composition.d.ts.map +1 -0
- package/dist/src/client/composition.js +523 -0
- package/dist/src/client/composition.js.map +1 -0
- package/dist/src/client/content.d.ts +73 -0
- package/dist/src/client/content.d.ts.map +1 -0
- package/dist/src/client/content.js +404 -0
- package/dist/src/client/content.js.map +1 -0
- package/dist/src/client/hooks.d.ts +62 -0
- package/dist/src/client/hooks.d.ts.map +1 -0
- package/dist/src/client/hooks.js +997 -0
- package/dist/src/client/hooks.js.map +1 -0
- package/dist/src/client/index.d.ts +11 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +9 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/transport.d.ts +12 -0
- package/dist/src/client/transport.d.ts.map +1 -0
- package/dist/src/client/transport.js +347 -0
- package/dist/src/client/transport.js.map +1 -0
- package/dist/src/client/types.d.ts +117 -0
- package/dist/src/client/types.d.ts.map +1 -0
- package/dist/src/client/types.js +10 -0
- package/dist/src/client/types.js.map +1 -0
- package/dist/src/client/validation.d.ts +3 -0
- package/dist/src/client/validation.d.ts.map +1 -0
- package/dist/src/client/validation.js +25 -0
- package/dist/src/client/validation.js.map +1 -0
- package/dist/src/compaction.d.ts +27 -0
- package/dist/src/compaction.d.ts.map +1 -0
- package/dist/src/compaction.js +155 -0
- package/dist/src/compaction.js.map +1 -0
- package/dist/src/content-upload.d.ts +60 -0
- package/dist/src/content-upload.d.ts.map +1 -0
- package/dist/src/content-upload.js +129 -0
- package/dist/src/content-upload.js.map +1 -0
- package/dist/src/draft/generated.d.ts +3085 -0
- package/dist/src/draft/generated.d.ts.map +1 -0
- package/dist/src/draft/generated.js +15372 -0
- package/dist/src/draft/generated.js.map +1 -0
- package/dist/src/draft/index.d.ts +31 -0
- package/dist/src/draft/index.d.ts.map +1 -0
- package/dist/src/draft/index.js +129 -0
- package/dist/src/draft/index.js.map +1 -0
- package/dist/src/draft/schemas.d.ts +8848 -0
- package/dist/src/draft/schemas.d.ts.map +1 -0
- package/dist/src/draft/schemas.js +9219 -0
- package/dist/src/draft/schemas.js.map +1 -0
- package/dist/src/draft-atomic.d.ts +18 -0
- package/dist/src/draft-atomic.d.ts.map +1 -0
- package/dist/src/draft-atomic.js +102 -0
- package/dist/src/draft-atomic.js.map +1 -0
- package/dist/src/draft-runtime.d.ts +89 -0
- package/dist/src/draft-runtime.d.ts.map +1 -0
- package/dist/src/draft-runtime.js +242 -0
- package/dist/src/draft-runtime.js.map +1 -0
- package/dist/src/elicitation.d.ts +18 -0
- package/dist/src/elicitation.d.ts.map +1 -0
- package/dist/src/elicitation.js +191 -0
- package/dist/src/elicitation.js.map +1 -0
- package/dist/src/errors.d.ts +7 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +15 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/framing.d.ts +7 -0
- package/dist/src/framing.d.ts.map +1 -0
- package/dist/src/framing.js +39 -0
- package/dist/src/framing.js.map +1 -0
- package/dist/src/generated.d.ts +304 -0
- package/dist/src/generated.d.ts.map +1 -0
- package/dist/src/generated.js +1885 -0
- package/dist/src/generated.js.map +1 -0
- package/dist/src/hook-runner.d.ts +11 -0
- package/dist/src/hook-runner.d.ts.map +1 -0
- package/dist/src/hook-runner.js +223 -0
- package/dist/src/hook-runner.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/observation.d.ts +26 -0
- package/dist/src/observation.d.ts.map +1 -0
- package/dist/src/observation.js +28 -0
- package/dist/src/observation.js.map +1 -0
- package/dist/src/server/attachments.d.ts +29 -0
- package/dist/src/server/attachments.d.ts.map +1 -0
- package/dist/src/server/attachments.js +112 -0
- package/dist/src/server/attachments.js.map +1 -0
- package/dist/src/server/hooks.d.ts +20 -0
- package/dist/src/server/hooks.d.ts.map +1 -0
- package/dist/src/server/hooks.js +129 -0
- package/dist/src/server/hooks.js.map +1 -0
- package/dist/src/server/index.d.ts +7 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +4 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/stdio.d.ts +15 -0
- package/dist/src/server/stdio.d.ts.map +1 -0
- package/dist/src/server/stdio.js +69 -0
- package/dist/src/server/stdio.js.map +1 -0
- package/dist/src/transport.d.ts +7 -0
- package/dist/src/transport.d.ts.map +1 -0
- package/dist/src/transport.js +195 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/types.d.ts +116 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +14 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validation.d.ts +7 -0
- package/dist/src/validation.d.ts.map +1 -0
- package/dist/src/validation.js +209 -0
- package/dist/src/validation.js.map +1 -0
- package/package.json +64 -0
- package/src/client/auth.ts +1076 -0
- package/src/client/composition.ts +708 -0
- package/src/client/content.ts +510 -0
- package/src/client/hooks.ts +1368 -0
- package/src/client/index.ts +28 -0
- package/src/client/transport.ts +408 -0
- package/src/client/types.ts +173 -0
- package/src/client/validation.ts +24 -0
- package/src/compaction.ts +196 -0
- package/src/content-upload.ts +205 -0
- package/src/draft/README.md +16 -0
- package/src/draft/ahp-codegen.lock.json +123 -0
- package/src/draft/generated.ts +19143 -0
- package/src/draft/index.ts +246 -0
- package/src/draft/schemas.ts +9413 -0
- package/src/draft-atomic.ts +119 -0
- package/src/draft-runtime.ts +365 -0
- package/src/elicitation.ts +254 -0
- package/src/errors.ts +30 -0
- package/src/framing.ts +56 -0
- package/src/generated.ts +2408 -0
- package/src/hook-runner.ts +310 -0
- package/src/index.ts +8 -0
- package/src/observation.ts +65 -0
- package/src/server/attachments.ts +128 -0
- package/src/server/hooks.ts +205 -0
- package/src/server/index.ts +18 -0
- package/src/server/node-crypto.d.ts +7 -0
- package/src/server/stdio.ts +81 -0
- package/src/transport.ts +273 -0
- package/src/types.ts +124 -0
- package/src/validation.ts +372 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/** MCP 2025-11-25 body binding. Host-owned resolver must verify upload ownership,
|
|
2
|
+
* length and digest before returning bytes. Identity comes from authentication,
|
|
3
|
+
* never event.source or elicitation.server. */
|
|
4
|
+
export function validateElicitationExchange(
|
|
5
|
+
request: any,
|
|
6
|
+
result: any,
|
|
7
|
+
resolve: (reference: any) => Uint8Array,
|
|
8
|
+
validate: (schema: string, value: unknown) => void,
|
|
9
|
+
principal: string,
|
|
10
|
+
effect?: { type: string; target?: string; operation?: string },
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
if (!principal) throw Error("Authenticated principal required");
|
|
13
|
+
for (const envelope of [request, result]) {
|
|
14
|
+
validate("intercept-request", envelope);
|
|
15
|
+
if (envelope.id !== envelope.params.event.id)
|
|
16
|
+
throw Error("Request/event ID mismatch");
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
request.params.event.type !== "user.elicitation.request" ||
|
|
20
|
+
result.params.event.type !== "user.elicitation.result"
|
|
21
|
+
)
|
|
22
|
+
throw Error("Wrong elicitation boundary");
|
|
23
|
+
validateElicitationCorrelation(request, result);
|
|
24
|
+
const req = request.params.event.elicitation,
|
|
25
|
+
res = result.params.event.elicitation;
|
|
26
|
+
const payload = readSelectedElicitation(req, "request", resolve, validate);
|
|
27
|
+
const answer = readSelectedElicitation(res, "result", resolve, validate);
|
|
28
|
+
if (req.mode !== res.mode || req.server !== res.server)
|
|
29
|
+
throw Error("Normalized metadata mismatch");
|
|
30
|
+
if (payload === null || answer === null) {
|
|
31
|
+
if (effect !== undefined) throw Error("Effects require selected bodies");
|
|
32
|
+
return {
|
|
33
|
+
selection: {
|
|
34
|
+
request: req.request?.selection ?? "omit",
|
|
35
|
+
result: res.result?.selection ?? "omit",
|
|
36
|
+
},
|
|
37
|
+
bodyValidation: "not-selected",
|
|
38
|
+
provenance: { kind: "mcp", authenticatedSource: principal },
|
|
39
|
+
externalCompletion: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
validateElicitationAnswer(payload, answer, validate);
|
|
43
|
+
const mode = payload.mode ?? "form";
|
|
44
|
+
if (
|
|
45
|
+
req.mode !== mode ||
|
|
46
|
+
res.mode !== mode ||
|
|
47
|
+
req.server !== res.server ||
|
|
48
|
+
res.action !== answer.action
|
|
49
|
+
)
|
|
50
|
+
throw Error("Normalized metadata mismatch");
|
|
51
|
+
if ((mode === "url" || answer.action !== "accept") && "content" in answer)
|
|
52
|
+
throw Error("Content is only permitted for accepted forms");
|
|
53
|
+
let provenance: Record<string, string> = {
|
|
54
|
+
kind: "mcp",
|
|
55
|
+
authenticatedSource: principal,
|
|
56
|
+
};
|
|
57
|
+
if (effect !== undefined) {
|
|
58
|
+
validate("effect", effect);
|
|
59
|
+
const kind = effect.type,
|
|
60
|
+
boundary = kind === "modify" ? result : request;
|
|
61
|
+
if (
|
|
62
|
+
!["return", "deny", "modify"].includes(kind) ||
|
|
63
|
+
!boundary.params.capabilities.effects.includes(kind)
|
|
64
|
+
)
|
|
65
|
+
throw Error("Effect not granted at this boundary");
|
|
66
|
+
if (
|
|
67
|
+
kind === "modify" &&
|
|
68
|
+
(effect.target !== "content" ||
|
|
69
|
+
!["replace", "merge"].includes(effect.operation ?? "") ||
|
|
70
|
+
boundary.params.capabilities.modify?.content?.[
|
|
71
|
+
effect.operation ?? ""
|
|
72
|
+
] !== true)
|
|
73
|
+
)
|
|
74
|
+
throw Error("Modify target/operation not granted");
|
|
75
|
+
provenance = { kind: "hook", authenticatedSource: principal, effect: kind };
|
|
76
|
+
}
|
|
77
|
+
return structuredClone({
|
|
78
|
+
request: payload,
|
|
79
|
+
result: answer,
|
|
80
|
+
provenance,
|
|
81
|
+
externalCompletion: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateElicitationAnswer(
|
|
86
|
+
request: any,
|
|
87
|
+
answer: any,
|
|
88
|
+
validate: (name: string, value: any) => void,
|
|
89
|
+
): void {
|
|
90
|
+
validate("mcp-elicitation#result", answer);
|
|
91
|
+
const mode = request.mode ?? "form";
|
|
92
|
+
if ((mode === "url" || answer.action !== "accept") && "content" in answer)
|
|
93
|
+
throw Error("Content is only permitted for accepted forms");
|
|
94
|
+
if (mode === "form" && answer.action === "accept")
|
|
95
|
+
validate("form-answer", {
|
|
96
|
+
schema: request.requestedSchema,
|
|
97
|
+
value: answer.content ?? {},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function validateElicitationMode(
|
|
102
|
+
mode: string,
|
|
103
|
+
capabilities?: Record<string, unknown>,
|
|
104
|
+
origin = "ahp",
|
|
105
|
+
): Record<string, unknown> {
|
|
106
|
+
if (!["ahp", "mcp"].includes(origin) || !["form", "url"].includes(mode))
|
|
107
|
+
throw Error("Unknown origin or mode");
|
|
108
|
+
let caps = structuredClone(capabilities ?? {});
|
|
109
|
+
if (
|
|
110
|
+
typeof caps !== "object" ||
|
|
111
|
+
caps === null ||
|
|
112
|
+
Array.isArray(caps) ||
|
|
113
|
+
Object.entries(caps).some(
|
|
114
|
+
([k, v]) =>
|
|
115
|
+
["form", "url"].includes(k) &&
|
|
116
|
+
(typeof v !== "object" || v === null || Array.isArray(v)),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
throw Error("Invalid elicitation capabilities");
|
|
120
|
+
if (
|
|
121
|
+
origin === "mcp" &&
|
|
122
|
+
capabilities !== undefined &&
|
|
123
|
+
Object.keys(capabilities).length === 0
|
|
124
|
+
)
|
|
125
|
+
caps = { form: {} };
|
|
126
|
+
if (!(mode in caps)) throw Error("Elicitation mode not registered");
|
|
127
|
+
return caps;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Stage the complete effect list on a snapshot; publish only after validation.
|
|
131
|
+
* The caller uploads the resulting complete JSON before emitting a result event. */
|
|
132
|
+
export function applyElicitationEffects(
|
|
133
|
+
request: any,
|
|
134
|
+
result: any | null,
|
|
135
|
+
resolve: (reference: any) => Uint8Array,
|
|
136
|
+
validate: (schema: string, value: any) => void,
|
|
137
|
+
principal: string,
|
|
138
|
+
effects: any[],
|
|
139
|
+
): Record<string, any> {
|
|
140
|
+
if (!principal || !Array.isArray(effects) || !effects.length)
|
|
141
|
+
throw Error("Authenticated effects required");
|
|
142
|
+
validate("intercept-request", request);
|
|
143
|
+
if (
|
|
144
|
+
request.id !== request.params.event.id ||
|
|
145
|
+
request.params.event.type !== "user.elicitation.request"
|
|
146
|
+
)
|
|
147
|
+
throw Error("Invalid request boundary");
|
|
148
|
+
if (result !== null) validateElicitationCorrelation(request, result);
|
|
149
|
+
const meta = request.params.event.elicitation;
|
|
150
|
+
const payload = readSelectedElicitation(meta, "request", resolve, validate);
|
|
151
|
+
if (payload === null) throw Error("Effects require selected request body");
|
|
152
|
+
if (meta.mode !== (payload.mode ?? "form")) throw Error("Mode mismatch");
|
|
153
|
+
const boundary = result ?? request,
|
|
154
|
+
caps = boundary.params.capabilities,
|
|
155
|
+
kinds: string[] = [];
|
|
156
|
+
let staged: any =
|
|
157
|
+
result === null
|
|
158
|
+
? null
|
|
159
|
+
: validateElicitationExchange(
|
|
160
|
+
request,
|
|
161
|
+
result,
|
|
162
|
+
resolve,
|
|
163
|
+
validate,
|
|
164
|
+
principal,
|
|
165
|
+
).result;
|
|
166
|
+
if (result !== null && staged === undefined)
|
|
167
|
+
throw Error("Effects require selected bodies");
|
|
168
|
+
for (const effect of effects) {
|
|
169
|
+
validate("effect", effect);
|
|
170
|
+
const kind = effect.type;
|
|
171
|
+
if (
|
|
172
|
+
!caps.effects.includes(kind) ||
|
|
173
|
+
!(result === null ? ["return", "deny"] : ["modify"]).includes(kind)
|
|
174
|
+
)
|
|
175
|
+
throw Error("Effect not granted at this boundary");
|
|
176
|
+
if (kind === "return" || kind === "deny") {
|
|
177
|
+
if (kinds.length) throw Error("Conflicting terminal effects");
|
|
178
|
+
staged =
|
|
179
|
+
kind === "return"
|
|
180
|
+
? structuredClone(effect.value)
|
|
181
|
+
: { action: "decline" };
|
|
182
|
+
} else {
|
|
183
|
+
if (
|
|
184
|
+
effect.target !== "content" ||
|
|
185
|
+
!["replace", "merge"].includes(effect.operation) ||
|
|
186
|
+
caps.modify?.content?.[effect.operation] !== true
|
|
187
|
+
)
|
|
188
|
+
throw Error("Modify target/operation not granted");
|
|
189
|
+
staged.content =
|
|
190
|
+
effect.operation === "replace"
|
|
191
|
+
? structuredClone(effect.value)
|
|
192
|
+
: { ...staged.content, ...structuredClone(effect.value) };
|
|
193
|
+
}
|
|
194
|
+
kinds.push(kind);
|
|
195
|
+
}
|
|
196
|
+
validateElicitationAnswer(payload, staged, validate);
|
|
197
|
+
return {
|
|
198
|
+
request: payload,
|
|
199
|
+
result: staged,
|
|
200
|
+
provenance: {
|
|
201
|
+
kind: "hook",
|
|
202
|
+
authenticatedSource: principal,
|
|
203
|
+
effects: kinds,
|
|
204
|
+
},
|
|
205
|
+
externalCompletion: false,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Metadata and omit never resolve or parse content. Selected gaps fail closed. */
|
|
210
|
+
export function readSelectedElicitation(
|
|
211
|
+
meta: any,
|
|
212
|
+
stage: "request" | "result",
|
|
213
|
+
resolve: (reference: any) => Uint8Array,
|
|
214
|
+
validate: (schema: string, value: any) => void,
|
|
215
|
+
): any | null {
|
|
216
|
+
const item = meta[stage];
|
|
217
|
+
if (item === undefined) return null;
|
|
218
|
+
validate("content-item", item);
|
|
219
|
+
if (item.mediaType !== "application/json")
|
|
220
|
+
throw Error("MCP body must be application/json");
|
|
221
|
+
if (item.selection !== "body") return null;
|
|
222
|
+
if (!item.body) throw Error("Selected body unavailable (fail closed)");
|
|
223
|
+
const payload = JSON.parse(
|
|
224
|
+
new TextDecoder("utf-8", { fatal: true }).decode(resolve(item.body)),
|
|
225
|
+
);
|
|
226
|
+
validate("mcp-elicitation#" + stage, payload);
|
|
227
|
+
if (stage === "request" && meta.mode !== (payload.mode ?? "form"))
|
|
228
|
+
throw Error("Mode mismatch");
|
|
229
|
+
if (stage === "result") {
|
|
230
|
+
if (meta.action !== payload.action) throw Error("Action mismatch");
|
|
231
|
+
if (
|
|
232
|
+
(meta.mode === "url" || payload.action !== "accept") &&
|
|
233
|
+
"content" in payload
|
|
234
|
+
)
|
|
235
|
+
throw Error("Forbidden content");
|
|
236
|
+
}
|
|
237
|
+
return payload;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Require source-scoped parent identity and identical session scope before IO. */
|
|
241
|
+
export function validateElicitationCorrelation(
|
|
242
|
+
request: any,
|
|
243
|
+
result: any,
|
|
244
|
+
): void {
|
|
245
|
+
const req = request.params.event,
|
|
246
|
+
res = result.params.event;
|
|
247
|
+
if (
|
|
248
|
+
typeof res.parentEventId !== "string" ||
|
|
249
|
+
res.parentEventId !== req.id ||
|
|
250
|
+
res.source !== req.source ||
|
|
251
|
+
res.session?.id !== req.session?.id
|
|
252
|
+
)
|
|
253
|
+
throw Error("Elicitation parent/source/session mismatch");
|
|
254
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type HookErrorCode =
|
|
2
|
+
| "INVALID_CONFIG"
|
|
3
|
+
| "SPAWN_ERROR"
|
|
4
|
+
| "IO_ERROR"
|
|
5
|
+
| "TIMEOUT"
|
|
6
|
+
| "MALFORMED_UTF8"
|
|
7
|
+
| "MALFORMED_JSON"
|
|
8
|
+
| "MALFORMED_JSON_RPC"
|
|
9
|
+
| "ID_MISMATCH"
|
|
10
|
+
| "JSON_RPC_ERROR"
|
|
11
|
+
| "INCOMPATIBLE_VERSION"
|
|
12
|
+
| "UNSUPPORTED_EVENT"
|
|
13
|
+
| "UNSUPPORTED_EFFECT"
|
|
14
|
+
| "MULTIPLE_EFFECTS";
|
|
15
|
+
|
|
16
|
+
export class HookOperationalError extends Error {
|
|
17
|
+
readonly code: HookErrorCode;
|
|
18
|
+
|
|
19
|
+
constructor(code: HookErrorCode, message: string, options?: ErrorOptions) {
|
|
20
|
+
super(message, options);
|
|
21
|
+
this.name = "HookOperationalError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function asOperationalError(error: unknown): HookOperationalError {
|
|
27
|
+
if (error instanceof HookOperationalError) return error;
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
return new HookOperationalError("IO_ERROR", message, { cause: error });
|
|
30
|
+
}
|
package/src/framing.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HookOperationalError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
const MAX_LINE_BYTES = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
/** Incremental, strict UTF-8 NDJSON decoder. Empty lines are not frames. */
|
|
6
|
+
export class NdjsonDecoder {
|
|
7
|
+
#buffer = Buffer.alloc(0);
|
|
8
|
+
readonly #decoder = new TextDecoder("utf-8", { fatal: true });
|
|
9
|
+
|
|
10
|
+
push(chunk: Uint8Array): string[] {
|
|
11
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
12
|
+
if (this.#buffer.length > MAX_LINE_BYTES && !this.#buffer.includes(0x0a)) {
|
|
13
|
+
throw new HookOperationalError(
|
|
14
|
+
"MALFORMED_JSON_RPC",
|
|
15
|
+
"NDJSON frame exceeds 1 MiB",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
let newline: number;
|
|
20
|
+
while ((newline = this.#buffer.indexOf(0x0a)) !== -1) {
|
|
21
|
+
let frame = this.#buffer.subarray(0, newline);
|
|
22
|
+
this.#buffer = this.#buffer.subarray(newline + 1);
|
|
23
|
+
if (frame.length > MAX_LINE_BYTES)
|
|
24
|
+
throw new HookOperationalError(
|
|
25
|
+
"MALFORMED_JSON_RPC",
|
|
26
|
+
"NDJSON frame exceeds 1 MiB",
|
|
27
|
+
);
|
|
28
|
+
if (frame.at(-1) === 0x0d) frame = frame.subarray(0, -1);
|
|
29
|
+
if (frame.length === 0) {
|
|
30
|
+
throw new HookOperationalError(
|
|
31
|
+
"MALFORMED_JSON_RPC",
|
|
32
|
+
"Empty NDJSON frame",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
lines.push(this.#decoder.decode(frame));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new HookOperationalError(
|
|
39
|
+
"MALFORMED_UTF8",
|
|
40
|
+
"NDJSON frame is not valid UTF-8",
|
|
41
|
+
{ cause: error },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
end(): void {
|
|
49
|
+
if (this.#buffer.length !== 0) {
|
|
50
|
+
throw new HookOperationalError(
|
|
51
|
+
"MALFORMED_JSON_RPC",
|
|
52
|
+
"Output ended with an incomplete NDJSON frame",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|