@vesk/adapter 0.2.70 → 0.2.72

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.
@@ -19,9 +19,6 @@ export interface DevHmrState {
19
19
  }
20
20
  /** Default state when no HMR provider is available (no server). */
21
21
  export declare function defaultDevHmrState(): DevHmrState;
22
- export declare function bodyTooLarge(maxBytes: number): Error & {
23
- status: number;
24
- };
25
22
  export interface DevPanelResponse {
26
23
  status: number;
27
24
  headers: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AASjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAM7C,OAAO,KAAK,EAAa,gBAAgB,EAAwB,MAAM,yBAAyB,CAAC;AAgBjG;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAuCD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkBD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B;;8BAE0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,WAAW,CAAC;IAChC,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACvE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAChH,CAUA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrD;AA8CD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAorBvG"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AASjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAO7C,OAAO,KAAK,EAAa,gBAAgB,EAAwB,MAAM,yBAAyB,CAAC;AAgBjG;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAgDD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B;;8BAE0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,WAAW,CAAC;IAChC,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACvE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAChH,CAUA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrD;AAUD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAorBvG"}
@@ -14,6 +14,7 @@ import { resolveRuntimeErrorPayload, renderDevErrorPage } from './ssr-error';
14
14
  import { renderDefaultNotFoundPage, renderDefaultErrorPage } from '@vesk/adapter/src/default-pages';
15
15
  import { buildTreeShakenRuntime, runtimeExportNames } from '@vesk/adapter/src/client-bundle';
16
16
  import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
17
+ import { makeWebRequest, bodyTooLarge } from '@vesk/adapter/src/web-request';
17
18
  import { getPluginRecords } from './plugins';
18
19
  // @vesk/agentic — optional AI plugin; gated by active state if installed
19
20
  import { createAgentRouter } from '@vesk/agentic/src/dev-api';
@@ -58,11 +59,6 @@ async function deliverResponse(res, response) {
58
59
  }
59
60
  res.end(await response.text());
60
61
  }
