@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
@@ -144,6 +144,12 @@ function urlOf(value) {
144
144
  if (urlUsername(url) || urlPassword(url)) throw new NativeTypeError("HTTP URL credentials are not allowed; use an Authorization header");
145
145
  return urlHref(url);
146
146
  }
147
+ // The transport owns message framing and routing, so an application header map
148
+ // may never restate them: a caller-supplied content-length, transfer-encoding
149
+ // or host lands on the wire beside the host's own framing and is a
150
+ // request-smuggling primitive. Cookie and proxy credentials are ordinary
151
+ // application headers here; they are forbidden only as secretHeader names.
152
+ const transportOwnedHttpHeaders = setOf(["connection", "content-length", "expect", "host", "keep-alive", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"]);
147
153
  function headersOf(value) {
148
154
  if (value == null) { requireHttpHost(); return new NativeMap(); }
149
155
  let size;
@@ -156,6 +162,9 @@ function headersOf(value) {
156
162
  if (typeof name !== "string" || typeof item !== "string" || !patternMatches(headerNamePattern, name) || patternMatches(lineBreakPattern, item)) {
157
163
  throw new NativeTypeError("HTTP headers must use valid string names and single-line values");
158
164
  }
165
+ if (call(nativeSetHas, transportOwnedHttpHeaders, [stringLower(name)])) {
166
+ throw new NativeTypeError("HTTP header '" + name + "' is transport-controlled");
167
+ }
159
168
  units += name.length + item.length;
160
169
  if (units > 65536) throw new NativeRangeError("HTTP headers cannot exceed 64 KiB");
161
170
  nativeReflectApply(nativeMapSet, output, [name, item]);
@@ -390,7 +399,7 @@ export class HttpTransportError extends NativeError {
390
399
  this.phase = phase;
391
400
  }
392
401
  }
393
- export class HttpError extends NativeError {
402
+ export class HttpResponseError extends NativeError {
394
403
  constructor(message, status, url, body = null) {
395
404
  if (typeof message !== "string") throw new NativeTypeError("HTTP error message must be text");
396
405
  if (message.length > 65536) throw new NativeRangeError("HTTP error messages cannot exceed 64 KiB");
@@ -398,7 +407,7 @@ export class HttpError extends NativeError {
398
407
  if (typeof url !== "string") throw new NativeTypeError("HTTP error URL must be text");
399
408
  if (url.length > 2 * 1024 * 1024) throw new NativeRangeError("HTTP error URLs cannot exceed 2 MiB");
400
409
  super(message);
401
- this.name = "HttpError";
410
+ this.name = "HttpResponseError";
402
411
  this.status = status;
403
412
  this.url = url;
404
413
  this.body = body;
@@ -409,7 +418,6 @@ class HttpResponse {
409
418
  constructor(response, request) {
410
419
  this.request = request;
411
420
  this.body = response.body;
412
- this.ok = response.ok;
413
421
  this.status = response.status;
414
422
  this.statusText = response.statusText;
415
423
  this.url = response.url;
@@ -519,7 +527,7 @@ class HttpResponse {
519
527
  const text = await this.text();
520
528
  return parseJsonText(text);
521
529
  }
522
- async parse(Type) { Type = runtimeHttpType(Type); return Type.parse(await this.json()); }
530
+ async parse(Type) { Type = runtimeHttpType(Type); return __velarJsonParseTyped(Type, await this.text(), "HTTP JSON text"); }
523
531
  }
524
532
 
525
533
  class Request {
@@ -579,12 +587,16 @@ class Request {
579
587
  const response = hostResponse(wire, this.handle);
580
588
  if (this.abortError) throw this.abortError;
581
589
  const wrapped = new HttpResponse(response, this);
582
- if (!wrapped.ok) {
590
+ // D90 R20: the 2xx question is asked here and nowhere else. The
591
+ // transport snapshot still carries ok; the response an author holds
592
+ // does not, because by the time it is returned the answer is always
593
+ // yes.
594
+ if (!response.ok) {
583
595
  const text = await wrapped.text();
584
596
  let body = text;
585
597
  try { body = text ? parseJsonText(text) : null; } catch {}
586
598
  const errorUrl = wrapped.url || this.url;
587
- throw new HttpError("HTTP " + wrapped.status + " for " + errorUrl, wrapped.status, errorUrl, body);
599
+ throw new HttpResponseError("HTTP " + wrapped.status + " for " + errorUrl, wrapped.status, errorUrl, body);
588
600
  }
589
601
  return wrapped;
590
602
  } catch (error) {
@@ -599,7 +611,7 @@ class Request {
599
611
  async bytes() { return (await this.response()).bytes(); }
600
612
  async json() { return (await this.response()).json(); }
601
613
  async streamText(consumer) { return (await this.response()).streamText(consumer); }
602
- async parse(Type) { Type = runtimeHttpType(Type); return Type.parse(await this.json()); }
614
+ async parse(Type) { Type = runtimeHttpType(Type); return (await this.response()).parse(Type); }
603
615
  cancel() { this.abort("cancelled"); return null; }
604
616
  }
605
617
 
@@ -1 +1 @@
1
- {"version":3,"file":"http-runtime.js","sourceRoot":"","sources":["../src/http-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAE7H,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;EAI/C,yBAAyB;EACzB,2BAA2B;EAC3B,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2lBnB,CAAC,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"http-runtime.js","sourceRoot":"","sources":["../src/http-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAE7H,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;EAI/C,yBAAyB;EACzB,2BAA2B;EAC3B,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAumBnB,CAAC,SAAS,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"node-host-runtime.d.ts","sourceRoot":"","sources":["../src/node-host-runtime.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,uBAAuB,QAqUvB,CAAC"}
1
+ {"version":3,"file":"node-host-runtime.d.ts","sourceRoot":"","sources":["../src/node-host-runtime.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,uBAAuB,QA4UvB,CAAC"}
@@ -96,16 +96,22 @@ export class __velarNodeHostHttpTransportError extends __velarNodeHostError {
96
96
  const __velarNodeHostPathErrorClasses = __velarNodeHostObjectCreate(null);
97
97
  ${VELAR_HOST_ERROR_PATH_NAMES.map((name) => `__velarNodeHostPathErrorClasses[${JSON.stringify(name)}] = __Velar${name};`).join("\n")}
98
98
 
99
+ // This function must never throw. Its caller runs inside the port handler,
100
+ // whose catch latches the permanent host failure, closes the port and
101
+ // terminates the worker, so one error record the proxy failed to enumerate
102
+ // would brick velar/fs, velar/http and velar/serve for the whole process. An
103
+ // unrecognised record rejects only its own request instead.
99
104
  function __velarNodeHostErrorOf(value, operation) {
100
- value = __velarNodeHostRecord(value, "Node host error");
105
+ try { value = __velarNodeHostRecord(value, "Node host error"); }
106
+ catch { return new __velarNodeHostTypeError("Node host returned an invalid error"); }
101
107
  if (typeof value.message !== "string" || value.message.length === 0 || value.message.length > 65536) {
102
- throw new __velarNodeHostTypeError("Node host returned an invalid error");
108
+ return new __velarNodeHostTypeError("Node host returned an invalid error");
103
109
  }
104
110
  if (value.name === "HttpTransportError") {
105
- if ((operation !== "http.request" && operation !== "http.read")
111
+ if ((operation !== "http.request" && operation !== "http.read" && operation !== "http.readBytes")
106
112
  || (value.phase !== "request" && value.phase !== "response")
107
113
  || (operation === "http.request" ? value.phase !== "request" : value.phase !== "response")) {
108
- throw new __velarNodeHostTypeError("Node host returned an invalid HTTP transport error");
114
+ return new __velarNodeHostTypeError("Node host returned an invalid HTTP transport error");
109
115
  }
110
116
  return new __velarNodeHostHttpTransportError(value.message, value.phase);
111
117
  }
@@ -115,7 +121,7 @@ function __velarNodeHostErrorOf(value, operation) {
115
121
  const pathed = __velarNodeHostPathErrorClasses[value.name];
116
122
  if (pathed) {
117
123
  if (typeof value.path !== "string" || value.path.length > 65536) {
118
- throw new __velarNodeHostTypeError("Node host returned an invalid error path");
124
+ return new __velarNodeHostTypeError("Node host returned an invalid error path");
119
125
  }
120
126
  return new pathed(value.message, value.path.length > 0 ? value.path : null);
121
127
  }
@@ -123,7 +129,7 @@ function __velarNodeHostErrorOf(value, operation) {
123
129
  if (value.name === "RangeError") return new __velarNodeHostRangeError(value.message);
124
130
  if (value.name === "TypeError") return new __velarNodeHostTypeError(value.message);
125
131
  if (value.name === "Error") return new __velarNodeHostError(value.message);
126
- throw new __velarNodeHostTypeError("Node host returned an invalid error");
132
+ return new __velarNodeHostTypeError("Node host returned an invalid error");
127
133
  }
128
134
 
129
135
  function __velarNodeHostUpdateReference() {
@@ -300,7 +306,8 @@ export function __velarNodeHostInvoke(operation, args) {
300
306
  if (__velarNodeHostFailure) return new __velarNodeHostPromise((_resolve, reject) => reject(__velarNodeHostFailure));
301
307
  const id = __velarNodeHostRequestId();
302
308
  return new __velarNodeHostPromise((resolve, reject) => {
303
- const handle = operation === "http.request" || operation === "http.read" || operation === "http.cancel" || operation === "http.close"
309
+ const handle = operation === "http.request" || operation === "http.read" || operation === "http.readBytes"
310
+ || operation === "http.cancel" || operation === "http.close"
304
311
  || operation === "fs.watchNext" || operation === "fs.watchClose"
305
312
  ? args[0]
306
313
  : null;
@@ -1 +1 @@
1
- {"version":3,"file":"node-host-runtime.js","sourceRoot":"","sources":["../src/node-host-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAExI,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAA;WACtC,sBAAsB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,cAAc,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,gCAAgC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4FhJ,2BAA2B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwOnI,CAAC,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"node-host-runtime.js","sourceRoot":"","sources":["../src/node-host-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAExI,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAA;WACtC,sBAAsB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,cAAc,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,gCAAgC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4FhJ,2BAA2B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+OnI,CAAC,SAAS,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"node-host-worker-runtime.d.ts","sourceRoot":"","sources":["../src/node-host-worker-runtime.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,6BAA6B,QAqgD7B,CAAC"}
1
+ {"version":3,"file":"node-host-worker-runtime.d.ts","sourceRoot":"","sources":["../src/node-host-worker-runtime.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,6BAA6B,QAiiD7B,CAAC"}
@@ -7,7 +7,7 @@ import { createReadStream, watch as watchNode } from "node:fs";
7
7
  import { appendFile, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { createServer, request as createHttpRequest } from "node:http";
9
9
  import { request as createHttpsRequest } from "node:https";
10
- import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
10
+ import { basename, dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
11
11
  import { URL as NodeURL } from "node:url";
12
12
  import { brotliCompress as brotliCompressNode, gzip as gzipNode } from "node:zlib";
13
13
  import { promisify } from "node:util";
@@ -87,10 +87,18 @@ function advanceHandle(handle) {
87
87
  return handle >= Number.MAX_SAFE_INTEGER ? 1 : handle + 1;
88
88
  }
89
89
 
90
+ // Exhausting the aggregate budget is a temporary load condition, not a server
91
+ // fault: admission already answers it with 503 at rejectIncomingRequest, and a
92
+ // response that cannot be reserved now gets the same answer instead of the
93
+ // opaque 500 that every other late failure gets. The identity is a class rather
94
+ // than the message so the send path can tell it apart from a genuine fault.
95
+ class ServeBudgetError extends RangeError {
96
+ constructor() { super("Node serve aggregate byte budget is exhausted"); }
97
+ }
98
+
90
99
  function reserveServeBytes(task, bytes) {
91
- if (!Number.isSafeInteger(bytes) || bytes < 0 || reservedServeBytes + bytes > maxServeAggregateBytes) {
92
- throw new RangeError("Node serve aggregate byte budget is exhausted");
93
- }
100
+ if (!Number.isSafeInteger(bytes) || bytes < 0) throw new RangeError("Node serve byte reservation must be a non-negative integer");
101
+ if (reservedServeBytes + bytes > maxServeAggregateBytes) throw new ServeBudgetError();
94
102
  reservedServeBytes += bytes;
95
103
  task.reservedBytes += bytes;
96
104
  }
@@ -104,9 +112,8 @@ function releaseServeBytes(task, bytes = task.reservedBytes) {
104
112
  }
105
113
 
106
114
  function reserveTransientServeBytes(bytes) {
107
- if (!Number.isSafeInteger(bytes) || bytes < 0 || reservedServeBytes + bytes > maxServeAggregateBytes) {
108
- throw new RangeError("Node serve aggregate byte budget is exhausted");
109
- }
115
+ if (!Number.isSafeInteger(bytes) || bytes < 0) throw new RangeError("Node serve byte reservation must be a non-negative integer");
116
+ if (reservedServeBytes + bytes > maxServeAggregateBytes) throw new ServeBudgetError();
110
117
  reservedServeBytes += bytes;
111
118
  }
112
119
 
@@ -425,6 +432,15 @@ const forbiddenHttpSecretHeaders = new Set([
425
432
  "connection", "content-length", "cookie", "cookie2", "host", "proxy-authorization",
426
433
  "te", "trailer", "transfer-encoding", "upgrade",
427
434
  ]);
435
+ // Framing and routing belong to the transport, not to the caller. An
436
+ // application that could set these beside Node's own framing could put two
437
+ // disagreeing lengths, a second encoding, or a forged authority on the wire.
438
+ // Credential names stay legal here: they are the caller's to send, and remain
439
+ // forbidden only for the secret-header path above.
440
+ const transportOwnedHttpHeaders = new Set([
441
+ "connection", "content-length", "expect", "host", "keep-alive", "proxy-connection",
442
+ "te", "trailer", "transfer-encoding", "upgrade",
443
+ ]);
428
444
 
429
445
  function httpMethod(value) {
430
446
  if (typeof value !== "string") throw new TypeError("HTTP method must be text");
@@ -458,6 +474,7 @@ function httpHeaderRecord(value) {
458
474
  || !httpHeaderNamePattern.test(pair[0]) || httpLineBreakPattern.test(pair[1])) {
459
475
  throw new TypeError("HTTP headers must use valid string names and single-line values");
460
476
  }
477
+ if (transportOwnedHttpHeaders.has(pair[0].toLowerCase())) throw new TypeError("HTTP header '" + pair[0] + "' is transport-controlled");
461
478
  units += pair[0].length + pair[1].length;
462
479
  if (units > 65536) throw new RangeError("HTTP headers cannot exceed 64 KiB");
463
480
  headers[pair[0].toLowerCase()] = pair[1];
@@ -732,8 +749,12 @@ function requestPath(value) {
732
749
  }
733
750
 
734
751
  function inside(root, target) {
752
+ // relative() emits ".." only as a whole segment, so the escape test compares
753
+ // whole segments too: a prefix test also rejects an ordinary top-level file
754
+ // whose own name begins with two dots. The separator is the platform's,
755
+ // because relative() writes an escape with a backslash on Windows.
735
756
  const path = relative(root, target);
736
- return path === "" || !path.startsWith("..") && !isAbsolute(path);
757
+ return path === "" || path !== ".." && !path.startsWith(".." + sep) && !isAbsolute(path);
737
758
  }
738
759
 
739
760
  async function staticFile(rootValue, pathValue, fallbackValue) {
@@ -864,6 +885,15 @@ function responseHasNoBody(task, status) {
864
885
  return task.request.method === "HEAD" || status >= 100 && status < 200 || status === 204 || status === 304;
865
886
  }
866
887
 
888
+ async function shedServeResponse(task) {
889
+ task.response.statusCode = 503;
890
+ task.response.setHeader("Retry-After", "1");
891
+ task.response.setHeader("Content-Type", "application/json; charset=utf-8");
892
+ await endServeResponse(task, responseHasNoBody(task, 503) ? undefined : '{"error":"outbound_budget_exhausted"}');
893
+ completeRequest(task);
894
+ return null;
895
+ }
896
+
867
897
  async function endServeResponse(task, value) {
868
898
  await new Promise((resolveEnd, rejectEnd) => {
869
899
  let settled = false;
@@ -893,27 +923,19 @@ function rawBodyOf(task, maximum) {
893
923
  return {data: null, bytes: maximum, tooLarge: true};
894
924
  }
895
925
  }
896
- if (declared !== null) {
897
- const output = Buffer.allocUnsafe(declared);
898
- reserveServeBytes(task, declared);
899
- let offset = 0;
900
- try {
901
- for await (const chunk of task.request) {
902
- const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
903
- if (offset + data.byteLength > declared) { task.request.resume(); releaseServeBytes(task, declared); return {data: null, bytes: maximum, tooLarge: true}; }
904
- data.copy(output, offset);
905
- offset += data.byteLength;
906
- }
907
- if (offset !== declared) throw new TypeError("Request body length does not match Content-Length");
908
- return {data: output, bytes: declared, tooLarge: false};
909
- } catch (error) { if (task.reservedBytes >= declared) releaseServeBytes(task, declared); throw error; }
910
- }
926
+ // A declared Content-Length is a client claim, not a delivered body, so it
927
+ // buys no allocation and no budget reservation up front: a header-only
928
+ // socket that declares 16 MiB and sends nothing would otherwise spend the
929
+ // process-global aggregate budget for the whole request timeout. The
930
+ // declaration is only the effective ceiling; the bytes that actually arrive
931
+ // are charged as they arrive.
932
+ const limit = declared === null ? maximum : declared;
911
933
  const chunks = [];
912
934
  let total = 0;
913
935
  try {
914
936
  for await (const chunk of task.request) {
915
937
  const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
916
- if (total + data.byteLength > maximum) {
938
+ if (total + data.byteLength > limit) {
917
939
  task.request.resume();
918
940
  releaseServeBytes(task, total);
919
941
  return {data: null, bytes: maximum, tooLarge: true};
@@ -922,6 +944,7 @@ function rawBodyOf(task, maximum) {
922
944
  total += data.byteLength;
923
945
  chunks.push(data);
924
946
  }
947
+ if (declared !== null && total !== declared) throw new TypeError("Request body length does not match Content-Length");
925
948
  reserveTransientServeBytes(total);
926
949
  try { return {data: Buffer.concat(chunks, total), bytes: total, tooLarge: false}; }
927
950
  finally { releaseTransientServeBytes(total); }
@@ -1354,14 +1377,19 @@ async function dispatch(operation, args) {
1354
1377
  if (compression !== null && compression !== "gzip" && compression !== "br") throw new TypeError("ServeResponse compression is invalid");
1355
1378
  const suppressBody = responseHasNoBody(task, status);
1356
1379
  let output = body;
1357
- if (!suppressBody && compression !== null) {
1358
- const inputBytes = Buffer.byteLength(body, "utf8");
1359
- reserveTransientServeBytes(inputBytes * 2);
1360
- try { output = await (compression === "br" ? brotliCompress(body) : gzip(body)); }
1361
- finally { releaseTransientServeBytes(inputBytes * 2); }
1362
- if (output.byteLength > maxServeBodyBytes) throw new RangeError("Compressed ServeResponse exceeds 16 MiB");
1380
+ try {
1381
+ if (!suppressBody && compression !== null) {
1382
+ const inputBytes = Buffer.byteLength(body, "utf8");
1383
+ reserveTransientServeBytes(inputBytes * 2);
1384
+ try { output = await (compression === "br" ? brotliCompress(body) : gzip(body)); }
1385
+ finally { releaseTransientServeBytes(inputBytes * 2); }
1386
+ if (output.byteLength > maxServeBodyBytes) throw new RangeError("Compressed ServeResponse exceeds 16 MiB");
1387
+ }
1388
+ if (!suppressBody) reserveServeBytes(task, typeof output === "string" ? Buffer.byteLength(output, "utf8") : output.byteLength);
1389
+ } catch (error) {
1390
+ if (!(error instanceof ServeBudgetError)) throw error;
1391
+ return await shedServeResponse(task);
1363
1392
  }
1364
- if (!suppressBody) reserveServeBytes(task, typeof output === "string" ? Buffer.byteLength(output, "utf8") : output.byteLength);
1365
1393
  task.response.statusCode = status;
1366
1394
  setHeaders(task.response, headers, cookies);
1367
1395
  if (!task.response.hasHeader("Content-Type") && (task.request.method === "HEAD" || !suppressBody)) {
@@ -1 +1 @@
1
- {"version":3,"file":"node-host-worker-runtime.js","sourceRoot":"","sources":["../src/node-host-worker-runtime.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,iFAAiF;AACjF,yEAAyE;AACzE,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqgDtD,CAAC,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"node-host-worker-runtime.js","sourceRoot":"","sources":["../src/node-host-worker-runtime.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,iFAAiF;AACjF,yEAAyE;AACzE,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiiDtD,CAAC,SAAS,EAAE,CAAC"}
@@ -1,5 +1,5 @@
1
1
  export interface VelarNodeConfig {
2
- /** Exported ServeApp value in the project entry module. */
2
+ /** Exported ServeApp or checked WebSocket startup function in the project entry module. */
3
3
  readonly app: string;
4
4
  readonly host: string;
5
5
  readonly port: number;
@@ -1 +1 @@
1
- {"version":3,"file":"project-config.d.ts","sourceRoot":"","sources":["../src/project-config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;KAC9B,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB;;;iBAGnB,OAAO,gBAAgB,MAAM,GAAG,eAAe;EAG5D,CAAC"}
1
+ {"version":3,"file":"project-config.d.ts","sourceRoot":"","sources":["../src/project-config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,2FAA2F;IAC3F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;KAC9B,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB;;;iBAGnB,OAAO,gBAAgB,MAAM,GAAG,eAAe;EAG5D,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Folds a route path's pre-split segments into its shape: every `{name:type}`
3
+ * capture collapses to `{}` and every literal segment stays itself, so two
4
+ * paths share a shape exactly when a request cannot tell them apart by
5
+ * position. The body uses only indexed access, `.length` and primitive string
6
+ * concatenation — no method lookups — because the serve runtime embeds this
7
+ * exact source inside its hardened primordial Realm, where prototypes are
8
+ * assumed hostile. Callers split on "/" themselves with whatever split they
9
+ * trust.
10
+ */
11
+ export declare function routeShapeFromSegments(segments: readonly string[]): string;
12
+ /**
13
+ * The same definition as JavaScript source, for the serve runtime template.
14
+ * Deriving it from the compiled function keeps the rule written once: editing
15
+ * `routeShapeFromSegments` edits both referees.
16
+ */
17
+ export declare const ROUTE_SHAPE_FROM_SEGMENTS_SOURCE: string;
18
+ /** The shape of a full route path, for callers outside the hardened Realm. */
19
+ export declare function routeShape(path: string): string;
20
+ //# sourceMappingURL=route-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-shape.d.ts","sourceRoot":"","sources":["../src/route-shape.ts"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAQ1E;AAED;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,EAAE,MAA0C,CAAC;AAE1F,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C"}
@@ -0,0 +1,36 @@
1
+ // D90 R19(c): the shape of a route — its method-independent collision key — is
2
+ // one concept, so it has exactly one definition. The static analyzer imports
3
+ // the function below; the velar/serve runtime interpolates its source into the
4
+ // emitted module. Two referees, one rule: a shape the compiler and the
5
+ // assembly check disagree about is the defect class this file exists to
6
+ // prevent.
7
+ /**
8
+ * Folds a route path's pre-split segments into its shape: every `{name:type}`
9
+ * capture collapses to `{}` and every literal segment stays itself, so two
10
+ * paths share a shape exactly when a request cannot tell them apart by
11
+ * position. The body uses only indexed access, `.length` and primitive string
12
+ * concatenation — no method lookups — because the serve runtime embeds this
13
+ * exact source inside its hardened primordial Realm, where prototypes are
14
+ * assumed hostile. Callers split on "/" themselves with whatever split they
15
+ * trust.
16
+ */
17
+ export function routeShapeFromSegments(segments) {
18
+ let shape = "";
19
+ for (let index = 0; index < segments.length; index += 1) {
20
+ const segment = segments[index];
21
+ const capture = segment !== undefined && segment[0] === "{" && segment[segment.length - 1] === "}";
22
+ shape += (index === 0 ? "" : "/") + (capture ? "{}" : segment);
23
+ }
24
+ return shape;
25
+ }
26
+ /**
27
+ * The same definition as JavaScript source, for the serve runtime template.
28
+ * Deriving it from the compiled function keeps the rule written once: editing
29
+ * `routeShapeFromSegments` edits both referees.
30
+ */
31
+ export const ROUTE_SHAPE_FROM_SEGMENTS_SOURCE = routeShapeFromSegments.toString();
32
+ /** The shape of a full route path, for callers outside the hardened Realm. */
33
+ export function routeShape(path) {
34
+ return routeShapeFromSegments(path.split("/"));
35
+ }
36
+ //# sourceMappingURL=route-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-shape.js","sourceRoot":"","sources":["../src/route-shape.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,uEAAuE;AACvE,wEAAwE;AACxE,WAAW;AAEX;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAA2B;IAChE,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACxD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,OAAO,GAAG,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;QACnG,KAAK,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAW,sBAAsB,CAAC,QAAQ,EAAE,CAAC;AAE1F,8EAA8E;AAC9E,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,sBAAsB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve-runtime.d.ts","sourceRoot":"","sources":["../src/serve-runtime.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,QAknGxB,CAAC"}
1
+ {"version":3,"file":"serve-runtime.d.ts","sourceRoot":"","sources":["../src/serve-runtime.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,wBAAwB,QAq3GxB,CAAC"}