@velarscript/node 0.12.1 → 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.
@@ -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;
@@ -186,9 +187,8 @@ function __velarServeIsSafeInteger(value) {
186
187
  }
187
188
 
188
189
  function __velarServeReserveOutbound(bytes) {
189
- if (!__velarServeIsSafeInteger(bytes) || bytes < 0 || __velarServeOutboundBytes + bytes > __velarServeMaxOutboundBytes) {
190
- throw new __velarServeRangeError("ServeResponse aggregate outbound byte budget is exhausted");
191
- }
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();
192
192
  __velarServeOutboundBytes += bytes;
193
193
  }
194
194
 
@@ -430,6 +430,19 @@ export class HttpError extends __velarServeError {
430
430
  }
431
431
  }
432
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
+
433
446
  function __velarServeIsFileResponse(value) {
434
447
  if (!value || typeof value !== "object") return false;
435
448
  const descriptor = __velarServeOwnDescriptor(value, __velarServeFileMarker);
@@ -600,6 +613,74 @@ function __velarServeIsUpload(value) {
600
613
 
601
614
  export const Upload = __velarServeTypeObject(__velarServeIsUpload, "Upload values are created from multipart route inputs");
602
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
+
603
684
  function __velarServeUploadValue(name, filename, contentType, data, states) {
604
685
  if (!__velarServeBytesType.is(data)) throw new __velarServeTypeError("Upload data must be Bytes");
605
686
  const state = {data};
@@ -620,7 +701,11 @@ function __velarServeUploadValue(name, filename, contentType, data, states) {
620
701
  catch { throw new __velarServeTypeError("Upload is not valid UTF-8 text"); }
621
702
  },
622
703
  bytes: async () => __velarServeBytesType.parse(current()),
623
- 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
+ },
624
709
  }]);
625
710
  }
626
711
 
@@ -929,12 +1014,13 @@ function __velarServeRoutePath(path, name = "Route path") {
929
1014
  return path;
930
1015
  }
931
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};
932
1022
  function __velarServeRouteShape(path) {
933
- const segments = __velarServeCall(__velarServeStringSplit, path, ["/"]);
934
- for (let index = 1; index < segments.length; index += 1) {
935
- if (__velarServeCall(__velarServeStringStartsWith, segments[index], ["{"]) && __velarServeCall(__velarServeStringEndsWith, segments[index], ["}"])) segments[index] = "{}";
936
- }
937
- return __velarServeCall(__velarServeArrayJoin, segments, ["/"]);
1023
+ return __velarServeRouteShapeFromSegments(__velarServeCall(__velarServeStringSplit, path, ["/"]));
938
1024
  }
939
1025
 
940
1026
  function __velarCreateServeRoute(method, path, parameters, handler, metadata = {}) {
@@ -1158,23 +1244,33 @@ function __velarCreateServeApp(name, items) {
1158
1244
  const routes = [];
1159
1245
  const lifecycles = [];
1160
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.
1161
1251
  const shapes = new __velarServeMap();
1162
- 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) => {
1163
1255
  if (routes.length >= __velarServeMaxRoutes) throw new __velarServeRangeError("ServeApp cannot contain more than 4096 routes after composition");
1164
1256
  const key = route.method + " " + __velarServeRouteShape(route.path);
1165
- if (__velarServeCall(__velarServeMapHas, shapes, [key])) throw new __velarServeTypeError("ServeApp contains conflicting route '" + key + "'");
1166
- __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}]);
1167
1263
  routes[routes.length] = route;
1168
1264
  };
