@juspay/neurolink 13.8.10 → 13.8.12
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/CHANGELOG.md +2 -3
- package/dist/browser/neurolink.min.js +412 -412
- package/dist/cli/commands/proxy.js +16 -1
- package/dist/factories/providerDescriptors.js +22 -10
- package/dist/models/manifests/cloudflareClef.d.ts +4 -2
- package/dist/models/manifests/cloudflareClef.js +5 -3
- package/dist/providers/cloudflareClef.d.ts +9 -0
- package/dist/providers/cloudflareClef.js +52 -1
- package/dist/providers/systemOneDecision.d.ts +6 -0
- package/dist/providers/systemOneDecision.js +9 -1
- package/dist/proxy/requestBodyLimit.d.ts +60 -0
- package/dist/proxy/requestBodyLimit.js +97 -0
- package/dist/server/adapters/honoAdapter.d.ts +13 -0
- package/dist/server/adapters/honoAdapter.js +73 -83
- package/dist/server/utils/rawBody.d.ts +17 -0
- package/dist/server/utils/rawBody.js +69 -4
- package/docs-site/static/search-index.json +10 -7
- package/package.json +11 -4
|
@@ -7,31 +7,11 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
|
|
8
8
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
9
9
|
import { isErrorResponse } from "../utils/validation.js";
|
|
10
|
-
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError,
|
|
10
|
+
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readBoundedRequestBody, } from "../utils/rawBody.js";
|
|
11
11
|
/**
|
|
12
12
|
* Hono-specific server adapter
|
|
13
13
|
* Supports multiple runtimes: Bun, Deno, Node.js
|
|
14
14
|
*/
|
|
15
|
-
/** Raw bodies already read for a request, so a second extraction never hits a consumed stream. */
|
|
16
|
-
const rawBodies = new WeakMap();
|
|
17
|
-
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
18
|
-
async function* webStreamChunks(stream) {
|
|
19
|
-
const reader = stream.getReader();
|
|
20
|
-
try {
|
|
21
|
-
for (;;) {
|
|
22
|
-
const { value, done } = await reader.read();
|
|
23
|
-
if (done) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (value) {
|
|
27
|
-
yield Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
reader.releaseLock();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
15
|
export class HonoServerAdapter extends BaseServerAdapter {
|
|
36
16
|
app;
|
|
37
17
|
server;
|
|
@@ -44,6 +24,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
44
24
|
// rely on initializeFramework's own local closure variables.
|
|
45
25
|
HTTPExceptionCtor;
|
|
46
26
|
streamSSEFn;
|
|
27
|
+
parseFormBody;
|
|
47
28
|
constructor(neurolink, config = {}) {
|
|
48
29
|
super(neurolink, config);
|
|
49
30
|
}
|
|
@@ -136,7 +117,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
136
117
|
* adapter (Express/Fastify/Koa) never pulls Hono's import graph in.
|
|
137
118
|
*/
|
|
138
119
|
async initializeFramework() {
|
|
139
|
-
const [{ Hono }, { cors }, { HTTPException }, { logger: honoLogger }, { secureHeaders }, { streamSSE }, { timeout },] = await Promise.all([
|
|
120
|
+
const [{ Hono }, { cors }, { HTTPException }, { logger: honoLogger }, { secureHeaders }, { streamSSE }, { timeout }, { parseBody },] = await Promise.all([
|
|
140
121
|
this.importFrameworkDependency("hono", "hono"),
|
|
141
122
|
this.importFrameworkDependency("hono/cors", "hono"),
|
|
142
123
|
this.importFrameworkDependency("hono/http-exception", "hono"),
|
|
@@ -144,9 +125,11 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
144
125
|
this.importFrameworkDependency("hono/secure-headers", "hono"),
|
|
145
126
|
this.importFrameworkDependency("hono/streaming", "hono"),
|
|
146
127
|
this.importFrameworkDependency("hono/timeout", "hono"),
|
|
128
|
+
this.importFrameworkDependency("hono/utils/body", "hono"),
|
|
147
129
|
]);
|
|
148
130
|
this.HTTPExceptionCtor = HTTPException;
|
|
149
131
|
this.streamSSEFn = streamSSE;
|
|
132
|
+
this.parseFormBody = parseBody;
|
|
150
133
|
this.app = new Hono();
|
|
151
134
|
// Add secure headers
|
|
152
135
|
this.app.use("*", secureHeaders());
|
|
@@ -243,17 +226,12 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
243
226
|
// Extract path parameters
|
|
244
227
|
const params = c.req.param();
|
|
245
228
|
// Reuse existing context from middleware if available, otherwise create new one
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
headers: this.extractHeaders(c),
|
|
253
|
-
query: this.extractQuery(c),
|
|
254
|
-
params,
|
|
255
|
-
body: await this.extractBody(c),
|
|
256
|
-
});
|
|
229
|
+
const created = this.requestContextStore.get(requestId) ||
|
|
230
|
+
(await this.createRequestContext(c, requestId, params));
|
|
231
|
+
if (isWebResponse(created)) {
|
|
232
|
+
return created;
|
|
233
|
+
}
|
|
234
|
+
const ctx = created;
|
|
257
235
|
// Emit request event
|
|
258
236
|
this.emit("request", {
|
|
259
237
|
requestId,
|
|
@@ -484,15 +462,11 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
484
462
|
let ctx = this.requestContextStore.get(requestId);
|
|
485
463
|
if (!ctx) {
|
|
486
464
|
// Create new context for the first middleware
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
query: this.extractQuery(c),
|
|
493
|
-
params,
|
|
494
|
-
body: await this.extractBody(c),
|
|
495
|
-
});
|
|
465
|
+
const created = await this.createRequestContext(c, requestId, params);
|
|
466
|
+
if (isWebResponse(created)) {
|
|
467
|
+
return created;
|
|
468
|
+
}
|
|
469
|
+
ctx = created;
|
|
496
470
|
// Store context for sharing between middleware and route handlers
|
|
497
471
|
this.requestContextStore.set(requestId, ctx);
|
|
498
472
|
}
|
|
@@ -667,22 +641,68 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
667
641
|
});
|
|
668
642
|
return query;
|
|
669
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* A request's context, or the 413 that answers a JSON or form body over its
|
|
646
|
+
* limit. That answer is built here, before the context exists, so no
|
|
647
|
+
* middleware (an auth check included) and no route runs for the request.
|
|
648
|
+
* An upload over the limit still reaches its route as the error itself,
|
|
649
|
+
* which answers in its own wire format.
|
|
650
|
+
*/
|
|
651
|
+
async createRequestContext(c, requestId, params) {
|
|
652
|
+
const body = await this.extractBody(c);
|
|
653
|
+
if (body instanceof RawBodyTooLargeError &&
|
|
654
|
+
!isRawBodyContentType(c.req.header("Content-Type"))) {
|
|
655
|
+
logger.warn("[HonoAdapter] Request rejected", {
|
|
656
|
+
requestId,
|
|
657
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
658
|
+
error: body.message,
|
|
659
|
+
});
|
|
660
|
+
this.emitRequestError({
|
|
661
|
+
requestId,
|
|
662
|
+
error: body,
|
|
663
|
+
timestamp: new Date(),
|
|
664
|
+
});
|
|
665
|
+
return c.json({
|
|
666
|
+
error: { code: "PAYLOAD_TOO_LARGE", message: body.message },
|
|
667
|
+
metadata: { requestId, timestamp: new Date().toISOString() },
|
|
668
|
+
}, 413);
|
|
669
|
+
}
|
|
670
|
+
return this.createContext({
|
|
671
|
+
requestId,
|
|
672
|
+
method: c.req.method,
|
|
673
|
+
path: c.req.path,
|
|
674
|
+
headers: this.extractHeaders(c),
|
|
675
|
+
query: this.extractQuery(c),
|
|
676
|
+
params,
|
|
677
|
+
body,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
670
680
|
async extractBody(c) {
|
|
671
681
|
if (!this.config.bodyParser.enabled) {
|
|
672
682
|
return undefined;
|
|
673
683
|
}
|
|
674
684
|
const contentType = c.req.header("Content-Type") || "";
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
685
|
+
// JSON and url-encoded forms are read through the same bounded read as
|
|
686
|
+
// uploads, with the limits the other adapters apply: `jsonLimit` for JSON,
|
|
687
|
+
// `maxSize` for forms. The bytes are then parsed the way Hono parses a
|
|
688
|
+
// body it has already buffered. A body that cannot be read or parsed is
|
|
689
|
+
// `undefined`, as it was when Hono did the reading.
|
|
690
|
+
const isJson = contentType.includes("application/json");
|
|
691
|
+
if (isJson || contentType.includes("application/x-www-form-urlencoded")) {
|
|
684
692
|
try {
|
|
685
|
-
|
|
693
|
+
const bytes = await readBoundedRequestBody(c.req.raw, parseByteLimit(isJson
|
|
694
|
+
? this.config.bodyParser.jsonLimit
|
|
695
|
+
: this.config.bodyParser.maxSize));
|
|
696
|
+
if (bytes instanceof RawBodyTooLargeError) {
|
|
697
|
+
return bytes;
|
|
698
|
+
}
|
|
699
|
+
return isJson
|
|
700
|
+
? JSON.parse(new TextDecoder().decode(bytes))
|
|
701
|
+
: await this.parseFormBody(new Request(c.req.url, {
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: { "Content-Type": contentType },
|
|
704
|
+
body: new Uint8Array(bytes),
|
|
705
|
+
}));
|
|
686
706
|
}
|
|
687
707
|
catch {
|
|
688
708
|
return undefined;
|
|
@@ -693,37 +713,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
693
713
|
// a throw from the middleware path would land in onError, which answers
|
|
694
714
|
// 500. The route answers 413 (or a validation error) from it.
|
|
695
715
|
if (isRawBodyContentType(contentType)) {
|
|
696
|
-
|
|
697
|
-
if (Number(c.req.header("Content-Length") ?? 0) > limit) {
|
|
698
|
-
return new RawBodyTooLargeError(limit);
|
|
699
|
-
}
|
|
700
|
-
// Read the web stream chunk by chunk and stop at the limit, so a
|
|
701
|
-
// chunked upload without a Content-Length cannot be buffered whole
|
|
702
|
-
// (and that before any route logic, including the API-key check, runs).
|
|
703
|
-
// The body is extracted more than once per request (middleware and
|
|
704
|
-
// route); a web stream can only be read once, so the outcome is kept
|
|
705
|
-
// per request the way Hono caches `arrayBuffer()`.
|
|
706
|
-
const cached = rawBodies.get(c.req.raw);
|
|
707
|
-
if (cached !== undefined) {
|
|
708
|
-
return cached;
|
|
709
|
-
}
|
|
710
|
-
const stream = c.req.raw.body;
|
|
711
|
-
if (!stream) {
|
|
712
|
-
return Buffer.alloc(0);
|
|
713
|
-
}
|
|
714
|
-
let outcome;
|
|
715
|
-
try {
|
|
716
|
-
outcome = await readRawBody(webStreamChunks(stream), limit);
|
|
717
|
-
}
|
|
718
|
-
catch (error) {
|
|
719
|
-
if (!(error instanceof RawBodyTooLargeError)) {
|
|
720
|
-
throw error;
|
|
721
|
-
}
|
|
722
|
-
await stream.cancel().catch(() => undefined);
|
|
723
|
-
outcome = error;
|
|
724
|
-
}
|
|
725
|
-
rawBodies.set(c.req.raw, outcome);
|
|
726
|
-
return outcome;
|
|
716
|
+
return readBoundedRequestBody(c.req.raw, parseByteLimit(this.config.bodyParser.rawMaxSize));
|
|
727
717
|
}
|
|
728
718
|
return undefined;
|
|
729
719
|
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
/** `true` for a content type whose body the adapters pass through unparsed. */
|
|
15
15
|
export declare function isRawBodyContentType(contentType: unknown): boolean;
|
|
16
|
+
/** "10mb" / "512kb" / "1gb" / a byte count → bytes, or `undefined` when it is none of those. */
|
|
17
|
+
export declare function tryParseByteLimit(limit: string | number | undefined): number | undefined;
|
|
16
18
|
/** "10mb" / "512kb" / "1gb" / a byte count → bytes. Falls back to 10 MB. */
|
|
17
19
|
export declare function parseByteLimit(limit: string | number | undefined): number;
|
|
18
20
|
/** Thrown when a raw body grows past the adapter's `maxSize`. */
|
|
@@ -22,6 +24,21 @@ export declare class RawBodyTooLargeError extends Error {
|
|
|
22
24
|
}
|
|
23
25
|
/** Read a Node request stream into one `Buffer`, refusing more than `maxBytes`. */
|
|
24
26
|
export declare function readRawBody(stream: AsyncIterable<Buffer | string>, maxBytes: number): Promise<Buffer>;
|
|
27
|
+
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
28
|
+
export declare function webStreamChunks(stream: ReadableStream<Uint8Array>): AsyncIterable<Buffer>;
|
|
29
|
+
/**
|
|
30
|
+
* Read a web `Request` body, refusing more than `maxBytes`.
|
|
31
|
+
*
|
|
32
|
+
* A declared `Content-Length` over the limit is refused without reading
|
|
33
|
+
* anything. Otherwise the stream is read chunk by chunk and stopped at the
|
|
34
|
+
* limit, so a chunked body without a `Content-Length` cannot be buffered whole.
|
|
35
|
+
* Over the limit the result is the error itself rather than a throw: a throw
|
|
36
|
+
* from a middleware lands in the framework's error handler, which answers 500.
|
|
37
|
+
*
|
|
38
|
+
* The outcome is kept per request, because a web stream can only be read once
|
|
39
|
+
* and the body may be asked for more than once (middleware, then route).
|
|
40
|
+
*/
|
|
41
|
+
export declare function readBoundedRequestBody(request: Request, maxBytes: number): Promise<Buffer | RawBodyTooLargeError>;
|
|
25
42
|
/** A web `Response` returned by a route handler. */
|
|
26
43
|
export declare function isWebResponse(value: unknown): value is Response;
|
|
27
44
|
/** Status, headers and body bytes of a web `Response`, for adapters that write to a Node response. */
|
|
@@ -17,8 +17,8 @@ const RAW_BODY_CONTENT_TYPE = /^\s*(multipart\/form-data|application\/octet-stre
|
|
|
17
17
|
export function isRawBodyContentType(contentType) {
|
|
18
18
|
return (typeof contentType === "string" && RAW_BODY_CONTENT_TYPE.test(contentType));
|
|
19
19
|
}
|
|
20
|
-
/** "10mb" / "512kb" / "1gb" / a byte count → bytes
|
|
21
|
-
export function
|
|
20
|
+
/** "10mb" / "512kb" / "1gb" / a byte count → bytes, or `undefined` when it is none of those. */
|
|
21
|
+
export function tryParseByteLimit(limit) {
|
|
22
22
|
if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
|
|
23
23
|
return limit;
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ export function parseByteLimit(limit) {
|
|
|
26
26
|
? limit.trim().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i)
|
|
27
27
|
: null;
|
|
28
28
|
if (!match) {
|
|
29
|
-
return
|
|
29
|
+
return undefined;
|
|
30
30
|
}
|
|
31
31
|
const units = {
|
|
32
32
|
b: 1,
|
|
@@ -34,7 +34,14 @@ export function parseByteLimit(limit) {
|
|
|
34
34
|
mb: 1024 * 1024,
|
|
35
35
|
gb: 1024 * 1024 * 1024,
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
const bytes = Math.floor(Number(match[1]) * units[(match[2] ?? "b").toLowerCase()]);
|
|
38
|
+
// A digit string long enough to overflow reads as Infinity, which no body
|
|
39
|
+
// can exceed: treat it as unparseable so the caller's default applies.
|
|
40
|
+
return Number.isFinite(bytes) ? bytes : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** "10mb" / "512kb" / "1gb" / a byte count → bytes. Falls back to 10 MB. */
|
|
43
|
+
export function parseByteLimit(limit) {
|
|
44
|
+
return tryParseByteLimit(limit) ?? 10 * 1024 * 1024;
|
|
38
45
|
}
|
|
39
46
|
/** Thrown when a raw body grows past the adapter's `maxSize`. */
|
|
40
47
|
export class RawBodyTooLargeError extends Error {
|
|
@@ -58,6 +65,64 @@ export async function readRawBody(stream, maxBytes) {
|
|
|
58
65
|
}
|
|
59
66
|
return Buffer.concat(chunks);
|
|
60
67
|
}
|
|
68
|
+
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
69
|
+
export async function* webStreamChunks(stream) {
|
|
70
|
+
const reader = stream.getReader();
|
|
71
|
+
try {
|
|
72
|
+
for (;;) {
|
|
73
|
+
const { value, done } = await reader.read();
|
|
74
|
+
if (done) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (value) {
|
|
78
|
+
yield Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
reader.releaseLock();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Bodies already read for a request, so a second read never hits a consumed stream. */
|
|
87
|
+
const boundedBodies = new WeakMap();
|
|
88
|
+
/**
|
|
89
|
+
* Read a web `Request` body, refusing more than `maxBytes`.
|
|
90
|
+
*
|
|
91
|
+
* A declared `Content-Length` over the limit is refused without reading
|
|
92
|
+
* anything. Otherwise the stream is read chunk by chunk and stopped at the
|
|
93
|
+
* limit, so a chunked body without a `Content-Length` cannot be buffered whole.
|
|
94
|
+
* Over the limit the result is the error itself rather than a throw: a throw
|
|
95
|
+
* from a middleware lands in the framework's error handler, which answers 500.
|
|
96
|
+
*
|
|
97
|
+
* The outcome is kept per request, because a web stream can only be read once
|
|
98
|
+
* and the body may be asked for more than once (middleware, then route).
|
|
99
|
+
*/
|
|
100
|
+
export async function readBoundedRequestBody(request, maxBytes) {
|
|
101
|
+
if (Number(request.headers.get("content-length") ?? 0) > maxBytes) {
|
|
102
|
+
return new RawBodyTooLargeError(maxBytes);
|
|
103
|
+
}
|
|
104
|
+
const cached = boundedBodies.get(request);
|
|
105
|
+
if (cached !== undefined) {
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
|
108
|
+
const stream = request.body;
|
|
109
|
+
if (!stream) {
|
|
110
|
+
return Buffer.alloc(0);
|
|
111
|
+
}
|
|
112
|
+
let outcome;
|
|
113
|
+
try {
|
|
114
|
+
outcome = await readRawBody(webStreamChunks(stream), maxBytes);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (!(error instanceof RawBodyTooLargeError)) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
await stream.cancel().catch(() => undefined);
|
|
121
|
+
outcome = error;
|
|
122
|
+
}
|
|
123
|
+
boundedBodies.set(request, outcome);
|
|
124
|
+
return outcome;
|
|
125
|
+
}
|
|
61
126
|
/** A web `Response` returned by a route handler. */
|
|
62
127
|
export function isWebResponse(value) {
|
|
63
128
|
return typeof Response !== "undefined" && value instanceof Response;
|