61
- export function bodyTooLarge(maxBytes) {
62
- const err = new Error(`Request body exceeds limit (${maxBytes} bytes)`);
63
- err.status = 413;
64
- return err;
65
- }
66
62
  function errorStatus(e, fallback) {
67
63
  const s = e?.status ?? e?.statusCode;
68
64
  return typeof s === 'number' && s >= 400 && s < 600 ? s : fallback;
@@ -149,49 +145,6 @@ export function injectDevScripts(html) {
149
145
  const scripts = '\t<script type="module" src="/_vesk/hmr.js"></script>\n' + devBootstrapScript() + '\n';
150
146
  return html.replace('</body>', scripts + '</body>');
151
147
  }
152
- function makeWebRequest(nodeReq, url, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
153
- const parsedUrl = new URL(url, `http://${nodeReq.headers.host || 'localhost'}`);
154
- const method = nodeReq.method || 'GET';
155
- let _bodyBuffer = null;
156
- async function getBody() {
157
- if (_bodyBuffer)
158
- return _bodyBuffer;
159
- const chunks = [];
160
- let total = 0;
161
- for await (const chunk of nodeReq) {
162
- total += chunk.byteLength;
163
- if (total > maxBodyBytes)
164
- throw bodyTooLarge(maxBodyBytes);
165
- chunks.push(Buffer.from(chunk));
166
- }
167
- _bodyBuffer = Buffer.concat(chunks);
168
- return _bodyBuffer;
169
- }
170
- const webRequest = new Request(parsedUrl, { method, headers: nodeReq.headers, body: null });
171
- webRequest.json = async () => { try {
172
- return JSON.parse((await getBody()).toString());
173
- }
174
- catch {
175
- return null;
176
- } };
177
- webRequest.text = async () => (await getBody()).toString('utf-8');
178
- webRequest.formData = async () => {
179
- const body = await getBody();
180
- const ct = String(nodeReq.headers['content-type'] || '');
181
- if (ct.includes('multipart/form-data')) {
182
- const temp = new Request('http://localhost', { method: 'POST', headers: nodeReq.headers, body: body });
183
- return temp.formData();
184
- }
185
- const fd = new FormData();
186
- if (ct.includes('x-www-form-urlencoded')) {
187
- for (const [k, v] of new URLSearchParams(body.toString()).entries())
188
- fd.append(k, v);
189
- }
190
- return fd;
191
- };
192
- webRequest.clone = () => webRequest;
193
- return webRequest;
194
- }
195
148
  const MIME = {
196
149
  '.svg': 'image/svg+xml', '.css': 'text/css', '.js': 'application/javascript',
197
150
  '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
@@ -1,10 +1,5 @@
1
1
  import { type Server } from 'node:http';
2
2
  import type { CompressionConfig, ProdServerOptions } from '@vesk/adapter/src/types';
3
- export declare function bodyTooLarge(maxBytes: number): Error & {
4
- status: number;
5
- };
6
- export declare function startProdServer(outDir: string, options?: ProdServerOptions & {
7
- compression?: CompressionConfig | false;
8
- healthCheck?: boolean;
9
- }): Promise<Server>;
3
+ export type { CompressionConfig, ProdServerOptions };
4
+ export declare function startProdServer(outDir: string, options?: ProdServerOptions): Promise<Server>;
10
5
  //# sourceMappingURL=prod-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAcjG,OAAO,KAAK,EAAkB,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAuFpG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AA0FD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG;IAAE,WAAW,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6mBvK"}
1
+ {"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAejG,OAAO,KAAK,EAAkB,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAIpG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,CAAC;AAyHrD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwoBlG"}
@@ -13,6 +13,7 @@ import { loadVeskConfig } from '@vesk/adapter/src/load-config';
13
13
  import { renderDefaultNotFoundPage, renderDefaultErrorPage } from '@vesk/adapter/src/default-pages';
14
14
  import { installResponsePipeline } from '@vesk/adapter/src/response-pipeline';
15
15
  import { resolveBoundaryFileForUrl } from '@vesk/adapter/src/ssr-function';
16
+ import { makeWebRequest, bodyTooLarge } from '@vesk/adapter/src/web-request';
16
17
  import { mimeFor } from '@vesk/adapter/src/platform-output';
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
  async function readBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
@@ -102,69 +103,10 @@ function pumpRemaining(res, reader) {
102
103
  res.end();
103
104
  })();
104
105
  }
105
- export function bodyTooLarge(maxBytes) {
106
- const err = new Error(`Request body exceeds limit (${maxBytes} bytes)`);
107
- err.status = 413;
108
- return err;
109
- }
110
106
  function errorStatus(e, fallback) {
111
107
  const s = e?.status ?? e?.statusCode;
112
108
  return typeof s === 'number' && s >= 400 && s < 600 ? s : fallback;
113
109
  }