1169
1265
  for (let index = 0; index < items.length; index += 1) {
1170
1266
  const item = items[index];
1171
- if (__velarServeIsRoute(item)) append(item);
1267
+ if (__velarServeIsRoute(item)) append(item, null);
1172
1268
  else if (__velarServeIsNotFound(item)) {
1173
1269
  if (notFound !== null) throw new __velarServeTypeError("ServeApp contains more than one @notFound fallback");
1174
1270
  notFound = item;
1175
1271
  }
1176
1272
  else if (__velarServeIsApp(item)) {
1177
- 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);
1178
1274
  for (let hook = 0; hook < item.lifecycles.length; hook += 1) {
1179
1275
  if (lifecycles.length >= __velarServeMaxLifecycles) throw new __velarServeRangeError("ServeApp cannot contain more than 4096 lifecycle pairs after composition");
1180
1276
  lifecycles[lifecycles.length] = item.lifecycles[hook];
@@ -1336,6 +1432,7 @@ function __velarServeCors(origins = ["*"], methods = ["GET", "POST", "PUT", "PAT
1336
1432
  methods = __velarServeStringList(methods, "middleware.cors methods");
1337
1433
  headers = __velarServeStringList(headers, "middleware.cors headers");
1338
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");
1339
1436
  return async (request, next) => {
1340
1437
  const origin = __velarServeCall(__velarServeMapHas, request.headers, ["origin"]) ? __velarServeCall(__velarServeMapGet, request.headers, ["origin"]) : null;
1341
1438
  const wildcard = __velarServeCall(__velarServeArrayIncludes, origins, ["*"]);
@@ -1466,18 +1563,26 @@ function __velarServeTimeout(milliseconds) {
1466
1563
  if (__velarServeActiveTimeouts >= __velarServeMaxActiveTimeouts) {
1467
1564
  return {status: 503, json: {error: "server_busy"}, headers: new __velarServeMap([["retry-after", "1"]])};
1468
1565
  }
1469
- __velarServeActiveTimeouts += 1;
1470
1566
  let timer = null;
1471
- let detached = false;
1472
- let pending;
1473
- try { pending = next(); }
1474
- catch (error) { __velarServeActiveTimeouts -= 1; throw error; }
1567
+ const pending = next();
1475
1568
  const expired = new __velarServePromise(resolve => { timer = __velarServeCall(__velarServeSetTimeout, globalThis, [() => resolve(__velarServeMissing), milliseconds]); });
1476
1569
  try {
1477
1570
  const result = await __velarServeCall(__velarServePromiseRace, __velarServePromise, [__velarServeCall(__velarServeObjectFreeze, __velarServeObject, [[pending, expired]])]);
1478
1571
  if (result !== __velarServeMissing) return result;
1479
- detached = true;
1480
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;
1481
1586
  __velarServeActiveBackgroundTasks += 1;
1482
1587
  const settlement = (async () => {
1483
1588
  try { await pending; }
@@ -1500,7 +1605,6 @@ function __velarServeTimeout(milliseconds) {
1500
1605
  };
1501
1606
  } finally {
1502
1607
  if (timer !== null) __velarServeCall(__velarServeClearTimeout, globalThis, [timer]);
1503
- if (!detached) __velarServeActiveTimeouts -= 1;
1504
1608
  }
1505
1609
  };
1506
1610
  }
@@ -1604,15 +1708,19 @@ function __velarServeDecodeScalar(raw, parameter) {
1604
1708
  function __velarServeCookieValue(request, name) {
1605
1709
  if (!__velarServeCall(__velarServeMapHas, request.headers, ["cookie"])) return __velarServeMissing;
1606
1710
  const pieces = __velarServeCall(__velarServeStringSplit, __velarServeCall(__velarServeMapGet, request.headers, ["cookie"]), [";"]);
1711
+ let encoded = null;
1712
+ let matches = 0;
1607
1713
  for (let index = 0; index < pieces.length; index += 1) {
1608
1714
  const piece = __velarServeCall(__velarServeStringTrim, pieces[index], []);
1609
1715
  const separator = __velarServeCall(__velarServeStringIndexOf, piece, ["="]);
1610
1716
  if (separator < 0 || __velarServeCall(__velarServeStringSlice, piece, [0, separator]) !== name) continue;
1611
- const encoded = __velarServeCall(__velarServeStringSlice, piece, [separator + 1]);
1612
- try { return __velarServeCall(__velarServeDecodeURIComponent, undefined, [encoded]); }
1613
- 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]);
1614
1720
  }
1615
- 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}); }
1616
1724
  }
1617
1725
 
1618
1726
  function __velarServeNamedInputRaw(descriptor, parameterName, request) {
@@ -1813,6 +1921,17 @@ function __velarServeMultipartHeaders(text) {
1813
1921
  return output;
1814
1922
  }
1815
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
+
1816
1935
  function __velarServeMultipart(data, boundary) {
1817
1936
  const opening = __velarServeBytePattern("--" + boundary);
1818
1937
  const separator = __velarServeBytePattern("\r\n\r\n");
@@ -1844,8 +1963,9 @@ function __velarServeMultipart(data, boundary) {
1844
1963
  __velarServeAddFormField(fields, name, __velarServeDecodeBytes(part));
1845
1964
  } else {
1846
1965
  if (filename.length > 1024 || __velarServeCall(__velarServeMapHas, files, [name])) throw new HttpError(400, {error: "invalid_multipart"});
1966
+ const base = __velarServeUploadBasename(filename);
1847
1967
  const contentType = typeof headers["content-type"] === "string" ? headers["content-type"] : "application/octet-stream";
1848
- __velarServeCall(__velarServeMapSet, files, [name, __velarServeUploadValue(name, filename, contentType, part, uploadStates)]);
1968
+ __velarServeCall(__velarServeMapSet, files, [name, __velarServeUploadValue(name, base, contentType, part, uploadStates)]);
1849
1969
  }
1850
1970
  parts += 1;
1851
1971
  if (parts > 128) throw new HttpError(413, {error: "too_many_form_parts"});
@@ -2024,14 +2144,22 @@ function __velarServeJsonContentType(headers) {
2024
2144
  || __velarServeCall(__velarServeStringStartsWith, value, ["application/"]) && __velarServeCall(__velarServeStringEndsWith, value, ["+json"]);
2025
2145
  }
2026
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
+
2027
2153
  function __velarServeAutomaticResponse(value) {
2028
2154
  if (__velarServeIsFileResponse(value)) return value;
2155
+ if (__velarServeIsResponseAttempt(value)) return __velarServeResponse(value);
2029
2156
  try { return __velarServeResponse(value); }
2030
2157
  catch { return __velarServeResponse({status: 200, json: value}); }
2031
2158
  }
2032
2159
 
2033
2160
  function __velarServeNotFoundResponse(value) {
2034
2161
  if (__velarServeIsFileResponse(value)) return value;
2162
+ if (__velarServeIsResponseAttempt(value)) return __velarServeResponse(value);
2035
2163
  try { return __velarServeResponse(value); }
2036
2164
  catch { return __velarServeResponse({status: 404, json: value}); }
2037
2165
  }
@@ -2081,6 +2209,7 @@ async function __velarServeHandleAppResponse(app, request, maxBodyBytes, context
2081
2209
  return await __velarServeApplyMiddleware(selected.route, request, invokeRoute);
2082
2210
  } catch (error) {
2083
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"}};
2084
2213
  throw error;
2085
2214
  }
2086
2215
  }
@@ -2612,6 +2741,16 @@ function __velarServeRequest(value) {
2612
2741
  }])};
