@velarscript/node 0.12.0 → 0.13.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 +40 -11
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +99 -23
- package/dist/compiler.js.map +1 -1
- package/dist/http-runtime.d.ts.map +1 -1
- package/dist/http-runtime.js +19 -7
- package/dist/http-runtime.js.map +1 -1
- package/dist/node-host-runtime.d.ts.map +1 -1
- package/dist/node-host-runtime.js +14 -7
- package/dist/node-host-runtime.js.map +1 -1
- package/dist/node-host-worker-runtime.d.ts.map +1 -1
- package/dist/node-host-worker-runtime.js +59 -31
- package/dist/node-host-worker-runtime.js.map +1 -1
- package/dist/project-config.d.ts +1 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/route-shape.d.ts +20 -0
- package/dist/route-shape.d.ts.map +1 -0
- package/dist/route-shape.js +36 -0
- package/dist/route-shape.js.map +1 -0
- package/dist/serve-runtime.d.ts.map +1 -1
- package/dist/serve-runtime.js +312 -52
- package/dist/serve-runtime.js.map +1 -1
- package/dist/server-analyzer.d.ts +52 -0
- package/dist/server-analyzer.d.ts.map +1 -1
- package/dist/server-analyzer.js +377 -12
- package/dist/server-analyzer.js.map +1 -1
- package/dist/server-ast.d.ts +13 -1
- package/dist/server-ast.d.ts.map +1 -1
- package/dist/server-ast.js.map +1 -1
- package/dist/server-emitter.d.ts +1 -0
- package/dist/server-emitter.d.ts.map +1 -1
- package/dist/server-emitter.js +13 -2
- package/dist/server-emitter.js.map +1 -1
- package/dist/server-parser.d.ts +2 -0
- package/dist/server-parser.d.ts.map +1 -1
- package/dist/server-parser.js +45 -20
- package/dist/server-parser.js.map +1 -1
- package/dist/websocket-runtime.d.ts.map +1 -1
- package/dist/websocket-runtime.js +73 -17
- package/dist/websocket-runtime.js.map +1 -1
- package/package.json +2 -2
package/dist/serve-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ROUTE_SHAPE_FROM_SEGMENTS_SOURCE } from "./route-shape.js";
|
|
1
2
|
// Application-facing velar/serve contract. HTTP sockets, request streams,
|
|
2
3
|
// backpressure and static-file effects live in the shared isolated Node host;
|
|
3
4
|
// this Realm owns only Velar values, handlers and strict JSON/type boundaries.
|
|
@@ -6,7 +7,7 @@ import { __velarNodeHostInvoke, __velarNodeHostOn } from "velar/node-host-v1";
|
|
|
6
7
|
import { normalizeError as __velarServeNormalizeError } from "velar/compiler-runtime-errors-v1";
|
|
7
8
|
import { __velarValidateDenseList as __velarServeValidateDenseList } from "velar/compiler-runtime-collection-lowering-v1";
|
|
8
9
|
import { Bytes as __velarServeBytesType } from "velar/binary";
|
|
9
|
-
import { writeBytes as __velarServeWriteBytes } from "velar/fs";
|
|
10
|
+
import { canonical as __velarServeFsCanonical, info as __velarServeFsInfo, writeBytes as __velarServeWriteBytes } from "velar/fs";
|
|
10
11
|
import { Cancellation as __velarServeCancellation } from "velar/task";
|
|
11
12
|
|
|
12
13
|
const __velarServeArray = globalThis.Array;
|
|
@@ -114,6 +115,7 @@ const __velarServeDefaultShutdownGrace = 30_000;
|
|
|
114
115
|
const __velarServeFileMarker = Symbol("velar.serve.file-response");
|
|
115
116
|
const __velarServeAppMarker = Symbol("velar.serve.app");
|
|
116
117
|
const __velarServeRouteMarker = Symbol("velar.serve.route");
|
|
118
|
+
const __velarServeNotFoundMarker = Symbol("velar.serve.not-found");
|
|
117
119
|
const __velarServeInputMarker = Symbol("velar.serve.input");
|
|
118
120
|
const __velarServeProviderMarker = Symbol("velar.serve.provider");
|
|
119
121
|
const __velarServeUploadMarker = Symbol("velar.serve.upload");
|
|
@@ -185,9 +187,8 @@ function __velarServeIsSafeInteger(value) {
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
function __velarServeReserveOutbound(bytes) {
|
|
188
|
-
if (!__velarServeIsSafeInteger(bytes) || bytes < 0
|
|
189
|
-
|
|
190
|
-
}
|
|
190
|
+
if (!__velarServeIsSafeInteger(bytes) || bytes < 0) throw new __velarServeRangeError("ServeResponse outbound byte reservation must be a non-negative integer");
|
|
191
|
+
if (__velarServeOutboundBytes + bytes > __velarServeMaxOutboundBytes) throw new __velarServeOutboundBudgetError();
|
|
191
192
|
__velarServeOutboundBytes += bytes;
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -429,6 +430,19 @@ export class HttpError extends __velarServeError {
|
|
|
429
430
|
}
|
|
430
431
|
}
|
|
431
432
|
|
|
433
|
+
// Exhausting the aggregate outbound budget is a temporary load condition, not a
|
|
434
|
+
// server fault: 503 with retry-after is what a load balancer and a client both
|
|
435
|
+
// know how to act on, and 500 is what neither can. Most reserves happen while a
|
|
436
|
+
// response is already on its way out, past the HttpError catch in
|
|
437
|
+
// __velarServeHandleRequest, so both send paths recognize this one error by
|
|
438
|
+
// identity and answer it themselves. The shed answer reserves nothing: it is a
|
|
439
|
+
// fixed, tiny payload, and reserving for it would fail by construction.
|
|
440
|
+
class __velarServeOutboundBudgetError extends HttpError {
|
|
441
|
+
constructor() {
|
|
442
|
+
super(503, {error: "outbound_budget_exhausted"}, new __velarServeMap([["retry-after", "1"]]));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
432
446
|
function __velarServeIsFileResponse(value) {
|
|
433
447
|
if (!value || typeof value !== "object") return false;
|
|
434
448
|
const descriptor = __velarServeOwnDescriptor(value, __velarServeFileMarker);
|
|
@@ -581,7 +595,7 @@ export const ServeApp = __velarServeTypeObject(
|
|
|
581
595
|
value => __velarServeIsApp(value),
|
|
582
596
|
"ServeApp values are declared with 'server name:' or built by velar/serve composition functions",
|
|
583
597
|
null,
|
|
584
|
-
__velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{createApp: __velarCreateServeApp, createRoute: __velarCreateServeRoute, testClient: __velarServeTestClient, nativeApp: __velarServeNativeApp}]),
|
|
598
|
+
__velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{createApp: __velarCreateServeApp, createRoute: __velarCreateServeRoute, createNotFound: __velarCreateServeNotFound, testClient: __velarServeTestClient, nativeApp: __velarServeNativeApp}]),
|
|
585
599
|
);
|
|
586
600
|
export const Server = __velarServeTypeObject(value => {
|
|
587
601
|
try {
|
|
@@ -599,6 +613,74 @@ function __velarServeIsUpload(value) {
|
|
|
599
613
|
|
|
600
614
|
export const Upload = __velarServeTypeObject(__velarServeIsUpload, "Upload values are created from multipart route inputs");
|
|
601
615
|
|
|
616
|
+
// Upload.save takes its containment root as a required argument and refuses any
|
|
617
|
+
// target that resolves outside it. The host operations bag __velarServeNativeFile
|
|
618
|
+
// uses for static files is not reachable from this Realm, so containment is
|
|
619
|
+
// composed from velar/fs instead of a new host operation: fail-closed textual
|
|
620
|
+
// rules on the caller's path, then a canonical check of the directory that will
|
|
621
|
+
// hold the file so a symbolic link cannot lead out of the root. That is the
|
|
622
|
+
// smaller of the two shapes and adds no trust boundary. This is complementary to
|
|
623
|
+
// the basename reduction in __velarServeUploadBasename, which bounds only what a
|
|
624
|
+
// client can put into Upload.filename.
|
|
625
|
+
function __velarServeUploadSegments(path) {
|
|
626
|
+
if (typeof path !== "string" || path.length === 0) throw new __velarServeTypeError("Upload.save path must be a non-empty path relative to its root");
|
|
627
|
+
if (path.length > __velarServeMaxPathCodeUnits || __velarServeCall(__velarServeStringIncludes, path, ["\0"])) {
|
|
628
|
+
throw new __velarServeRangeError("Upload.save path is outside the supported bounds");
|
|
629
|
+
}
|
|
630
|
+
// The root's separator is a host detail this Realm cannot consult, so both
|
|
631
|
+
// separators are refused, exactly as __velarServeNativeEscapes refuses both.
|
|
632
|
+
// Only '..' and an absolute path genuinely leave the root; a backslash, an
|
|
633
|
+
// empty segment and a '.' are refused because this Realm will not normalize a
|
|
634
|
+
// path on the caller's behalf, so each refusal says which of the two it is
|
|
635
|
+
// rather than naming an escape that did not happen.
|
|
636
|
+
if (__velarServeCall(__velarServeStringIncludes, path, ["\\"])) throw new __velarServeError("Upload.save path cannot contain a backslash: it is a path separator on some hosts");
|
|
637
|
+
if (__velarServeCall(__velarServeStringStartsWith, path, ["/"])) throw new __velarServeError("Upload.save path escapes its root: it is absolute");
|
|
638
|
+
const segments = __velarServeCall(__velarServeStringSplit, path, ["/"]);
|
|
639
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
640
|
+
const segment = segments[index];
|
|
641
|
+
if (segment === "..") throw new __velarServeError("Upload.save path escapes its root: it has a '..' segment");
|
|
642
|
+
if (segment === "") throw new __velarServeError("Upload.save path must be normalized: it has an empty segment");
|
|
643
|
+
if (segment === ".") throw new __velarServeError("Upload.save path must be normalized: it has a '.' segment");
|
|
644
|
+
}
|
|
645
|
+
return segments;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function __velarServeUploadContains(root, target) {
|
|
649
|
+
if (target === root) return true;
|
|
650
|
+
if (target.length <= root.length || __velarServeCall(__velarServeStringSlice, target, [0, root.length]) !== root) return false;
|
|
651
|
+
const boundary = __velarServeCall(__velarServeStringSlice, target, [root.length, root.length + 1]);
|
|
652
|
+
return boundary === "/" || boundary === "\\";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function __velarServeUploadTarget(path, root) {
|
|
656
|
+
const segments = __velarServeUploadSegments(path);
|
|
657
|
+
if (typeof root !== "string" || root.length === 0) throw new __velarServeTypeError("Upload.save root must be a non-empty directory path");
|
|
658
|
+
// Resolution failures arrive from the host as an errno naming an absolute path
|
|
659
|
+
// the caller never wrote, so both are answered in the caller's own terms: the
|
|
660
|
+
// root it named, or the relative directory it asked for.
|
|
661
|
+
let base;
|
|
662
|
+
try { base = await __velarServeFsCanonical(root); }
|
|
663
|
+
catch { throw new __velarServeError("Upload.save root does not resolve to an existing directory"); }
|
|
664
|
+
let directory = base;
|
|
665
|
+
let relative = "";
|
|
666
|
+
for (let index = 0; index + 1 < segments.length; index += 1) {
|
|
667
|
+
directory += "/" + segments[index];
|
|
668
|
+
relative += relative === "" ? segments[index] : "/" + segments[index];
|
|
669
|
+
}
|
|
670
|
+
if (directory !== base) {
|
|
671
|
+
try { directory = await __velarServeFsCanonical(directory); }
|
|
672
|
+
catch { throw new __velarServeError("Upload.save path names a directory that does not exist under the root: " + relative); }
|
|
673
|
+
if (!__velarServeUploadContains(base, directory)) throw new __velarServeError("Upload.save path escapes its root through a symbolic link");
|
|
674
|
+
}
|
|
675
|
+
const target = directory + "/" + segments[segments.length - 1];
|
|
676
|
+
// A write follows a symbolic link at the target itself, so the last segment is
|
|
677
|
+
// canonicalized by kind rather than by path: refusing the link is fail-closed
|
|
678
|
+
// and needs no second containment test.
|
|
679
|
+
const existing = await __velarServeFsInfo(target);
|
|
680
|
+
if (existing !== null && existing.kind === "symlink") throw new __velarServeError("Upload.save refuses to write through a symbolic link");
|
|
681
|
+
return target;
|
|
682
|
+
}
|
|
683
|
+
|
|
602
684
|
function __velarServeUploadValue(name, filename, contentType, data, states) {
|
|
603
685
|
if (!__velarServeBytesType.is(data)) throw new __velarServeTypeError("Upload data must be Bytes");
|
|
604
686
|
const state = {data};
|
|
@@ -619,7 +701,11 @@ function __velarServeUploadValue(name, filename, contentType, data, states) {
|
|
|
619
701
|
catch { throw new __velarServeTypeError("Upload is not valid UTF-8 text"); }
|
|
620
702
|
},
|
|
621
703
|
bytes: async () => __velarServeBytesType.parse(current()),
|
|
622
|
-
save: async
|
|
704
|
+
save: async (path, root) => {
|
|
705
|
+
const value = current();
|
|
706
|
+
await __velarServeWriteBytes(await __velarServeUploadTarget(path, root), value);
|
|
707
|
+
return null;
|
|
708
|
+
},
|
|
623
709
|
}]);
|
|
624
710
|
}
|
|
625
711
|
|
|
@@ -897,6 +983,12 @@ function __velarServeIsRoute(value) {
|
|
|
897
983
|
return descriptor?.enumerable === true && "value" in descriptor && descriptor.value === true;
|
|
898
984
|
}
|
|
899
985
|
|
|
986
|
+
function __velarServeIsNotFound(value) {
|
|
987
|
+
if (!value || typeof value !== "object") return false;
|
|
988
|
+
const descriptor = __velarServeOwnDescriptor(value, __velarServeNotFoundMarker);
|
|
989
|
+
return descriptor?.enumerable === true && "value" in descriptor && descriptor.value === true;
|
|
990
|
+
}
|
|
991
|
+
|
|
900
992
|
function __velarServeIsApp(value) {
|
|
901
993
|
if (!value || typeof value !== "object") return false;
|
|
902
994
|
const descriptor = __velarServeOwnDescriptor(value, __velarServeAppMarker);
|
|
@@ -922,12 +1014,13 @@ function __velarServeRoutePath(path, name = "Route path") {
|
|
|
922
1014
|
return path;
|
|
923
1015
|
}
|
|
924
1016
|
|
|
1017
|
+
// D90 R19(c): the shape rule is written once, in route-shape.ts, and this
|
|
1018
|
+
// module interpolates that one definition. The shared core touches only
|
|
1019
|
+
// indexed access and .length, so it stays sound inside this hardened Realm;
|
|
1020
|
+
// the split it never performs itself happens here with the captured split.
|
|
1021
|
+
const __velarServeRouteShapeFromSegments = ${ROUTE_SHAPE_FROM_SEGMENTS_SOURCE};
|
|
925
1022
|
function __velarServeRouteShape(path) {
|
|
926
|
-
|
|
927
|
-
for (let index = 1; index < segments.length; index += 1) {
|
|
928
|
-
if (__velarServeCall(__velarServeStringStartsWith, segments[index], ["{"]) && __velarServeCall(__velarServeStringEndsWith, segments[index], ["}"])) segments[index] = "{}";
|
|
929
|
-
}
|
|
930
|
-
return __velarServeCall(__velarServeArrayJoin, segments, ["/"]);
|
|
1023
|
+
return __velarServeRouteShapeFromSegments(__velarServeCall(__velarServeStringSplit, path, ["/"]));
|
|
931
1024
|
}
|
|
932
1025
|
|
|
933
1026
|
function __velarCreateServeRoute(method, path, parameters, handler, metadata = {}) {
|
|
@@ -1032,6 +1125,21 @@ function __velarCreateServeRoute(method, path, parameters, handler, metadata = {
|
|
|
1032
1125
|
}]);
|
|
1033
1126
|
}
|
|
1034
1127
|
|
|
1128
|
+
function __velarCreateServeNotFound(handler, middleware = []) {
|
|
1129
|
+
if (typeof handler !== "function") throw new __velarServeTypeError("@notFound handler is invalid");
|
|
1130
|
+
if (!__velarServeIsArray(middleware) || middleware.length > 64) throw new __velarServeRangeError("@notFound cannot have more than 64 middleware functions");
|
|
1131
|
+
const checkedMiddleware = [];
|
|
1132
|
+
for (let index = 0; index < middleware.length; index += 1) {
|
|
1133
|
+
if (typeof middleware[index] !== "function") throw new __velarServeTypeError("@notFound middleware entries must be functions");
|
|
1134
|
+
checkedMiddleware[checkedMiddleware.length] = middleware[index];
|
|
1135
|
+
}
|
|
1136
|
+
return __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{
|
|
1137
|
+
[__velarServeNotFoundMarker]: true,
|
|
1138
|
+
handler,
|
|
1139
|
+
middleware: __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [checkedMiddleware]),
|
|
1140
|
+
}]);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1035
1143
|
function __velarServeDocumentationText(value, name, maximum) {
|
|
1036
1144
|
if (value == null) return null;
|
|
1037
1145
|
if (typeof value !== "string" || value.length === 0 || value.length > maximum || /[\0]/u.test(value)) {
|
|
@@ -1118,7 +1226,7 @@ function __velarServeBodyLimit(value) {
|
|
|
1118
1226
|
return value;
|
|
1119
1227
|
}
|
|
1120
1228
|
|
|
1121
|
-
function __velarServeAppValue(name, routes, lifecycles = []) {
|
|
1229
|
+
function __velarServeAppValue(name, routes, lifecycles = [], notFound = null) {
|
|
1122
1230
|
const router = __velarServeRouter(routes);
|
|
1123
1231
|
return __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{
|
|
1124
1232
|
[__velarServeAppMarker]: true,
|
|
@@ -1126,34 +1234,54 @@ function __velarServeAppValue(name, routes, lifecycles = []) {
|
|
|
1126
1234
|
routes: __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [routes]),
|
|
1127
1235
|
router,
|
|
1128
1236
|
lifecycles: __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [lifecycles]),
|
|
1237
|
+
notFound,
|
|
1129
1238
|
}]);
|
|
1130
1239
|
}
|
|
1131
1240
|
|
|
1132
1241
|
function __velarCreateServeApp(name, items) {
|
|
1133
1242
|
if (typeof name !== "string" || name.length === 0 || name.length > 256) throw new __velarServeTypeError("ServeApp name must be bounded text");
|
|
1134
|
-
if (!__velarServeIsArray(items) || items.length > __velarServeMaxRoutes) throw new __velarServeTypeError("ServeApp items cannot exceed 4096
|
|
1243
|
+
if (!__velarServeIsArray(items) || items.length > __velarServeMaxRoutes + 1) throw new __velarServeTypeError("ServeApp items cannot exceed 4096 routes and one fallback");
|
|
1135
1244
|
const routes = [];
|
|
1136
1245
|
const lifecycles = [];
|
|
1246
|
+
let notFound = null;
|
|
1247
|
+
// D90 R19(b): assembly is the moment the final table exists, so the final
|
|
1248
|
+
// table is judged here — a conflict names both routes and both origins,
|
|
1249
|
+
// because the statically invisible half of a collision is exactly the one
|
|
1250
|
+
// the author cannot see in his own file.
|
|
1137
1251
|
const shapes = new __velarServeMap();
|
|
1138
|
-
const
|
|
1252
|
+
const describeRoute = entry => "'" + entry.route.method + " " + entry.route.path + "'"
|
|
1253
|
+
+ (entry.source === null ? " declared by this server" : " composed in from '" + entry.source + "'");
|
|
1254
|
+
const append = (route, source) => {
|
|
1139
1255
|
if (routes.length >= __velarServeMaxRoutes) throw new __velarServeRangeError("ServeApp cannot contain more than 4096 routes after composition");
|
|
1140
1256
|
const key = route.method + " " + __velarServeRouteShape(route.path);
|
|
1141
|
-
|
|
1142
|
-
|
|
1257
|
+
const previous = __velarServeCall(__velarServeMapGet, shapes, [key]);
|
|
1258
|
+
if (previous !== undefined) {
|
|
1259
|
+
throw new __velarServeTypeError("ServeApp '" + name + "' contains conflicting routes: " + describeRoute({route, source})
|
|
1260
|
+
+ " and " + describeRoute(previous) + " both answer '" + key + "' — narrow or remove one");
|
|
1261
|
+
}
|
|
1262
|
+
__velarServeCall(__velarServeMapSet, shapes, [key, {route, source}]);
|
|
1143
1263
|
routes[routes.length] = route;
|
|
1144
1264
|
};
|
|
1145
1265
|
for (let index = 0; index < items.length; index += 1) {
|
|
1146
1266
|
const item = items[index];
|
|
1147
|
-
if (__velarServeIsRoute(item)) append(item);
|
|
1267
|
+
if (__velarServeIsRoute(item)) append(item, null);
|
|
1268
|
+
else if (__velarServeIsNotFound(item)) {
|
|
1269
|
+
if (notFound !== null) throw new __velarServeTypeError("ServeApp contains more than one @notFound fallback");
|
|
1270
|
+
notFound = item;
|
|
1271
|
+
}
|
|
1148
1272
|
else if (__velarServeIsApp(item)) {
|
|
1149
|
-
for (let route = 0; route < item.routes.length; route += 1) append(item.routes[route]);
|
|
1273
|
+
for (let route = 0; route < item.routes.length; route += 1) append(item.routes[route], item.name);
|
|
1150
1274
|
for (let hook = 0; hook < item.lifecycles.length; hook += 1) {
|
|
1151
1275
|
if (lifecycles.length >= __velarServeMaxLifecycles) throw new __velarServeRangeError("ServeApp cannot contain more than 4096 lifecycle pairs after composition");
|
|
1152
1276
|
lifecycles[lifecycles.length] = item.lifecycles[hook];
|
|
1153
1277
|
}
|
|
1278
|
+
if (item.notFound !== null) {
|
|
1279
|
+
if (notFound !== null) throw new __velarServeTypeError("ServeApp contains more than one @notFound fallback");
|
|
1280
|
+
notFound = item.notFound;
|
|
1281
|
+
}
|
|
1154
1282
|
} else throw new __velarServeTypeError("A server composition entry must be a ServeApp");
|
|
1155
1283
|
}
|
|
1156
|
-
return __velarServeAppValue(name, routes, lifecycles);
|
|
1284
|
+
return __velarServeAppValue(name, routes, lifecycles, notFound);
|
|
1157
1285
|
}
|
|
1158
1286
|
|
|
1159
1287
|
export function prefix(path, app) {
|
|
@@ -1163,6 +1291,7 @@ export function prefix(path, app) {
|
|
|
1163
1291
|
throw new __velarServeTypeError("prefix path must contain only literal path segments");
|
|
1164
1292
|
}
|
|
1165
1293
|
if (path === "/") return app;
|
|
1294
|
+
if (app.notFound !== null) throw new __velarServeTypeError("prefix cannot scope @notFound; compose the fallback on the final server instead");
|
|
1166
1295
|
const routes = [];
|
|
1167
1296
|
for (let index = 0; index < app.routes.length; index += 1) {
|
|
1168
1297
|
const route = app.routes[index];
|
|
@@ -1175,7 +1304,7 @@ export function prefix(path, app) {
|
|
|
1175
1304
|
);
|
|
1176
1305
|
}
|
|
1177
1306
|
const output = __velarCreateServeApp(app.name, routes);
|
|
1178
|
-
return __velarServeAppValue(output.name, output.routes, app.lifecycles);
|
|
1307
|
+
return __velarServeAppValue(output.name, output.routes, app.lifecycles, null);
|
|
1179
1308
|
}
|
|
1180
1309
|
|
|
1181
1310
|
export function staticFiles(path, root, fallback = null) {
|
|
@@ -1208,8 +1337,18 @@ export function use(app, middleware) {
|
|
|
1208
1337
|
{...__velarServeRouteMetadata(route), middleware: entries},
|
|
1209
1338
|
);
|
|
1210
1339
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1340
|
+
let notFound = app.notFound;
|
|
1341
|
+
if (notFound !== null) {
|
|
1342
|
+
const entries = [];
|
|
1343
|
+
for (let item = 0; item < notFound.middleware.length; item += 1) entries[entries.length] = notFound.middleware[item];
|
|
1344
|
+
for (let item = 0; item < additions.length; item += 1) entries[entries.length] = additions[item];
|
|
1345
|
+
notFound = __velarCreateServeNotFound(notFound.handler, entries);
|
|
1346
|
+
}
|
|
1347
|
+
const items = [];
|
|
1348
|
+
for (let index = 0; index < routes.length; index += 1) items[items.length] = routes[index];
|
|
1349
|
+
if (notFound !== null) items[items.length] = notFound;
|
|
1350
|
+
const output = __velarCreateServeApp(app.name, items);
|
|
1351
|
+
return __velarServeAppValue(output.name, output.routes, app.lifecycles, output.notFound);
|
|
1213
1352
|
}
|
|
1214
1353
|
|
|
1215
1354
|
export function bodyLimit(app, maxBytes) {
|
|
@@ -1227,7 +1366,7 @@ export function bodyLimit(app, maxBytes) {
|
|
|
1227
1366
|
);
|
|
1228
1367
|
}
|
|
1229
1368
|
const output = __velarCreateServeApp(app.name, routes);
|
|
1230
|
-
return __velarServeAppValue(output.name, output.routes, app.lifecycles);
|
|
1369
|
+
return __velarServeAppValue(output.name, output.routes, app.lifecycles, app.notFound);
|
|
1231
1370
|
}
|
|
1232
1371
|
|
|
1233
1372
|
export function lifecycle(app, startup = null, shutdown = null) {
|
|
@@ -1237,7 +1376,7 @@ export function lifecycle(app, startup = null, shutdown = null) {
|
|
|
1237
1376
|
const lifecycles = [];
|
|
1238
1377
|
for (let index = 0; index < app.lifecycles.length; index += 1) lifecycles[index] = app.lifecycles[index];
|
|
1239
1378
|
lifecycles[lifecycles.length] = __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{startup, shutdown}]);
|
|
1240
|
-
return __velarServeAppValue(app.name, app.routes, lifecycles);
|
|
1379
|
+
return __velarServeAppValue(app.name, app.routes, lifecycles, app.notFound);
|
|
1241
1380
|
}
|
|
1242
1381
|
|
|
1243
1382
|
function __velarServeResponseWithHeaders(value, additions) {
|
|
@@ -1293,6 +1432,7 @@ function __velarServeCors(origins = ["*"], methods = ["GET", "POST", "PUT", "PAT
|
|
|
1293
1432
|
methods = __velarServeStringList(methods, "middleware.cors methods");
|
|
1294
1433
|
headers = __velarServeStringList(headers, "middleware.cors headers");
|
|
1295
1434
|
if (typeof credentials !== "boolean" || !__velarServeIsSafeInteger(maxAge) || maxAge < 0 || maxAge > 86400) throw new __velarServeTypeError("middleware.cors options are invalid");
|
|
1435
|
+
if (credentials && __velarServeCall(__velarServeArrayIncludes, origins, ["*"])) throw new __velarServeTypeError("middleware.cors cannot combine credentials with the '*' origin wildcard");
|
|
1296
1436
|
return async (request, next) => {
|
|
1297
1437
|
const origin = __velarServeCall(__velarServeMapHas, request.headers, ["origin"]) ? __velarServeCall(__velarServeMapGet, request.headers, ["origin"]) : null;
|
|
1298
1438
|
const wildcard = __velarServeCall(__velarServeArrayIncludes, origins, ["*"]);
|
|
@@ -1423,18 +1563,26 @@ function __velarServeTimeout(milliseconds) {
|
|
|
1423
1563
|
if (__velarServeActiveTimeouts >= __velarServeMaxActiveTimeouts) {
|
|
1424
1564
|
return {status: 503, json: {error: "server_busy"}, headers: new __velarServeMap([["retry-after", "1"]])};
|
|
1425
1565
|
}
|
|
1426
|
-
__velarServeActiveTimeouts += 1;
|
|
1427
1566
|
let timer = null;
|
|
1428
|
-
|
|
1429
|
-
let pending;
|
|
1430
|
-
try { pending = next(); }
|
|
1431
|
-
catch (error) { __velarServeActiveTimeouts -= 1; throw error; }
|
|
1567
|
+
const pending = next();
|
|
1432
1568
|
const expired = new __velarServePromise(resolve => { timer = __velarServeCall(__velarServeSetTimeout, globalThis, [() => resolve(__velarServeMissing), milliseconds]); });
|
|
1433
1569
|
try {
|
|
1434
1570
|
const result = await __velarServeCall(__velarServePromiseRace, __velarServePromise, [__velarServeCall(__velarServeObjectFreeze, __velarServeObject, [[pending, expired]])]);
|
|
1435
1571
|
if (result !== __velarServeMissing) return result;
|
|
1436
|
-
detached = true;
|
|
1437
1572
|
__velarServeCancellation.__velarCancel(request.cancellation, "Request timed out");
|
|
1573
|
+
// The published bound counts unfinished timed-out continuations, and the
|
|
1574
|
+
// background reservation at __velarServeRunBackground subtracts exactly
|
|
1575
|
+
// this many slots from the process total. Admission alone cannot hold
|
|
1576
|
+
// that count: a burst admitted while nothing was detached can expire
|
|
1577
|
+
// together. When the detached budget is full the request has already been
|
|
1578
|
+
// cancelled, so wait for it to unwind here instead of detaching work the
|
|
1579
|
+
// process no longer accounts for.
|
|
1580
|
+
if (__velarServeActiveTimeouts >= __velarServeMaxActiveTimeouts) {
|
|
1581
|
+
try { await pending; }
|
|
1582
|
+
catch (error) { __velarServeReportFailure(error); }
|
|
1583
|
+
return {status: 504, json: {error: "request_timeout"}};
|
|
1584
|
+
}
|
|
1585
|
+
__velarServeActiveTimeouts += 1;
|
|
1438
1586
|
__velarServeActiveBackgroundTasks += 1;
|
|
1439
1587
|
const settlement = (async () => {
|
|
1440
1588
|
try { await pending; }
|
|
@@ -1457,7 +1605,6 @@ function __velarServeTimeout(milliseconds) {
|
|
|
1457
1605
|
};
|
|
1458
1606
|
} finally {
|
|
1459
1607
|
if (timer !== null) __velarServeCall(__velarServeClearTimeout, globalThis, [timer]);
|
|
1460
|
-
if (!detached) __velarServeActiveTimeouts -= 1;
|
|
1461
1608
|
}
|
|
1462
1609
|
};
|
|
1463
1610
|
}
|
|
@@ -1561,15 +1708,19 @@ function __velarServeDecodeScalar(raw, parameter) {
|
|
|
1561
1708
|
function __velarServeCookieValue(request, name) {
|
|
1562
1709
|
if (!__velarServeCall(__velarServeMapHas, request.headers, ["cookie"])) return __velarServeMissing;
|
|
1563
1710
|
const pieces = __velarServeCall(__velarServeStringSplit, __velarServeCall(__velarServeMapGet, request.headers, ["cookie"]), [";"]);
|
|
1711
|
+
let encoded = null;
|
|
1712
|
+
let matches = 0;
|
|
1564
1713
|
for (let index = 0; index < pieces.length; index += 1) {
|
|
1565
1714
|
const piece = __velarServeCall(__velarServeStringTrim, pieces[index], []);
|
|
1566
1715
|
const separator = __velarServeCall(__velarServeStringIndexOf, piece, ["="]);
|
|
1567
1716
|
if (separator < 0 || __velarServeCall(__velarServeStringSlice, piece, [0, separator]) !== name) continue;
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1717
|
+
matches += 1;
|
|
1718
|
+
if (matches > 1) throw new HttpError(400, {error: "duplicate_cookie", parameter: name});
|
|
1719
|
+
encoded = __velarServeCall(__velarServeStringSlice, piece, [separator + 1]);
|
|
1571
1720
|
}
|
|
1572
|
-
return __velarServeMissing;
|
|
1721
|
+
if (matches === 0) return __velarServeMissing;
|
|
1722
|
+
try { return __velarServeCall(__velarServeDecodeURIComponent, undefined, [encoded]); }
|
|
1723
|
+
catch { throw new HttpError(400, {error: "invalid_cookie", parameter: name}); }
|
|
1573
1724
|
}
|
|
1574
1725
|
|
|
1575
1726
|
function __velarServeNamedInputRaw(descriptor, parameterName, request) {
|
|
@@ -1770,6 +1921,17 @@ function __velarServeMultipartHeaders(text) {
|
|
|
1770
1921
|
return output;
|
|
1771
1922
|
}
|
|
1772
1923
|
|
|
1924
|
+
function __velarServeUploadBasename(filename) {
|
|
1925
|
+
// A client may send a full path, including a Windows path with backslashes.
|
|
1926
|
+
// An Upload name is one file name, never a path an application can compose
|
|
1927
|
+
// into a directory it did not intend to write.
|
|
1928
|
+
const slashed = __velarServeCall(__velarServeStringSplit, filename, ["/"]);
|
|
1929
|
+
const separated = __velarServeCall(__velarServeStringSplit, slashed[slashed.length - 1], ["\\"]);
|
|
1930
|
+
const base = separated[separated.length - 1];
|
|
1931
|
+
if (base === "" || base === "." || base === ".." || __velarServeCall(__velarServeStringIncludes, base, ["\0"])) throw new HttpError(400, {error: "invalid_multipart"});
|
|
1932
|
+
return base;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1773
1935
|
function __velarServeMultipart(data, boundary) {
|
|
1774
1936
|
const opening = __velarServeBytePattern("--" + boundary);
|
|
1775
1937
|
const separator = __velarServeBytePattern("\r\n\r\n");
|
|
@@ -1801,8 +1963,9 @@ function __velarServeMultipart(data, boundary) {
|
|
|
1801
1963
|
__velarServeAddFormField(fields, name, __velarServeDecodeBytes(part));
|
|
1802
1964
|
} else {
|
|
1803
1965
|
if (filename.length > 1024 || __velarServeCall(__velarServeMapHas, files, [name])) throw new HttpError(400, {error: "invalid_multipart"});
|
|
1966
|
+
const base = __velarServeUploadBasename(filename);
|
|
1804
1967
|
const contentType = typeof headers["content-type"] === "string" ? headers["content-type"] : "application/octet-stream";
|
|
1805
|
-
__velarServeCall(__velarServeMapSet, files, [name, __velarServeUploadValue(name,
|
|
1968
|
+
__velarServeCall(__velarServeMapSet, files, [name, __velarServeUploadValue(name, base, contentType, part, uploadStates)]);
|
|
1806
1969
|
}
|
|
1807
1970
|
parts += 1;
|
|
1808
1971
|
if (parts > 128) throw new HttpError(413, {error: "too_many_form_parts"});
|
|
@@ -1981,12 +2144,26 @@ function __velarServeJsonContentType(headers) {
|
|
|
1981
2144
|
|| __velarServeCall(__velarServeStringStartsWith, value, ["application/"]) && __velarServeCall(__velarServeStringEndsWith, value, ["+json"]);
|
|
1982
2145
|
}
|
|
1983
2146
|
|
|
2147
|
+
function __velarServeIsResponseAttempt(value) {
|
|
2148
|
+
if (!value || typeof value !== "object" || __velarServeIsArray(value)) return false;
|
|
2149
|
+
if (!__velarServeOwnDescriptor(value, "status")) return false;
|
|
2150
|
+
return !!(__velarServeOwnDescriptor(value, "json") || __velarServeOwnDescriptor(value, "text") || __velarServeOwnDescriptor(value, "stream"));
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1984
2153
|
function __velarServeAutomaticResponse(value) {
|
|
1985
2154
|
if (__velarServeIsFileResponse(value)) return value;
|
|
2155
|
+
if (__velarServeIsResponseAttempt(value)) return __velarServeResponse(value);
|
|
1986
2156
|
try { return __velarServeResponse(value); }
|
|
1987
2157
|
catch { return __velarServeResponse({status: 200, json: value}); }
|
|
1988
2158
|
}
|
|
1989
2159
|
|
|
2160
|
+
function __velarServeNotFoundResponse(value) {
|
|
2161
|
+
if (__velarServeIsFileResponse(value)) return value;
|
|
2162
|
+
if (__velarServeIsResponseAttempt(value)) return __velarServeResponse(value);
|
|
2163
|
+
try { return __velarServeResponse(value); }
|
|
2164
|
+
catch { return __velarServeResponse({status: 404, json: value}); }
|
|
2165
|
+
}
|
|
2166
|
+
|
|
1990
2167
|
async function __velarServeHandleAppResponse(app, request, maxBodyBytes, context) {
|
|
1991
2168
|
try {
|
|
1992
2169
|
const actual = __velarServeCall(__velarServeStringSplit, request.path, ["/"]);
|
|
@@ -2014,6 +2191,13 @@ async function __velarServeHandleAppResponse(app, request, maxBodyBytes, context
|
|
|
2014
2191
|
const methods = __velarServeAllowedMethods(allowed);
|
|
2015
2192
|
return await __velarServeApplyMiddleware(pathOwner.route, request, async () => ({status: 405, json: {error: "method_not_allowed"}, headers: new __velarServeMap([["allow", __velarServeCall(__velarServeArrayJoin, methods, [", "])]])}));
|
|
2016
2193
|
}
|
|
2194
|
+
if (app.notFound !== null) {
|
|
2195
|
+
return await __velarServeApplyMiddleware(
|
|
2196
|
+
app.notFound,
|
|
2197
|
+
request,
|
|
2198
|
+
async () => __velarServeNotFoundResponse(await __velarServeCall(app.notFound.handler, undefined, [request])),
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2017
2201
|
return {status: 404, json: {error: "not_found"}};
|
|
2018
2202
|
}
|
|
2019
2203
|
let selected = candidates[0];
|
|
@@ -2025,6 +2209,7 @@ async function __velarServeHandleAppResponse(app, request, maxBodyBytes, context
|
|
|
2025
2209
|
return await __velarServeApplyMiddleware(selected.route, request, invokeRoute);
|
|
2026
2210
|
} catch (error) {
|
|
2027
2211
|
if (error instanceof HttpError) return {status: error.status, json: error.body, headers: error.headers};
|
|
2212
|
+
if (error instanceof RequestBodyTooLargeError) return {status: 413, json: {error: "request_too_large"}};
|
|
2028
2213
|
throw error;
|
|
2029
2214
|
}
|
|
2030
2215
|
}
|
|
@@ -2462,7 +2647,7 @@ function __velarServeDocumentRoutes(app, documentation) {
|
|
|
2462
2647
|
output[output.length] = __velarCreateServeRoute(route.method, route.path, route.parameters, route.handler, __velarServeRouteMetadata(route, __velarServeCall(__velarServeMapGet, configured, [key])));
|
|
2463
2648
|
}
|
|
2464
2649
|
if (__velarServeCall(__velarServeMapSize, seen, []) !== size) throw new __velarServeTypeError("docs routes contains a route that the application does not declare");
|
|
2465
|
-
return __velarServeAppValue(app.name, output, app.lifecycles);
|
|
2650
|
+
return __velarServeAppValue(app.name, output, app.lifecycles, app.notFound);
|
|
2466
2651
|
}
|
|
2467
2652
|
|
|
2468
2653
|
function __velarServeOpenApiPath(path) {
|
|
@@ -2556,6 +2741,16 @@ function __velarServeRequest(value) {
|
|
|
2556
2741
|
}])};
|
|
2557
2742
|
}
|
|
2558
2743
|
|
|
2744
|
+
// The budget error can also surface after a response has started, where the host
|
|
2745
|
+
// refuses a second terminal response; that attempt fails and the request falls
|
|
2746
|
+
// through to the opaque failure exactly as any other late error does.
|
|
2747
|
+
async function __velarServeShedOutbound(handle) {
|
|
2748
|
+
try {
|
|
2749
|
+
await __velarNodeHostInvoke("serve.respond", [handle, 503, [["retry-after", "1"]], "json", '{"error":"outbound_budget_exhausted"}', null, null, []]);
|
|
2750
|
+
return true;
|
|
2751
|
+
} catch { return false; }
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2559
2754
|
async function __velarServeWriteResponse(handle, value) {
|
|
2560
2755
|
let cleanup = null;
|
|
2561
2756
|
let backgroundTasks = null;
|
|
@@ -2625,7 +2820,8 @@ function __velarServeNativeHeaders(request) {
|
|
|
2625
2820
|
return output;
|
|
2626
2821
|
}
|
|
2627
2822
|
|
|
2628
|
-
function __velarServeNativeRequest(request) {
|
|
2823
|
+
function __velarServeNativeRequest(request, maximum = __velarServeMaxBodyBytes) {
|
|
2824
|
+
maximum = __velarServeBodyLimit(maximum);
|
|
2629
2825
|
const method = request.method ?? "GET";
|
|
2630
2826
|
if (typeof method !== "string" || !__velarServeCall(__velarServeRegExpTest, __velarServeMethodPattern, [method])) throw new __velarServeTypeError("Native HTTP method is invalid");
|
|
2631
2827
|
const target = request.url ?? "/";
|
|
@@ -2642,7 +2838,7 @@ function __velarServeNativeRequest(request) {
|
|
|
2642
2838
|
try {
|
|
2643
2839
|
for await (const chunk of request) {
|
|
2644
2840
|
const data = chunk instanceof __velarServeUint8Array ? chunk : __velarServeCall(__velarServeTextEncode, __velarServeUtf8Encoder, [__velarServeString(chunk)]);
|
|
2645
|
-
if (total + data.byteLength >
|
|
2841
|
+
if (total + data.byteLength > maximum) { request.resume(); throw new RequestBodyTooLargeError(maximum); }
|
|
2646
2842
|
__velarServeReserveOutbound(data.byteLength);
|
|
2647
2843
|
total += data.byteLength;
|
|
2648
2844
|
reservedBodyBytes += data.byteLength;
|
|
@@ -2665,20 +2861,21 @@ function __velarServeNativeRequest(request) {
|
|
|
2665
2861
|
})();
|
|
2666
2862
|
return await bodyPromise;
|
|
2667
2863
|
};
|
|
2668
|
-
const bytes = async (maxBytes =
|
|
2864
|
+
const bytes = async (maxBytes = maximum) => {
|
|
2669
2865
|
if (!__velarServeIsSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > __velarServeMaxBodyBytes) throw new __velarServeRangeError("Request body maxBytes must be an integer from 1 through 16777216");
|
|
2670
2866
|
const data = await rawBody();
|
|
2671
|
-
|
|
2867
|
+
const effective = maxBytes > maximum ? maximum : maxBytes;
|
|
2868
|
+
if (data.byteLength > effective) throw new RequestBodyTooLargeError(effective);
|
|
2672
2869
|
return data;
|
|
2673
2870
|
};
|
|
2674
|
-
const body = async (maxBytes =
|
|
2871
|
+
const body = async (maxBytes = maximum) => {
|
|
2675
2872
|
const data = await bytes(maxBytes);
|
|
2676
2873
|
try { return __velarServeCall(__velarServeTextDecode, __velarServeUtf8Decoder, [data]); }
|
|
2677
2874
|
catch { throw new __velarServeTypeError("Request body must be valid UTF-8 text"); }
|
|
2678
2875
|
};
|
|
2679
|
-
const json = async (maxBytes =
|
|
2876
|
+
const json = async (maxBytes = maximum) => __velarJsonParse(await body(maxBytes), "ServeRequest JSON text");
|
|
2680
2877
|
return {
|
|
2681
|
-
request: __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{method, path, query: query.values, queryAll: query.all, headers: __velarServeNativeHeaders(request), cancellation, text: body, bytes, json, parse: async (Type, maxBytes =
|
|
2878
|
+
request: __velarServeCall(__velarServeObjectFreeze, __velarServeObject, [{method, path, query: query.values, queryAll: query.all, headers: __velarServeNativeHeaders(request), cancellation, text: body, bytes, json, parse: async (Type, maxBytes = maximum) => { Type = __velarRequireRuntimeType(Type, "ServeRequest.parse"); return Type.parse(await json(maxBytes)); }}]),
|
|
2682
2879
|
cancellation,
|
|
2683
2880
|
cleanup() { if (reservedBodyBytes > 0) { __velarServeReleaseOutbound(reservedBodyBytes); reservedBodyBytes = 0; } return null; },
|
|
2684
2881
|
};
|
|
@@ -2693,14 +2890,49 @@ function __velarServeNativeSetHeaders(response, headers, cookies = []) {
|
|
|
2693
2890
|
for (let index = 0; index < cookies.length; index += 1) allCookies[allCookies.length] = cookies[index];
|
|
2694
2891
|
if (allCookies.length > 0) response.setHeader("Set-Cookie", allCookies);
|
|
2695
2892
|
}
|
|
2893
|
+
// Every error branch of __velarServeHandleNative answers with a body of its own,
|
|
2894
|
+
// so it has to start from an empty header set: a content-length staged for the
|
|
2895
|
+
// response that failed makes the client wait for bytes that will never arrive,
|
|
2896
|
+
// and a Set-Cookie staged by a handler whose request was never served hands out
|
|
2897
|
+
// a session for nothing. The isolated-host transport gets this for free — it
|
|
2898
|
+
// sheds before the host ever sets a header — so this is the native transport
|
|
2899
|
+
// reaching the same state.
|
|
2900
|
+
function __velarServeNativeResetHeaders(response) {
|
|
2901
|
+
const names = response.getHeaderNames();
|
|
2902
|
+
for (let index = 0; index < names.length; index += 1) response.removeHeader(names[index]);
|
|
2903
|
+
}
|
|
2904
|
+
class __velarServeNativeNotFound extends __velarServeError {}
|
|
2905
|
+
function __velarServeNativeMissing(error) {
|
|
2906
|
+
const code = error?.code;
|
|
2907
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR";
|
|
2908
|
+
}
|
|
2909
|
+
// A relative path carries .. only as a whole segment, so a bare two-dot prefix
|
|
2910
|
+
// test also refuses an ordinary top-level file whose own name begins with two
|
|
2911
|
+
// dots — the same over-strict form the host worker's containment check carried.
|
|
2912
|
+
// The operations bag has no separator to consult and may come from a bridge
|
|
2913
|
+
// embedding, so both separators are refused: fail closed on Windows rather than
|
|
2914
|
+
// trust a default.
|
|
2915
|
+
function __velarServeNativeEscapes(path, operations) {
|
|
2916
|
+
return path === ".." || path.startsWith("../") || path.startsWith("..\\") || operations.isAbsolute(path);
|
|
2917
|
+
}
|
|
2696
2918
|
async function __velarServeNativeFile(value, operations) {
|
|
2697
|
-
|
|
2919
|
+
let root;
|
|
2920
|
+
// A static root that does not exist is the same miss as a file that does not
|
|
2921
|
+
// exist: reporting it as a failure would answer 500 and write the absolute
|
|
2922
|
+
// deployment path to stderr, which the host transport never does.
|
|
2923
|
+
try { root = await operations.realpath(operations.resolve(value.root)); }
|
|
2924
|
+
catch (error) { if (__velarServeNativeMissing(error)) throw new __velarServeNativeNotFound("fileResponse root does not name a directory"); throw error; }
|
|
2698
2925
|
const load = async path => {
|
|
2699
|
-
|
|
2926
|
+
let target;
|
|
2927
|
+
try { target = await operations.realpath(operations.resolve(root, path.startsWith("/") ? "." + path : path)); }
|
|
2928
|
+
catch (error) { if (__velarServeNativeMissing(error)) throw new __velarServeNativeNotFound("fileResponse path does not name a file"); throw error; }
|
|
2700
2929
|
const relative = operations.relative(root, target);
|
|
2701
|
-
if (relative
|
|
2702
|
-
|
|
2703
|
-
|
|
2930
|
+
if (__velarServeNativeEscapes(relative, operations)) throw new __velarServeNativeNotFound("fileResponse path escapes its root");
|
|
2931
|
+
let info;
|
|
2932
|
+
try { info = await operations.stat(target); }
|
|
2933
|
+
catch (error) { if (__velarServeNativeMissing(error)) throw new __velarServeNativeNotFound("fileResponse path does not name a file"); throw error; }
|
|
2934
|
+
if (!info.isFile()) throw new __velarServeNativeNotFound("fileResponse path does not name a file");
|
|
2935
|
+
if (info.size > 64 * 1024 * 1024) throw new __velarServeRangeError("fileResponse file exceeds 64 MiB");
|
|
2704
2936
|
return {target, info};
|
|
2705
2937
|
};
|
|
2706
2938
|
try { return await load(value.path); } catch (error) { if (value.fallback === null) throw error; return load(value.fallback); }
|
|
@@ -2770,13 +3002,13 @@ async function __velarServeNativeBody(response, value, checked, suppressBody, op
|
|
|
2770
3002
|
if (!response.hasHeader("vary")) response.setHeader("vary", "Accept-Encoding");
|
|
2771
3003
|
return __velarServeWithOutbound(compressed.byteLength, () => __velarServeNativeEnd(response, compressed));
|
|
2772
3004
|
}
|
|
2773
|
-
async function __velarServeHandleNative(handler, request, response, operations) {
|
|
3005
|
+
async function __velarServeHandleNative(handler, request, response, operations, maxBodyBytes = __velarServeMaxBodyBytes) {
|
|
2774
3006
|
let cleanup = null;
|
|
2775
3007
|
let backgroundTasks = null;
|
|
2776
3008
|
let incoming = null;
|
|
2777
3009
|
let disconnected = null;
|
|
2778
3010
|
try {
|
|
2779
|
-
incoming = __velarServeNativeRequest(request);
|
|
3011
|
+
incoming = __velarServeNativeRequest(request, maxBodyBytes);
|
|
2780
3012
|
disconnected = () => { if (!response.writableFinished) __velarServeCancellation.__velarCancel(incoming.cancellation, "client_disconnect"); };
|
|
2781
3013
|
request.once("aborted", disconnected);
|
|
2782
3014
|
response.once("close", disconnected);
|
|
@@ -2791,7 +3023,34 @@ async function __velarServeHandleNative(handler, request, response, operations)
|
|
|
2791
3023
|
let writing = false;
|
|
2792
3024
|
const write = async chunk => { if (writing) throw new __velarServeError("ServeResponse allows only one active stream write"); writing = true; try { if (typeof chunk !== "string" || __velarUtf8ByteLength(chunk) > 1024 * 1024) throw new __velarServeTypeError("ServeResponse.stream chunks must be text of at most 1 MiB"); if (!suppressBody) await __velarServeWithOutbound(__velarUtf8ByteLength(chunk), () => __velarServeNativeWrite(response, chunk)); return null; } finally { writing = false; } };
|
|
2793
3025
|
const result = await checked.stream(write); if (result !== null) throw new __velarServeTypeError("ServeResponse.stream producer must resolve to null"); if (writing) throw new __velarServeError("ServeResponse stream producer returned before its write completed"); await __velarServeNativeEnd(response); return null;
|
|
2794
|
-
} catch (error) {
|
|
3026
|
+
} catch (error) {
|
|
3027
|
+
if (error instanceof RequestBodyTooLargeError && !response.headersSent) {
|
|
3028
|
+
__velarServeNativeResetHeaders(response);
|
|
3029
|
+
response.statusCode = 413;
|
|
3030
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
3031
|
+
response.end(request.method === "HEAD" ? undefined : '{"error":"request_too_large"}');
|
|
3032
|
+
return null;
|
|
3033
|
+
}
|
|
3034
|
+
if (error instanceof __velarServeNativeNotFound && !response.headersSent) {
|
|
3035
|
+
__velarServeNativeResetHeaders(response);
|
|
3036
|
+
response.statusCode = 404;
|
|
3037
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
3038
|
+
response.end(request.method === "HEAD" ? undefined : "Not found");
|
|
3039
|
+
return null;
|
|
3040
|
+
}
|
|
3041
|
+
if (error instanceof __velarServeOutboundBudgetError && !response.headersSent) {
|
|
3042
|
+
__velarServeNativeResetHeaders(response);
|
|
3043
|
+
response.statusCode = 503;
|
|
3044
|
+
response.setHeader("retry-after", "1");
|
|
3045
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
3046
|
+
response.end(request.method === "HEAD" ? undefined : '{"error":"outbound_budget_exhausted"}');
|
|
3047
|
+
return null;
|
|
3048
|
+
}
|
|
3049
|
+
__velarServeReportFailure(error);
|
|
3050
|
+
if (!response.headersSent) { __velarServeNativeResetHeaders(response); response.statusCode = 500; response.setHeader("content-type", "text/plain; charset=utf-8"); response.end("Internal server error"); }
|
|
3051
|
+
else response.destroy();
|
|
3052
|
+
return null;
|
|
3053
|
+
}
|
|
2795
3054
|
finally {
|
|
2796
3055
|
if (disconnected !== null) { request.off("aborted", disconnected); response.off("close", disconnected); }
|
|
2797
3056
|
await __velarServeRunBackground(backgroundTasks);
|
|
@@ -3073,6 +3332,7 @@ async function __velarServeDispatch(event) {
|
|
|
3073
3332
|
if (typeof handler !== "function") { __velarServeReportFailure(new __velarServeError("Node host requested an unknown server token")); await __velarNodeHostInvoke("serve.fail", [value.handle]); return; }
|
|
3074
3333
|
try { await __velarServeWriteResponse(value.handle, await handler(value.request)); }
|
|
3075
3334
|
catch (error) {
|
|
3335
|
+
if (error instanceof __velarServeOutboundBudgetError && await __velarServeShedOutbound(value.handle)) return;
|
|
3076
3336
|
__velarServeReportFailure(error);
|
|
3077
3337
|
try { await __velarNodeHostInvoke("serve.fail", [value.handle]); }
|
|
3078
3338
|
catch {}
|