114
- function makeWebRequest(nodeReq, url, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
115
- const parsedUrl = new URL(url, `http://${nodeReq.headers.host || 'localhost'}`);
116
- const method = nodeReq.method || 'GET';
117
- let _bodyBuffer = null;
118
- async function getBody() {
119
- if (_bodyBuffer)
120
- return _bodyBuffer;
121
- const chunks = [];
122
- let total = 0;
123
- for await (const chunk of nodeReq) {
124
- total += chunk.byteLength;
125
- if (total > maxBodyBytes)
126
- throw bodyTooLarge(maxBodyBytes);
127
- chunks.push(Buffer.from(chunk));
128
- }
129
- _bodyBuffer = Buffer.concat(chunks);
130
- return _bodyBuffer;
131
- }
132
- const webRequest = new Request(parsedUrl, { method, headers: nodeReq.headers, body: null });
133
- webRequest.json = async () => { try {
134
- return JSON.parse((await getBody()).toString());
135
- }
136
- catch {
137
- return null;
138
- } };
139
- webRequest.text = async () => (await getBody()).toString('utf-8');
140
- webRequest.formData = async () => {
141
- const body = await getBody();
142
- const ct = String(nodeReq.headers['content-type'] || '');
143
- if (ct.includes('multipart/form-data')) {
144
- const temp = new Request('http://localhost', { method: 'POST', headers: nodeReq.headers, body: body });
145
- return temp.formData();
146
- }
147
- const fd = new FormData();
148
- if (ct.includes('x-www-form-urlencoded')) {
149
- for (const [k, v] of new URLSearchParams(body.toString()).entries())
150
- fd.append(k, v);
151
- }
152
- return fd;
153
- };
154
- webRequest.clone = () => webRequest;
155
- const cookies = {};
156
- for (const [k, v] of String(nodeReq.headers.cookie || '').split(';').map(s => s.trim()).filter(Boolean)) {
157
- const eq = k.indexOf('=');
158
- if (eq > -1)
159
- cookies[k.slice(0, eq)] = decodeURIComponent(k.slice(eq + 1));
160
- }
161
- webRequest.cookies = cookies;
162
- const query = {};
163
- for (const [k, v] of parsedUrl.searchParams.entries())
164
- query[k] = v;
165
- webRequest.query = query;
166
- return webRequest;
167
- }
168
110
  export async function startProdServer(outDir, options) {
169
111
  const port = options?.port || 3000;
170
112
  const host = options?.host || '127.0.0.1';
@@ -253,9 +195,26 @@ export async function startProdServer(outDir, options) {
253
195
  }
254
196
  }
255
197
  const functionCache = new Map();