2613
2742
  }
2614
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
+
2615
2754
  async function __velarServeWriteResponse(handle, value) {
2616
2755
  let cleanup = null;
2617
2756
  let backgroundTasks = null;
@@ -2681,7 +2820,8 @@ function __velarServeNativeHeaders(request) {
2681
2820
  return output;
2682
2821
  }
2683
2822
 
2684
- function __velarServeNativeRequest(request) {
2823
+ function __velarServeNativeRequest(request, maximum = __velarServeMaxBodyBytes) {
2824
+ maximum = __velarServeBodyLimit(maximum);
2685
2825
  const method = request.method ?? "GET";
2686
2826
  if (typeof method !== "string" || !__velarServeCall(__velarServeRegExpTest, __velarServeMethodPattern, [method])) throw new __velarServeTypeError("Native HTTP method is invalid");
2687
2827
  const target = request.url ?? "/";
@@ -2698,7 +2838,7 @@ function __velarServeNativeRequest(request) {
2698
2838
  try {
2699
2839
  for await (const chunk of request) {
2700
2840
  const data = chunk instanceof __velarServeUint8Array ? chunk : __velarServeCall(__velarServeTextEncode, __velarServeUtf8Encoder, [__velarServeString(chunk)]);
2701
- if (total + data.byteLength > __velarServeMaxBodyBytes) { request.resume(); throw new RequestBodyTooLargeError(__velarServeMaxBodyBytes); }
2841
+ if (total + data.byteLength > maximum) { request.resume(); throw new RequestBodyTooLargeError(maximum); }
2702
2842
  __velarServeReserveOutbound(data.byteLength);
2703
2843
  total += data.byteLength;
2704
2844
  reservedBodyBytes += data.byteLength;
@@ -2721,20 +2861,21 @@ function __velarServeNativeRequest(request) {
2721
2861
  })();
2722
2862
  return await bodyPromise;
2723
2863
  };
2724
- const bytes = async (maxBytes = __velarServeMaxBodyBytes) => {
2864
+ const bytes = async (maxBytes = maximum) => {
2725
2865
  if (!__velarServeIsSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > __velarServeMaxBodyBytes) throw new __velarServeRangeError("Request body maxBytes must be an integer from 1 through 16777216");
2726
2866
  const data = await rawBody();
2727
- if (data.byteLength > maxBytes) throw new RequestBodyTooLargeError(maxBytes);
2867
+ const effective = maxBytes > maximum ? maximum : maxBytes;
2868
+ if (data.byteLength > effective) throw new RequestBodyTooLargeError(effective);
2728
2869
  return data;
2729
2870
  };
2730
- const body = async (maxBytes = __velarServeMaxBodyBytes) => {
2871
+ const body = async (maxBytes = maximum) => {
2731
2872
  const data = await bytes(maxBytes);
2732
2873
  try { return __velarServeCall(__velarServeTextDecode, __velarServeUtf8Decoder, [data]); }
2733
2874
  catch { throw new __velarServeTypeError("Request body must be valid UTF-8 text"); }
2734
2875
  };
2735
- 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");
2736
2877
  return {
2737
- 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)); }}]),
2738
2879
  cancellation,
2739
2880
  cleanup() { if (reservedBodyBytes > 0) { __velarServeReleaseOutbound(reservedBodyBytes); reservedBodyBytes = 0; } return null; },
2740
2881
  };
