@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.
Files changed (41) hide show
  1. package/README.md +40 -11
  2. package/dist/compiler.d.ts.map +1 -1
  3. package/dist/compiler.js +99 -23
  4. package/dist/compiler.js.map +1 -1
  5. package/dist/http-runtime.d.ts.map +1 -1
  6. package/dist/http-runtime.js +19 -7
  7. package/dist/http-runtime.js.map +1 -1
  8. package/dist/node-host-runtime.d.ts.map +1 -1
  9. package/dist/node-host-runtime.js +14 -7
  10. package/dist/node-host-runtime.js.map +1 -1
  11. package/dist/node-host-worker-runtime.d.ts.map +1 -1
  12. package/dist/node-host-worker-runtime.js +59 -31
  13. package/dist/node-host-worker-runtime.js.map +1 -1
  14. package/dist/project-config.d.ts +1 -1
  15. package/dist/project-config.d.ts.map +1 -1
  16. package/dist/route-shape.d.ts +20 -0
  17. package/dist/route-shape.d.ts.map +1 -0
  18. package/dist/route-shape.js +36 -0
  19. package/dist/route-shape.js.map +1 -0
  20. package/dist/serve-runtime.d.ts.map +1 -1
  21. package/dist/serve-runtime.js +312 -52
  22. package/dist/serve-runtime.js.map +1 -1
  23. package/dist/server-analyzer.d.ts +52 -0
  24. package/dist/server-analyzer.d.ts.map +1 -1
  25. package/dist/server-analyzer.js +377 -12
  26. package/dist/server-analyzer.js.map +1 -1
  27. package/dist/server-ast.d.ts +13 -1
  28. package/dist/server-ast.d.ts.map +1 -1
  29. package/dist/server-ast.js.map +1 -1
  30. package/dist/server-emitter.d.ts +1 -0
  31. package/dist/server-emitter.d.ts.map +1 -1
  32. package/dist/server-emitter.js +13 -2
  33. package/dist/server-emitter.js.map +1 -1
  34. package/dist/server-parser.d.ts +2 -0
  35. package/dist/server-parser.d.ts.map +1 -1
  36. package/dist/server-parser.js +45 -20
  37. package/dist/server-parser.js.map +1 -1
  38. package/dist/websocket-runtime.d.ts.map +1 -1
  39. package/dist/websocket-runtime.js +73 -17
  40. package/dist/websocket-runtime.js.map +1 -1
  41. package/package.json +2 -2
@@ -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 || __velarServeOutboundBytes + bytes > __velarServeMaxOutboundBytes) {
189
- throw new __velarServeRangeError("ServeResponse aggregate outbound byte budget is exhausted");
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 path => { await __velarServeWriteBytes(path, current()); return null; },
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
- const segments = __velarServeCall(__velarServeStringSplit, path, ["/"]);
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 entries");
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 append = route => {
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
- if (__velarServeCall(__velarServeMapHas, shapes, [key])) throw new __velarServeTypeError("ServeApp contains conflicting route '" + key + "'");
1142
- __velarServeCall(__velarServeMapSet, shapes, [key, true]);
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
- const output = __velarCreateServeApp(app.name, routes);
1212
- return __velarServeAppValue(output.name, output.routes, app.lifecycles);
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
- let detached = false;
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
- const encoded = __velarServeCall(__velarServeStringSlice, piece, [separator + 1]);
1569
- try { return __velarServeCall(__velarServeDecodeURIComponent, undefined, [encoded]); }
1570
- catch { throw new HttpError(400, {error: "invalid_cookie", parameter: name}); }
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, filename, contentType, part, uploadStates)]);
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 > __velarServeMaxBodyBytes) { request.resume(); throw new RequestBodyTooLargeError(__velarServeMaxBodyBytes); }
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 = __velarServeMaxBodyBytes) => {
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
- if (data.byteLength > maxBytes) throw new RequestBodyTooLargeError(maxBytes);
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 = __velarServeMaxBodyBytes) => {
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 = __velarServeMaxBodyBytes) => __velarJsonParse(await body(maxBytes), "ServeRequest JSON text");
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 = __velarServeMaxBodyBytes) => { Type = __velarRequireRuntimeType(Type, "ServeRequest.parse"); return Type.parse(await json(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
- const root = await operations.realpath(operations.resolve(value.root));
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
- const target = await operations.realpath(operations.resolve(root, path.startsWith("/") ? "." + path : path));
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.startsWith("..") || operations.isAbsolute(relative)) throw new __velarServeTypeError("fileResponse path escapes its root");
2702
- const info = await operations.stat(target);
2703
- if (!info.isFile() || info.size > 64 * 1024 * 1024) throw new __velarServeRangeError("fileResponse file exceeds 64 MiB");
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) { __velarServeReportFailure(error); if (!response.headersSent) { response.statusCode = 500; response.setHeader("content-type", "text/plain; charset=utf-8"); response.end("Internal server error"); } else response.destroy(); return null; }
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 {}