198
+ /** Import failures, kept so the same broken module is not re-imported per request. */
199
+ const functionFailures = new Map();
200
+ /**
201
+ * Load a generated function module.
202
+ *
203
+ * Returns null both for "no such file" and for "the file exists but could not
204
+ * be imported", and the caller cannot tell them apart — which turned a route
205
+ * with a bad import into a **404 with no diagnostic anywhere**. A missing
206
+ * export or an unresolvable dependency is a build bug, and the only evidence
207
+ * was an empty 404. `functionFailure` records what happened so the request
208
+ * path can answer 500 and log it, which is what a broken route is.
209
+ */
210
+ function functionFailure(funcPath) {
211
+ return functionFailures.get(funcPath);
212
+ }
256
213
  async function loadFunction(funcPath) {
257
214
  if (functionCache.has(funcPath))
258
215
  return functionCache.get(funcPath);
216
+ if (functionFailures.has(funcPath))
217
+ return null;
259
218
  const fullPath = resolve(outDir, funcPath);
260
219
  if (!existsSync(fullPath))
261
220
  return null;
@@ -264,7 +223,9 @@ export async function startProdServer(outDir, options) {
264
223
  functionCache.set(funcPath, mod);
265
224
  return mod;
266
225
  }
267
- catch {
226
+ catch (e) {
227
+ console.error(`vesk start: failed to load ${funcPath}:`, e instanceof Error ? e.message : e);
228
+ functionFailures.set(funcPath, e);
268
229
  return null;
269
230
  }
270
231
  }
@@ -579,19 +540,27 @@ export async function startProdServer(outDir, options) {
579
540
  const params = matchPath(route.path, url.pathname);
580
541
  if (params) {
581
542
  const mod = await loadFunction(route.function);
582
- if (mod) {
583
- try {
584
- const webRequest = makeWebRequest(req, url.href, maxBodyBytes);
585
- const response = await withSecurityScope(() => mod.handle(webRequest));
586
- await deliverResponse(res, response);
587
- return;
588
- }
589
- catch (e) {
590
- const status = errorStatus(e, 500);
591
- res.writeHead(status, { 'Content-Type': 'application/json' });
592
- res.end(JSON.stringify({ error: status === 500 ? 'Internal Server Error' : e instanceof Error ? e.message : String(e) }));
543
+ if (!mod) {
544
+ // The route matched, so a 404 would be a lie: the handler is
545
+ // there and could not be loaded.
546
+ if (functionFailure(route.function)) {
547
+ res.writeHead(500, { 'Content-Type': 'application/json' });
548
+ res.end(JSON.stringify({ error: 'Internal Server Error' }));
593
549
  return;
594
550
  }
551
+ continue;
552
+ }
553
+ try {
554
+ const webRequest = makeWebRequest(req, url.href, maxBodyBytes);
555
+ const response = await withSecurityScope(() => mod.handle(webRequest));
556
+ await deliverResponse(res, response);
557
+ return;
558
+ }
559
+ catch (e) {
560
+ const status = errorStatus(e, 500);
561
+ res.writeHead(status, { 'Content-Type': 'application/json' });
562
+ res.end(JSON.stringify({ error: status === 500 ? 'Internal Server Error' : e instanceof Error ? e.message : String(e) }));
563
+ return;
595
564
  }
596
565
  }
597
566
  }
@@ -1 +1 @@
1
- {"version":3,"file":"response-pipeline.d.ts","sourceRoot":"","sources":["../src/response-pipeline.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAKjE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAgC/F;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAWjG;AAED,MAAM,WAAW,uBAAuB;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AA8BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACtC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,uBAA4B,GACnC,IAAI,CA2ON"}
1
+ {"version":3,"file":"response-pipeline.d.ts","sourceRoot":"","sources":["../src/response-pipeline.ts"],"names":[],"mappings":"AAgEA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAKjE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAgC/F;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAWjG;AAED,MAAM,WAAW,uBAAuB;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AA8BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACtC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,uBAA4B,GACnC,IAAI,CA2ON"}
@@ -29,6 +29,29 @@
29
29
  * Deliberately not here: range requests, `Last-Modified`/`If-Modified-Since`
30
30
  * (the ETag covers the same revalidation need and cannot disagree with itself),
31
31
  * and compression of already-encoded bodies.
32
+ *
33
+ * ## Shape
34
+ *
35
+ * ```ts
36
+ * installResponsePipeline(res, req, {
37
+ * compress?: boolean; // default true
38
+ * compressThreshold?: number;// default 1024
39
+ * etag?: boolean; // default true
40
+ * brotliQuality?: number; // 0-11, default 5
41
+ * gzipLevel?: number; // 1-9, default 6
42
+ * })
43
+ *
44
+ * // pure helpers, exported for testing and for any other transport
45
+ * negotiateEncoding(acceptEncoding): 'br' | 'gzip' | null
46
+ * isCompressibleType(contentType): boolean
47
+ * etagFor(body: Buffer): '"..."'
48
+ * ifNoneMatchSatisfied(ifNoneMatch, etag): boolean
49
+ * ```
50
+ *
51
+ * Install it ONCE per request, before any header is written. It is idempotent
52
+ * per `res` in the sense that matters — a second install would defer again —
53
+ * but a handler that bypasses it loses compression, `Vary`, ETag and 304 for
54
+ * everything it serves, silently.
32
55
  */
33
56
  import { createHash } from 'node:crypto';
34
57
  import { brotliCompress, constants as zlibConstants, createBrotliCompress, createGzip, gzip, } from 'node:zlib';
@@ -1 +1 @@
1
- {"version":3,"file":"server-only-plugin.d.ts","sourceRoot":"","sources":["../src/server-only-plugin.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACtF;AAED,kGAAkG;AAClG,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,KAAK,EAAE;QACb,SAAS,EAAE,CACT,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,EAC5C,EAAE,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,KACtD,IAAI,CAAC;QACV,MAAM,EAAE,CACN,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,EAC3C,EAAE,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,KAAK;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,KACjE,IAAI,CAAC;KACX,KAAK,IAAI,CAAC;CACZ;AAMD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CA+C1E;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAmB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAoDxE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,iBAAiB,CAoDzF"}
1
+ {"version":3,"file":"server-only-plugin.d.ts","sourceRoot":"","sources":["../src/server-only-plugin.ts"],"names":[],"mappings":"AA6CA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACtF;AAED,kGAAkG;AAClG,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,KAAK,EAAE;QACb,SAAS,EAAE,CACT,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,EAC5C,EAAE,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,KACtD,IAAI,CAAC;QACV,MAAM,EAAE,CACN,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,EAC3C,EAAE,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,KAAK;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,KACjE,IAAI,CAAC;KACX,KAAK,IAAI,CAAC;CACZ;AAMD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CA+C1E;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAmB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAoDxE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,iBAAiB,CAoDzF"}
@@ -19,6 +19,25 @@
19
19
  * TCP drivers are NOT stubbed. A browser bundle cannot contain one under any
20
20
  * circumstances, so a stub would ship a page that looks fine and fails at the
21
21
  * first query; they are reported and left for esbuild to reject.
22
+ *
23
+ * ## Shape
24
+ *
25
+ * ```ts
26
+ * serverOnlyPlugin({
27
+ * onStub?: (hit: { specifier, binding, importer }) => void, // stubbed
28
+ * onDriver?:(hit: { specifier, importer }) => void, // refused
29
+ * onEager?: (hit: { specifier, bindings: string[], importer }) => void, // module-scope: build error
30
+ * })
31
+ *
32
+ * // helpers, exported for testing
33
+ * importedNamesFor(code, specifier): string[] // what the importer asks for
34
+ * referencedAtTopLevel(code, name): boolean // module-scope use?
35
+ * stubFactorySource(specifier): string // the shared throwing proxy
36
+ * ```
37
+ *
38
+ * `onEager` returning null (rather than a stub) is deliberate: the browser would
39
+ * evaluate that statement while loading the chunk, so there is no call site to
40
+ * throw at.
22
41
  */
23
42
  import { readFileSync } from 'node:fs';
24
43
  import { parse } from '@vesk/compiler/src/parser';
@@ -25,6 +25,32 @@
25
25
  * opt-out: the alternative is discovering it in a deployment log.
26
26
  *
27
27
  * AST-only throughout. No text scanning — see the module rules.
28
+ *
29
+ * ## Shape
30
+ *
31
+ * ```ts
32
+ * // classification
33
+ * classifySpecifier(spec): 'server-only-package' | 'tcp-driver' | null
34
+ * isServerOnlySpecifier(spec): boolean
35
+ * isTcpDriverSpecifier(spec): boolean
36
+ *
37
+ * // enforcement
38
+ * serverOnlyStubs(notes: Array<{ specifier, binding }>): string // from a recorded list
39
+ * serverOnlyImportStubs(code, classify?): string // scanned from source
40
+ * scanServerOnlyImports(files: Array<{ path, code }>): ServerOnlyViolation[]
41
+ * tcpDriverViolations(appDir): ServerOnlyViolation[] // whole app tree
42
+ * edgeDriverBuildError(violations): Error // the socketless message
43
+ *
44
+ * // target
45
+ * isSocketlessTarget(platform, target): boolean
46
+ * collectAppSources(dir, out?): string[]
47
+ * ```
48
+ *
49
+ * Adding to the table: extend `SERVER_ONLY_EXACT` / `TCP_DRIVERS`, and — if the
50
+ * package is legitimately usable on the client through a subpath — leave that
51
+ * subpath unmatched, as `slintorm/edge` is. See
52
+ * `packages/adapter/AGENTS.md` for the three outcomes the plugin must
53
+ * distinguish.
28
54
  */
29
55
  export type ServerOnlyKind = 'server-only-package' | 'tcp-driver';
30
56
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"server-only.d.ts","sourceRoot":"","sources":["../src/server-only.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAsCH,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,YAAY,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAMrE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAkB5F;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,GAAG,IAAwB,GAAG,MAAM,CAoBjI;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAWD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC3C,mBAAmB,EAAE,CA8BvB;AAYD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,KAAK,CAU7E;AAYD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGlH;AAMD,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAUzE"}
1
+ {"version":3,"file":"server-only.d.ts","sourceRoot":"","sources":["../src/server-only.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAsCH,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,YAAY,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAMrE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAkB5F;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,GAAG,IAAwB,GAAG,MAAM,CAoBjI;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAWD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC3C,mBAAmB,EAAE,CA8BvB;AAYD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,KAAK,CAU7E;AAYD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGlH;AAMD,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAUzE"}
@@ -25,6 +25,32 @@
25
25
  * opt-out: the alternative is discovering it in a deployment log.
26
26
  *
27
27
  * AST-only throughout. No text scanning — see the module rules.
28
+ *
29
+ * ## Shape
30
+ *
31
+ * ```ts
32
+ * // classification
33
+ * classifySpecifier(spec): 'server-only-package' | 'tcp-driver' | null
34
+ * isServerOnlySpecifier(spec): boolean
35
+ * isTcpDriverSpecifier(spec): boolean
36
+ *
37
+ * // enforcement
38
+ * serverOnlyStubs(notes: Array<{ specifier, binding }>): string // from a recorded list
39
+ * serverOnlyImportStubs(code, classify?): string // scanned from source
40
+ * scanServerOnlyImports(files: Array<{ path, code }>): ServerOnlyViolation[]
41
+ * tcpDriverViolations(appDir): ServerOnlyViolation[] // whole app tree
42
+ * edgeDriverBuildError(violations): Error // the socketless message
43
+ *
44
+ * // target
45
+ * isSocketlessTarget(platform, target): boolean
46
+ * collectAppSources(dir, out?): string[]
47
+ * ```
48
+ *
49
+ * Adding to the table: extend `SERVER_ONLY_EXACT` / `TCP_DRIVERS`, and — if the
50
+ * package is legitimately usable on the client through a subpath — leave that
51
+ * subpath unmatched, as `slintorm/edge` is. See
52
+ * `packages/adapter/AGENTS.md` for the three outcomes the plugin must
53
+ * distinguish.
28
54
  */
29
55
  import { parse } from '@vesk/compiler/src/parser';
30
56
  import { readFileSync, readdirSync, statSync } from 'node:fs';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The `Request` handed to API routes and server actions.
3
+ *
4
+ * One implementation, used by both the dev and production servers. They each
5
+ * carried an identical copy until this existed, which is how the same defect sat
6
+ * in both: the shim built its `Request` with `body: null` and supplied only
7
+ * whole-body readers (`text()`, `json()`, `formData()`). Anything reading
8
+ * `request.body` — which is every streaming parser, including
9
+ * `@vesk/plugin-multipart` — saw no body at all and failed with `NO_BODY`.
10
+ * That is the difference between an upload handler that works and one that
11
+ * 400s on every request, so the body is now a real stream.
12
+ *
13
+ * The stream is created LAZILY, on first access to `request.body`. Attaching
14
+ * `data` listeners eagerly would start draining every GET body in the process,
15
+ * and a GET that never asked for its body would still be read.
16
+ *
17
+ * Reading is single-pass by definition, so both access patterns have to work on
18
+ * one socket: every byte that flows through the stream is cached, and
19
+ * `text()`/`json()`/`formData()` serve from that cache if the stream has already
20
+ * been taken. A handler that reads `request.body` and then calls
21
+ * `request.text()` gets the body once, not a hang.
22
+ *
23
+ * ## Shape
24
+ *
25
+ * ```ts
26
+ * makeWebRequest(nodeReq, url, maxBodyBytes?): ExtendedRequest
27
+ *
28
+ * interface ExtendedRequest extends Request {
29
+ * body: ReadableStream<Uint8Array> | null; // lazy; null for GET/HEAD
30
+ * text(): Promise<string>;
31
+ * json(): Promise<unknown>;
32
+ * formData(): Promise<FormData>;
33
+ * clone(): ExtendedRequest; // same object — one socket, one read
34
+ * cookies: Record<string, string>;
35
+ * query: Record<string, string>;
36
+ * }
37
+ *
38
+ * bodyTooLarge(maxBytes): Error & { status: 413 } // re-exported by prod-server
39
+ * ```
40
+ *
41
+ * One instance per request. Do not attach string-keyed properties to it — that is
42
+ * a public name the framework has to reserve forever, with no type for it. Use a
43
+ * private `Symbol` (see `AUTO_PARSED` in `@vesk/plugin-multipart`).
44
+ */
45
+ import type { IncomingMessage } from 'node:http';
46
+ export interface ExtendedRequest extends Request {
47
+ json(): Promise<unknown>;
48
+ text(): Promise<string>;
49
+ formData(): Promise<FormData>;
50
+ clone(): ExtendedRequest;
51
+ cookies: Record<string, string>;
52
+ query: Record<string, string>;
53
+ }
54
+ export declare function bodyTooLarge(maxBytes: number): Error & {
55
+ status: number;
56
+ };
57
+ export declare function makeWebRequest(nodeReq: IncomingMessage, url: string, maxBodyBytes?: number): ExtendedRequest;
58
+ //# sourceMappingURL=web-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-request.d.ts","sourceRoot":"","sources":["../src/web-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAIjD,MAAM,WAAW,eAAgB,SAAQ,OAAO;IAC9C,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9B,KAAK,IAAI,eAAe,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,MAAM,EACX,YAAY,GAAE,MAA+B,GAC5C,eAAe,CAwIjB"}
@@ -0,0 +1,165 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-codegen';
3
+ export function bodyTooLarge(maxBytes) {
4
+ const err = new Error(`Request body exceeds limit (${maxBytes} bytes)`);
5
+ err.status = 413;
6
+ return err;
7
+ }
8
+ export function makeWebRequest(nodeReq, url, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
9
+ const parsedUrl = new URL(url, `http://${nodeReq.headers.host || 'localhost'}`);
10
+ const method = nodeReq.method || 'GET';
11
+ let cachedBody = null;
12
+ let streamTaken = false;
13
+ let streamEnded = false;
14
+ const endWaiters = [];
15
+ const settleEnd = (body) => {
16
+ if (body !== null)
17
+ cachedBody = body;
18
+ streamEnded = true;
19
+ for (const resolve of endWaiters.splice(0))
20
+ resolve();
21
+ };
22
+ function createBodyStream() {
23
+ streamTaken = true;
24
+ const cache = [];
25
+ let total = 0;
26
+ return new ReadableStream({
27
+ start(controller) {
28
+ nodeReq.on('data', (chunk) => {
29
+ total += chunk.length;
30
+ if (total > maxBodyBytes) {
31
+ // Same cap the buffered readers enforce, checked as the bytes arrive
32
+ // so an oversized upload is refused mid-body rather than after the
33
+ // last byte has been read and held in memory.
34
+ settleEnd(Buffer.concat(cache));
35
+ cache.length = 0;
36
+ try {
37
+ controller.error(bodyTooLarge(maxBodyBytes));
38
+ }
39
+ catch { /* already settled */ }
40
+ nodeReq.destroy();
41
+ return;
42
+ }
43
+ cache.push(chunk);
44
+ try {
45
+ controller.enqueue(new Uint8Array(chunk));
46
+ }
47
+ catch { /* consumer cancelled */ }
48
+ });
49
+ nodeReq.on('end', () => {
50
+ settleEnd(Buffer.concat(cache));
51
+ cache.length = 0;
52
+ try {
53
+ controller.close();
54
+ }
55
+ catch { /* errored */ }
56
+ });
57
+ nodeReq.on('error', (e) => {
58
+ settleEnd(Buffer.concat(cache));
59
+ cache.length = 0;
60
+ try {
61
+ controller.error(e);
62
+ }
63
+ catch { /* already settled */ }
64
+ });
65
+ },
66
+ });
67
+ }
68
+ async function getBody() {
69
+ if (cachedBody)
70
+ return cachedBody;
71
+ if (streamTaken) {
72
+ // The stream is the reader now. Everything it has seen is in the cache,
73
+ // and it settles on 'end' — so wait rather than racing an empty read.
74
+ while (!streamEnded)
75
+ await new Promise(r => endWaiters.push(r));
76
+ return cachedBody || Buffer.alloc(0);
77
+ }
78
+ const chunks = [];
79
+ let total = 0;
80
+ for await (const chunk of nodeReq) {
81
+ total += chunk.byteLength;
82
+ if (total > maxBodyBytes)
83
+ throw bodyTooLarge(maxBodyBytes);
84
+ chunks.push(Buffer.from(chunk));
85
+ }
86
+ cachedBody = Buffer.concat(chunks);
87
+ return cachedBody;
88
+ }
89
+ const webRequest = new Request(parsedUrl, {
90
+ method,
91
+ headers: nodeReq.headers,
92
+ body: null,
93
+ });
94
+ // Shadow the prototype's `body` getter with a lazily-created stream over the
95
+ // Node request. An own accessor on the instance wins over the prototype one,
96
+ // which is the only reason this works without a Node-specific Request class.
97
+ let bodyStream = null;
98
+ Object.defineProperty(webRequest, 'body', {
99
+ configurable: true,
100
+ enumerable: false,
101
+ get: () => {
102
+ if (bodyStream)
103
+ return bodyStream;
104
+ // `new Request(..., { body: null })` reports null for GET/HEAD, which is
105
+ // correct; only a request that can carry a body needs a stream.
106
+ if (method === 'GET' || method === 'HEAD')
107
+ return null;
108
+ bodyStream = createBodyStream();
109
+ return bodyStream;
110
+ },
111
+ });
112
+ webRequest.json = async () => { try {
113
+ return JSON.parse((await getBody()).toString());
114
+ }
115
+ catch {
116
+ return null;
117
+ } };
118
+ webRequest.text = async () => (await getBody()).toString('utf-8');
119
+ webRequest.formData = async () => {
120
+ const body = await getBody();
121
+ const ct = String(nodeReq.headers['content-type'] || '');
122
+ if (ct.includes('multipart/form-data')) {
123
+ const temp = new Request('http://localhost', { method: 'POST', headers: nodeReq.headers, body: body });
124
+ return temp.formData();
125
+ }
126
+ const fd = new FormData();
127
+ if (ct.includes('x-www-form-urlencoded')) {
128
+ for (const [k, v] of new URLSearchParams(body.toString()).entries())
129
+ fd.append(k, v);
130
+ }
131
+ return fd;
132
+ };
133
+ webRequest.clone = () => webRequest;
134
+ const cookies = {};
135
+ // Iterate the pieces, do NOT destructure them. `cookie.split(';')` yields an
136
+ // array of STRINGS, and a string is iterable, so `for (const [k, v] of …)`
137
+ // bound `k` to the first CHARACTER of the pair ('s' of "session=…"),
138
+ // `indexOf('=')` was always -1, and the loop never populated anything.
139
+ // `request.cookies` has therefore been `{}` in every production request that
140
+ // ever read it. The `[k, v]` form is right for `searchParams.entries()`,
141
+ // which really does yield pairs — which is why the bug survived next to
142
+ // working code.
143
+ for (const part of String(nodeReq.headers.cookie || '').split(';')) {
144
+ const eq = part.indexOf('=');
145
+ if (eq === -1)
146
+ continue;
147
+ const name = part.slice(0, eq).trim();
148
+ if (!name)
149
+ continue;
150
+ const raw = part.slice(eq + 1).trim();
151
+ try {
152
+ cookies[name] = decodeURIComponent(raw);
153
+ }
154
+ catch {
155
+ // A malformed percent-escape must not lose the whole cookie jar.
156
+ cookies[name] = raw;
157
+ }
158
+ }
159
+ webRequest.cookies = cookies;
160
+ const query = {};
161
+ for (const [k, v] of parsedUrl.searchParams.entries())
162
+ query[k] = v;
163
+ webRequest.query = query;
164
+ return webRequest;
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/adapter",
3
- "version": "0.2.70",
3
+ "version": "0.2.72",
4
4
  "type": "module",
5
5
  "description": "Vesk adapter — builds deployable .vesk/ output for Deno and Node serverless",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  "sharp": "^0.35.3"
24
24
  },
25
25
  "dependencies": {
26
- "@vesk/compiler": "^0.2.70",
27
- "@vesk/runtime": "^0.2.70",
28
- "@vesk/types": "^0.2.70"
26
+ "@vesk/compiler": "^0.2.72",
27
+ "@vesk/runtime": "^0.2.72",
28
+ "@vesk/types": "^0.2.72"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",