@@ -2749,14 +2890,49 @@ function __velarServeNativeSetHeaders(response, headers, cookies = []) {
2749
2890
  for (let index = 0; index < cookies.length; index += 1) allCookies[allCookies.length] = cookies[index];
2750
2891
  if (allCookies.length > 0) response.setHeader("Set-Cookie", allCookies);
2751
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
+ }
2752
2918
  async function __velarServeNativeFile(value, operations) {
2753
- 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; }
2754
2925
  const load = async path => {
2755
- 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; }
2756
2929
  const relative = operations.relative(root, target);
2757
- if (relative.startsWith("..") || operations.isAbsolute(relative)) throw new __velarServeTypeError("fileResponse path escapes its root");
2758
- const info = await operations.stat(target);
2759
- 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");
2760
2936
  return {target, info};
2761
2937
  };
2762
2938
  try { return await load(value.path); } catch (error) { if (value.fallback === null) throw error; return load(value.fallback); }
@@ -2826,13 +3002,13 @@ async function __velarServeNativeBody(response, value, checked, suppressBody, op
2826
3002
  if (!response.hasHeader("vary")) response.setHeader("vary", "Accept-Encoding");
2827
3003
  return __velarServeWithOutbound(compressed.byteLength, () => __velarServeNativeEnd(response, compressed));
2828
3004
  }
