@zuplo/cli 6.72.17 → 6.73.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/node_modules/@zuplo/core/package.json +1 -1
- package/node_modules/@zuplo/graphql/package.json +1 -1
- package/node_modules/@zuplo/openapi-tools/package.json +1 -1
- package/node_modules/@zuplo/otel/package.json +1 -1
- package/node_modules/@zuplo/runtime/out/esm/{chunk-S2CH4265.js → chunk-XLXPRKLV.js} +97 -97
- package/node_modules/@zuplo/runtime/out/esm/chunk-XLXPRKLV.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/types/index.d.ts +8 -1
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/node_modules/fast-uri/index.js +17 -0
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/security.test.js +62 -0
- package/node_modules/hono/dist/cjs/helper/streaming/sse.js +4 -3
- package/node_modules/hono/dist/cjs/request.js +7 -2
- package/node_modules/hono/dist/cjs/utils/body.js +6 -0
- package/node_modules/hono/dist/helper/streaming/sse.js +4 -3
- package/node_modules/hono/dist/request.js +7 -2
- package/node_modules/hono/dist/types/middleware/combine/index.d.ts +1 -1
- package/node_modules/hono/dist/utils/body.js +6 -0
- package/node_modules/hono/package.json +1 -1
- package/package.json +6 -6
- package/node_modules/@zuplo/runtime/out/esm/chunk-S2CH4265.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-S2CH4265.js.LEGAL.txt → chunk-XLXPRKLV.js.LEGAL.txt} +0 -0
|
@@ -615,6 +615,13 @@ export declare interface AIGatewaySemanticCacheOutboundPolicyOptions {}
|
|
|
615
615
|
* route's chain is what enables it. `namespace` defaults to the AI Gateway
|
|
616
616
|
* configuration id when omitted so caching stays scoped per tenant.
|
|
617
617
|
*
|
|
618
|
+
* Cache outcomes are reported on the response via the RFC 9211 `Cache-Status`
|
|
619
|
+
* header under the cache name `zp-aigw-sem-cache` (hit:
|
|
620
|
+
* `zp-aigw-sem-cache; hit; detail="similarity=0.93"`; miss:
|
|
621
|
+
* `zp-aigw-sem-cache; fwd=miss; stored`) alongside the legacy
|
|
622
|
+
* `x-ai-gateway-cache: HIT|MISS` and `x-ai-gateway-cache-similarity` headers,
|
|
623
|
+
* kept for compatibility.
|
|
624
|
+
*
|
|
618
625
|
* @title AI Gateway Semantic Cache (v2)
|
|
619
626
|
* @product ai-gateway
|
|
620
627
|
* @hidden
|
|
@@ -2673,7 +2680,7 @@ declare interface CacheOptions {
|
|
|
2673
2680
|
}
|
|
2674
2681
|
|
|
2675
2682
|
/**
|
|
2676
|
-
* How long a cached response lives, in seconds. Defaults to 3600 (1 hour).
|
|
2683
|
+
* How long a cached response lives, in whole seconds. Defaults to 3600 (1 hour); maximum 2592000 (30 days).
|
|
2677
2684
|
* @public
|
|
2678
2685
|
*/
|
|
2679
2686
|
declare type CacheTTLSeconds = number;
|
|
@@ -202,6 +202,10 @@ function serialize (cmpts, opts) {
|
|
|
202
202
|
|
|
203
203
|
const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
|
|
204
204
|
|
|
205
|
+
// Captures the authority component (between "//" and the next "/", "?" or "#"),
|
|
206
|
+
// with or without a scheme prefix, for the literal-backslash rejection below.
|
|
207
|
+
const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
|
|
208
|
+
|
|
205
209
|
/**
|
|
206
210
|
* @param {import('./types/index').URIComponent} parsed
|
|
207
211
|
* @param {RegExpMatchArray} matches
|
|
@@ -248,6 +252,19 @@ function parseWithStatus (uri, opts) {
|
|
|
248
252
|
}
|
|
249
253
|
}
|
|
250
254
|
|
|
255
|
+
// A literal backslash (U+005C) is not a valid RFC 3986 URI character and is
|
|
256
|
+
// not an authority delimiter. Reject it in the authority rather than
|
|
257
|
+
// rewriting it: normalizing "\" -> "/" (WHATWG error recovery) could silently
|
|
258
|
+
// change the resource identified by an otherwise-invalid input, and lets "\"
|
|
259
|
+
// act as a host delimiter here while Node's native URL parses a different
|
|
260
|
+
// host (SSRF / redirect / origin-allowlist bypass). Percent-encoded %5C is
|
|
261
|
+
// untouched and remains valid encoded data.
|
|
262
|
+
const authorityMatch = uri.match(AUTHORITY_PREFIX)
|
|
263
|
+
if (authorityMatch !== null && authorityMatch[1].indexOf('\\') !== -1) {
|
|
264
|
+
parsed.error = 'URI authority must not contain a literal backslash.'
|
|
265
|
+
malformedAuthorityOrPort = true
|
|
266
|
+
}
|
|
267
|
+
|
|
251
268
|
const matches = uri.match(URI_PARSE)
|
|
252
269
|
|
|
253
270
|
if (matches) {
|
|
@@ -159,3 +159,65 @@ test('parse canonicalises IDN / Unicode hosts to their ASCII form', (t) => {
|
|
|
159
159
|
t.equal(parsed.host, expectedHost, `host canonicalised to ASCII: ${description}`)
|
|
160
160
|
})
|
|
161
161
|
})
|
|
162
|
+
|
|
163
|
+
test('parse rejects a literal backslash in the authority as malformed (RFC 3986)', (t) => {
|
|
164
|
+
// Regression for the host-confusion bypass: a literal "\" is invalid RFC 3986
|
|
165
|
+
// syntax and must be flagged malformed, not silently rewritten. Otherwise "\"
|
|
166
|
+
// acts as a host delimiter here while Node's native URL parses a different
|
|
167
|
+
// host, defeating a host-based SSRF/redirect/origin allowlist.
|
|
168
|
+
const cases = [
|
|
169
|
+
'http://evil.com\\@allowed.com',
|
|
170
|
+
'https://169.254.169.254\\@trusted.example.com',
|
|
171
|
+
'http://127.0.0.1\\@public.example.com',
|
|
172
|
+
'https://attacker.com\\@api.internal',
|
|
173
|
+
'http://a\\@b',
|
|
174
|
+
'ws://evil.com\\@allowed.com/chat',
|
|
175
|
+
'wss://evil.com\\@allowed.com/chat',
|
|
176
|
+
'http://evil.com\\%40allowed.com',
|
|
177
|
+
'//evil.com\\@allowed.com'
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
t.plan(cases.length)
|
|
181
|
+
|
|
182
|
+
cases.forEach((input) => {
|
|
183
|
+
t.equal(
|
|
184
|
+
fastURI.parse(input).error,
|
|
185
|
+
'URI authority must not contain a literal backslash.',
|
|
186
|
+
input
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('normalize does not canonicalize a literal-backslash URI into a different valid URL', (t) => {
|
|
192
|
+
const cases = [
|
|
193
|
+
'http://evil.com\\@allowed.com',
|
|
194
|
+
'https://attacker.com\\@api.internal'
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
t.plan(cases.length)
|
|
198
|
+
|
|
199
|
+
cases.forEach((input) => {
|
|
200
|
+
t.equal(fastURI.normalize(input), input, input)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('parse leaves percent-encoded %5C untouched as encoded data (not rejected)', (t) => {
|
|
205
|
+
// Only the literal "\" byte is rejected; %5C stays valid encoded data and
|
|
206
|
+
// does not diverge from the native URL parser, so it must not be flagged.
|
|
207
|
+
const input = 'http://evil.com%5C@allowed.com'
|
|
208
|
+
const parsed = fastURI.parse(input)
|
|
209
|
+
|
|
210
|
+
t.plan(2)
|
|
211
|
+
t.notOk(parsed.error, '%5C is valid encoded data, not malformed')
|
|
212
|
+
t.equal(parsed.host, new URL(input).hostname, '%5C host matches native URL (no divergence)')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('parse does not reject a literal backslash in the query or fragment', (t) => {
|
|
216
|
+
// The rejection is scoped to the authority/path (the host-confusion surface);
|
|
217
|
+
// a backslash after "?"/"#" is normalized as encoded data as before.
|
|
218
|
+
const parsed = fastURI.parse('http://host.example.com/?x=\\y#z\\w')
|
|
219
|
+
|
|
220
|
+
t.plan(2)
|
|
221
|
+
t.notOk(parsed.error, 'backslash in query/fragment does not mark the URI malformed')
|
|
222
|
+
t.equal(parsed.host, 'host.example.com', 'host parsed normally')
|
|
223
|
+
})
|
|
@@ -33,8 +33,9 @@ class SSEStreamingApi extends import_stream.StreamingApi {
|
|
|
33
33
|
const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
|
|
34
34
|
return `data: ${line}`;
|
|
35
35
|
}).join("\n");
|
|
36
|
-
for (const key of ["event", "id"
|
|
37
|
-
|
|
36
|
+
for (const key of ["event", "id"]) {
|
|
37
|
+
const value = message[key];
|
|
38
|
+
if (value && /[\r\n]/.test(value)) {
|
|
38
39
|
throw new Error(`${key} must not contain "\\r" or "\\n"`);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -42,7 +43,7 @@ class SSEStreamingApi extends import_stream.StreamingApi {
|
|
|
42
43
|
message.event && `event: ${message.event}`,
|
|
43
44
|
dataLines,
|
|
44
45
|
message.id && `id: ${message.id}`,
|
|
45
|
-
message.retry && `retry: ${message.retry}`
|
|
46
|
+
message.retry !== void 0 && `retry: ${message.retry}`
|
|
46
47
|
].filter(Boolean).join("\n") + "\n\n";
|
|
47
48
|
await this.write(sseData);
|
|
48
49
|
}
|
|
@@ -316,11 +316,16 @@ const cloneRawRequest = async (req) => {
|
|
|
316
316
|
message: "Cannot clone request: body was already consumed and not cached. Please use HonoRequest methods (e.g., req.json(), req.text()) instead of consuming req.raw directly."
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
+
const body = await req[cacheKey]();
|
|
320
|
+
const headers = req.header();
|
|
321
|
+
if (body instanceof FormData) {
|
|
322
|
+
delete headers["content-type"];
|
|
323
|
+
}
|
|
319
324
|
const requestInit = {
|
|
320
|
-
body
|
|
325
|
+
body,
|
|
321
326
|
cache: req.raw.cache,
|
|
322
327
|
credentials: req.raw.credentials,
|
|
323
|
-
headers
|
|
328
|
+
headers,
|
|
324
329
|
integrity: req.raw.integrity,
|
|
325
330
|
keepalive: req.raw.keepalive,
|
|
326
331
|
method: req.method,
|
|
@@ -33,6 +33,12 @@ const parseBody = async (request, options = /* @__PURE__ */ Object.create(null))
|
|
|
33
33
|
return {};
|
|
34
34
|
};
|
|
35
35
|
async function parseFormData(request, options) {
|
|
36
|
+
if (!isRawRequest(request) && request.bodyCache.formData) {
|
|
37
|
+
return convertFormDataToBodyData(
|
|
38
|
+
await request.bodyCache.formData,
|
|
39
|
+
options
|
|
40
|
+
);
|
|
41
|
+
}
|
|
36
42
|
const headers = isRawRequest(request) ? request.headers : request.raw.headers;
|
|
37
43
|
const arrayBuffer = await request.arrayBuffer();
|
|
38
44
|
const formDataPromise = (0, import_buffer.bufferToFormData)(arrayBuffer, headers.get("Content-Type") || "");
|
|
@@ -11,8 +11,9 @@ var SSEStreamingApi = class extends StreamingApi {
|
|
|
11
11
|
const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
|
|
12
12
|
return `data: ${line}`;
|
|
13
13
|
}).join("\n");
|
|
14
|
-
for (const key of ["event", "id"
|
|
15
|
-
|
|
14
|
+
for (const key of ["event", "id"]) {
|
|
15
|
+
const value = message[key];
|
|
16
|
+
if (value && /[\r\n]/.test(value)) {
|
|
16
17
|
throw new Error(`${key} must not contain "\\r" or "\\n"`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
@@ -20,7 +21,7 @@ var SSEStreamingApi = class extends StreamingApi {
|
|
|
20
21
|
message.event && `event: ${message.event}`,
|
|
21
22
|
dataLines,
|
|
22
23
|
message.id && `id: ${message.id}`,
|
|
23
|
-
message.retry && `retry: ${message.retry}`
|
|
24
|
+
message.retry !== void 0 && `retry: ${message.retry}`
|
|
24
25
|
].filter(Boolean).join("\n") + "\n\n";
|
|
25
26
|
await this.write(sseData);
|
|
26
27
|
}
|
|
@@ -294,11 +294,16 @@ var cloneRawRequest = async (req) => {
|
|
|
294
294
|
message: "Cannot clone request: body was already consumed and not cached. Please use HonoRequest methods (e.g., req.json(), req.text()) instead of consuming req.raw directly."
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
|
+
const body = await req[cacheKey]();
|
|
298
|
+
const headers = req.header();
|
|
299
|
+
if (body instanceof FormData) {
|
|
300
|
+
delete headers["content-type"];
|
|
301
|
+
}
|
|
297
302
|
const requestInit = {
|
|
298
|
-
body
|
|
303
|
+
body,
|
|
299
304
|
cache: req.raw.cache,
|
|
300
305
|
credentials: req.raw.credentials,
|
|
301
|
-
headers
|
|
306
|
+
headers,
|
|
302
307
|
integrity: req.raw.integrity,
|
|
303
308
|
keepalive: req.raw.keepalive,
|
|
304
309
|
method: req.method,
|
|
@@ -71,7 +71,7 @@ export declare const every: (...middleware: (MiddlewareHandler | Condition)[]) =
|
|
|
71
71
|
* @example
|
|
72
72
|
* ```ts
|
|
73
73
|
* import { except } from 'hono/combine'
|
|
74
|
-
* import { bearerAuth } from 'hono/bearer-auth
|
|
74
|
+
* import { bearerAuth } from 'hono/bearer-auth'
|
|
75
75
|
*
|
|
76
76
|
* // If client is accessing public API, then skip authentication.
|
|
77
77
|
* // Otherwise, require a valid token.
|
|
@@ -12,6 +12,12 @@ var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) =
|
|
|
12
12
|
return {};
|
|
13
13
|
};
|
|
14
14
|
async function parseFormData(request, options) {
|
|
15
|
+
if (!isRawRequest(request) && request.bodyCache.formData) {
|
|
16
|
+
return convertFormDataToBodyData(
|
|
17
|
+
await request.bodyCache.formData,
|
|
18
|
+
options
|
|
19
|
+
);
|
|
20
|
+
}
|
|
15
21
|
const headers = isRawRequest(request) ? request.headers : request.raw.headers;
|
|
16
22
|
const arrayBuffer = await request.arrayBuffer();
|
|
17
23
|
const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuplo/cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.73.0",
|
|
4
4
|
"repository": "https://github.com/zuplo/zuplo",
|
|
5
5
|
"author": "Zuplo, Inc.",
|
|
6
6
|
"type": "module",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"@opentelemetry/api": "1.9.0",
|
|
28
28
|
"@opentelemetry/api-logs": "0.201.1",
|
|
29
29
|
"@swc/core": "1.10.18",
|
|
30
|
-
"@zuplo/core": "6.
|
|
30
|
+
"@zuplo/core": "6.73.0",
|
|
31
31
|
"@zuplo/editor": "1.0.20821740935",
|
|
32
|
-
"@zuplo/openapi-tools": "6.
|
|
33
|
-
"@zuplo/runtime": "6.
|
|
32
|
+
"@zuplo/openapi-tools": "6.73.0",
|
|
33
|
+
"@zuplo/runtime": "6.73.0",
|
|
34
34
|
"chalk": "5.4.1",
|
|
35
35
|
"chokidar": "3.5.3",
|
|
36
36
|
"cookie": "1.0.2",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"workerd": "1.20241230.0",
|
|
62
62
|
"yargs": "17.7.2",
|
|
63
63
|
"zod": "3.25.76",
|
|
64
|
-
"@zuplo/graphql": "6.
|
|
65
|
-
"@zuplo/otel": "6.
|
|
64
|
+
"@zuplo/graphql": "6.73.0",
|
|
65
|
+
"@zuplo/otel": "6.73.0"
|
|
66
66
|
},
|
|
67
67
|
"bundleDependencies": [
|
|
68
68
|
"@inquirer/prompts",
|