2829
- async function __velarServeHandleNative(handler, request, response, operations) {
3005
+ async function __velarServeHandleNative(handler, request, response, operations, maxBodyBytes = __velarServeMaxBodyBytes) {
2830
3006
  let cleanup = null;
2831
3007
  let backgroundTasks = null;
2832
3008
  let incoming = null;
2833
3009
  let disconnected = null;
2834
3010
  try {
2835
- incoming = __velarServeNativeRequest(request);
3011
+ incoming = __velarServeNativeRequest(request, maxBodyBytes);
2836
3012
  disconnected = () => { if (!response.writableFinished) __velarServeCancellation.__velarCancel(incoming.cancellation, "client_disconnect"); };
2837
3013
  request.once("aborted", disconnected);
2838
3014
  response.once("close", disconnected);
@@ -2847,7 +3023,34 @@ async function __velarServeHandleNative(handler, request, response, operations)
2847
3023
  let writing = false;
2848
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; } };
2849
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;
2850
- } 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
+ }
2851
3054
  finally {
2852
3055
  if (disconnected !== null) { request.off("aborted", disconnected); response.off("close", disconnected); }
2853
3056
  await __velarServeRunBackground(backgroundTasks);
@@ -3129,6 +3332,7 @@ async function __velarServeDispatch(event) {
3129
3332
  if (typeof handler !== "function") { __velarServeReportFailure(new __velarServeError("Node host requested an unknown server token")); await __velarNodeHostInvoke("serve.fail", [value.handle]); return; }
3130
3333
  try { await __velarServeWriteResponse(value.handle, await handler(value.request)); }
3131
3334
  catch (error) {
3335
+ if (error instanceof __velarServeOutboundBudgetError && await __velarServeShedOutbound(value.handle)) return;
3132
3336
  __velarServeReportFailure(error);
3133
3337
  try { await __velarNodeHostInvoke("serve.fail", [value.handle]); }
3134
3338
  catch {}
@@ -1 +1 @@
1
- {"version":3,"file":"serve-runtime.js","sourceRoot":"","sources":["../src/serve-runtime.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0qGjD,CAAC,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"serve-runtime.js","sourceRoot":"","sources":["../src/serve-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,MAAM,kBAAkB,CAAC;AAEpE,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAw/BL,gCAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA63E5E,CAAC,SAAS,EAAE,CAAC"}
@@ -18,6 +18,16 @@ export declare function parseRouteResultHint(value: string | undefined): {
18
18
  export declare class VelarNodeAnalyzer extends Analyzer {
19
19
  private readonly contextualRouteParameters;
20
20
  private readonly routeInputs;
21
+ /** Servers declared by the module under analysis, the only spread targets this analyzer can resolve. */
22
+ private readonly moduleServers;
23
+ /** Module-level `const alias = name` and `let alias = name` bindings, so a spread of an alias resolves to the server it names. */
24
+ private readonly moduleServerAliases;
25
+ /** Local names imported from velar/serve that name a path-preserving combinator, so a spread of a call resolves through it. */
26
+ private readonly moduleServeCombinators;
27
+ /** One answer per `let` alias name to "was this binding ever reassigned?", because the predicate walks the whole program. */
28
+ private readonly stableAliases;
29
+ /** The program under analysis, held for the alias-stability walk. */
30
+ private moduleProgram;
21
31
  private readonly nodeModulePath;
22
32
  constructor(context?: AnalysisContext, extensions?: readonly CompilerAnalysisExtension[]);
23
33
  analyze(program: Program): readonly import("@velarscript/compiler").Diagnostic[];
@@ -27,6 +37,47 @@ export declare class VelarNodeAnalyzer extends Analyzer {
27
37
  readonly kind: string;
28
38
  }, parameter: Parameter): ValueType | null;
29
39
  private analyzeServer;
40
+ /**
41
+ * Enters one route into this server's shape map and compares it against every route already
42
+ * entered, whether that route was written here or composed in by a spread. Composition is why the
43
+ * entries carry an origin: a conflicting route the author cannot see in his own file has to name
44
+ * the server it came from.
45
+ */
46
+ private recordRoute;
47
+ /**
48
+ * The routes and fallback a spread composes into the server that writes it, or null when the
49
+ * spread is not statically resolvable. This analyzer sees one module, so a spread contributes
50
+ * only when its value reaches a server declared in this module: a plain identifier, an alias of
51
+ * one, or a velar/serve combinator call around one — `use`, `bodyLimit`, `docs` and `lifecycle`
52
+ * carry paths through unchanged, and `prefix` translates them by its literal path. An imported
53
+ * server, a computed prefix path, or any other expression is let through unchecked, because a
54
+ * false conflict here would block a correct program; D90 R19's runtime referee judges the final
55
+ * table at assembly instead. Composition is followed transitively; the visited set bounds a
56
+ * cycle and starts holding the composing server, so a cycle never folds a server's own routes
57
+ * back into itself and reports each as conflicting with itself.
58
+ */
59
+ private composedItems;
60
+ /**
61
+ * The server declaration a spread value names, or null when it is anything else. A
62
+ * `const other = base` alias chain of this module's own servers resolves too, because the alias
63
+ * holds exactly that ServeApp — and so does a `let` alias the whole module never reassigns,
64
+ * because an unwritten `let` holds its initializer exactly as a `const` does. A reassigned or
65
+ * ambiguous `let`, a member path, a conditional, an import, or a parameter contributes nothing.
66
+ * A call resolves through the path-preserving velar/serve combinators when the callee still
67
+ * reaches its velar/serve import: `prefix` with a literal path translates what its app argument
68
+ * declares, and `use`/`bodyLimit`/`docs`/`lifecycle` pass it through untouched. A computed
69
+ * prefix path contributes nothing — the assembly-time referee owns it. An alias's initializer
70
+ * re-enters this resolver whole, so `const scoped = prefix("/api", routes)` resolves exactly as
71
+ * the spelled-out spread does; the followed set bounds an alias cycle.
72
+ */
73
+ private resolveComposedServer;
74
+ /** Whether a `let` alias has never been reassigned, asked once per name and program. */
75
+ private aliasBindingIsStable;
76
+ /**
77
+ * Whether a name still reaches the declaration this module recorded for it. An import, a
78
+ * shadowing binding, or a parameter of the same name reaches a different binding.
79
+ */
80
+ private resolvesTo;
30
81
  private analyzeNotFound;
31
82
  private analyzeRoute;
32
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-analyzer.d.ts","sourceRoot":"","sources":["../src/server-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EASR,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,iCAAiC,CAAC;AAyBzC,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAC;AAC/I,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAC;AAC5I,KAAK,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,UAAQ,GAAG,MAAM,CAE5I;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG;IAClE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B,GAAG,IAAI,CAcP;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAErH;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,GAAG,IAAI,CAWjL;AAED,qBAAa,iBAAkB,SAAQ,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAgC;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAE/C,YAAY,OAAO,GAAE,eAAoB,EAAE,UAAU,GAAE,SAAS,yBAAyB,EAAO,EAG/F;IAEQ,OAAO,CAAC,OAAO,EAAE,OAAO,yDAShC;IAED,UAAmB,4BAA4B,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAI7E;IAED,UAAmB,yBAAyB,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAI1E;IAED,UAAmB,kCAAkC,CACnD,SAAS,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EACpC,SAAS,EAAE,SAAS,GACnB,SAAS,GAAG,IAAI,CAYlB;IAED,OAAO,CAAC,aAAa;IAgCrB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,YAAY;CAqGrB;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gCAAgC,GAAG,SAAS,GAAG,SAAS,CA0KnG"}
1
+ {"version":3,"file":"server-analyzer.d.ts","sourceRoot":"","sources":["../src/server-analyzer.ts"],"names":[],"mappings":"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"}