@vesk/compiler 0.1.1 → 0.2.8

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 (51) hide show
  1. package/dist/api-routes.d.ts +6 -1
  2. package/dist/api-routes.d.ts.map +1 -1
  3. package/dist/api-routes.js +52 -7
  4. package/dist/client-codegen.d.ts +15 -0
  5. package/dist/client-codegen.d.ts.map +1 -1
  6. package/dist/client-codegen.js +73 -10
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +25 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +33 -4
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/ir-generator.d.ts.map +1 -1
  16. package/dist/ir-generator.js +109 -1
  17. package/dist/md-inline.d.ts +17 -0
  18. package/dist/md-inline.d.ts.map +1 -0
  19. package/dist/md-inline.js +149 -0
  20. package/dist/middleware.d.ts.map +1 -1
  21. package/dist/middleware.js +42 -7
  22. package/dist/router.d.ts +2 -0
  23. package/dist/router.d.ts.map +1 -1
  24. package/dist/router.js +48 -3
  25. package/dist/server-codegen.d.ts +1 -1
  26. package/dist/server-codegen.d.ts.map +1 -1
  27. package/dist/server-codegen.js +1 -1
  28. package/dist/server-jsgen.d.ts.map +1 -1
  29. package/dist/server-jsgen.js +4 -3
  30. package/dist/server-render.d.ts.map +1 -1
  31. package/dist/server-render.js +16 -8
  32. package/dist/server-utils.d.ts +31 -0
  33. package/dist/server-utils.d.ts.map +1 -1
  34. package/dist/server-utils.js +478 -52
  35. package/dist/ssr-store.d.ts +1 -1
  36. package/dist/ssr-store.d.ts.map +1 -1
  37. package/dist/ssr-store.js +3 -0
  38. package/dist/typecheck.d.ts +2 -0
  39. package/dist/typecheck.d.ts.map +1 -1
  40. package/dist/typecheck.js +452 -29
  41. package/dist/types.d.ts +4 -129
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js +1 -0
  44. package/dist/vesk-plugin.d.ts +2 -0
  45. package/dist/vesk-plugin.d.ts.map +1 -1
  46. package/dist/vesk-plugin.js +152 -0
  47. package/dist/vsk-tsx.d.ts +62 -0
  48. package/dist/vsk-tsx.d.ts.map +1 -1
  49. package/dist/vsk-tsx.js +844 -362
  50. package/package.json +2 -1
  51. package/dist/package.json +0 -15
@@ -134,31 +134,264 @@ export function isStatic(body) {
134
134
  }
135
135
  export function escapeHtml(str) {
136
136
  return str
137
- .replace(/&/g, '&')
138
- .replace(/</g, '&lt;')
139
- .replace(/>/g, '&gt;')
140
- .replace(/"/g, '&quot;')
141
- .replace(/'/g, '&#39;');
142
- }
143
- const SECRET_PATTERNS = [
144
- { pattern: /(sk_live_[a-zA-Z0-9]{20,})/g, replace: 'sk_live_***' },
145
- { pattern: /(sk_test_[a-zA-Z0-9]{20,})/g, replace: 'sk_test_***' },
146
- { pattern: /(ghp_[a-zA-Z0-9]{36,})/g, replace: 'ghp_***' },
147
- { pattern: /(gho_[a-zA-Z0-9]{36,})/g, replace: 'gho_***' },
148
- { pattern: /(xox[bpsra]-[a-zA-Z0-9-]{20,})/g, replace: 'xox-***' },
149
- { pattern: /(Bearer\s+)[a-zA-Z0-9._-]{20,}/g, replace: '$1***' },
150
- { pattern: /(Authorization:\s*Basic\s+)[a-zA-Z0-9+/=]{20,}/gi, replace: '$1***' },
151
- { pattern: /(-----BEGIN\s+(?:RSA\s+)?PRIVATE KEY-----)[\s\S]*?(-----END\s+(?:RSA\s+)?PRIVATE KEY-----)/g, replace: '$1***$2' },
152
- { pattern: /(['"]?(?:api[_-]?key|secret|password|token|auth|private[_-]?key|access[_-]?key|session[_-]?secret)[, }\]'"*]*[:=]\s*['"]?)(?!.*\*\*\*)([^'"\s,}\]]{8,})(['"]?)/gi, replace: '$1***$3' },
137
+ .split('&').join('&amp;')
138
+ .split('<').join('&lt;')
139
+ .split('>').join('&gt;')
140
+ .split('"').join('&quot;')
141
+ .split("'").join('&#39;');
142
+ }
143
+ /** Escapes double quotes for embedding text inside a double-quoted attribute. */
144
+ export function quoteAttr(str) {
145
+ return str.split('"').join('&quot;');
146
+ }
147
+ /**
148
+ * Makes JSON output safe to embed inside an inline <script> block.
149
+ * Escapes `<` (breaks out of script context via `</script>`), line
150
+ * separators U+2028/U+2029 (valid JSON but invalid JS string literals).
151
+ */
152
+ export function safeJsonForScript(json) {
153
+ return json
154
+ .split('<').join('\\u003c')
155
+ .split('\u2028').join('\\u2028')
156
+ .split('\u2029').join('\\u2029');
157
+ }
158
+ /** Cryptographically strong random hex token (WebCrypto CSPRNG). */
159
+ export function randomToken(bytes = 24) {
160
+ const buf = new Uint8Array(bytes);
161
+ crypto.getRandomValues(buf);
162
+ let hex = '';
163
+ for (const b of buf)
164
+ hex += b.toString(16).padStart(2, '0');
165
+ return hex;
166
+ }
167
+ /** Default request-body cap (bytes) for servers and action/API handlers. */
168
+ export const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
169
+ function isAlnum(code) {
170
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
171
+ }
172
+ /** Token body charset: alnum plus base64url/JWT punctuation. */
173
+ function isTokenChar(code) {
174
+ return isAlnum(code) || code === 46 /* . */ || code === 95 /* _ */ || code === 45 /* - */ || code === 43 /* + */ || code === 47 /* / */ || code === 61 /* = */;
175
+ }
176
+ const TOKEN_PREFIXES = [
177
+ {
178
+ match: (s, i) => {
179
+ if (!s.startsWith('sk_', i))
180
+ return null;
181
+ if (s.startsWith('sk_live_', i))
182
+ return 'sk_live_';
183
+ if (s.startsWith('sk_test_', i))
184
+ return 'sk_test_';
185
+ return null;
186
+ },
187
+ emit: (keep) => keep + '***',
188
+ minLen: 20,
189
+ },
190
+ {
191
+ match: (s, i) => {
192
+ if (s.startsWith('ghp_', i))
193
+ return 'ghp_';
194
+ if (s.startsWith('gho_', i))
195
+ return 'gho_';
196
+ return null;
197
+ },
198
+ emit: (keep) => keep + '***',
199
+ minLen: 36,
200
+ },
201
+ {
202
+ match: (s, i) => {
203
+ if (!s.startsWith('xox', i) || i + 4 >= s.length)
204
+ return null;
205
+ const c = s[i + 3];
206
+ if (c !== 'b' && c !== 'p' && c !== 's' && c !== 'r' && c !== 'a')
207
+ return null;
208
+ if (s[i + 4] !== '-')
209
+ return null;
210
+ return 'xox' + c + '-';
211
+ },
212
+ emit: () => 'xox-***',
213
+ minLen: 20,
214
+ },
215
+ ];
216
+ const LOWER = (c) => c >= 'A' && c <= 'Z' ? String.fromCharCode(c.charCodeAt(0) + 32) : c;
217
+ function ciStartsWith(s, word, i) {
218
+ if (i + word.length > s.length)
219
+ return false;
220
+ for (let j = 0; j < word.length; j++) {
221
+ if (LOWER(s[i + j]) !== word[j])
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Replaces known secret token shapes with `prefix***`. Char-scan pass over
228
+ * the whole string (no regex). Returns a new string.
229
+ */
230
+ function scanSecretTokens(str) {
231
+ let out = '';
232
+ let i = 0;
233
+ while (i < str.length) {
234
+ // PEM private key blocks: BEGIN header kept, body collapsed, END kept.
235
+ if (str.startsWith('-----BEGIN ', i)) {
236
+ let hdrEnd = -1;
237
+ for (let j = i + 11; j + 5 <= str.length; j++) {
238
+ if (str.startsWith('-----', j)) {
239
+ hdrEnd = j;
240
+ break;
241
+ }
242
+ }
243
+ const endIdx = str.indexOf('-----END ', i + 11);
244
+ if (hdrEnd !== -1 && endIdx !== -1 && endIdx > hdrEnd) {
245
+ let closeIdx = -1;
246
+ for (let j = endIdx + 9; j + 5 <= str.length; j++) {
247
+ if (str.startsWith('-----', j)) {
248
+ closeIdx = j;
249
+ break;
250
+ }
251
+ }
252
+ if (closeIdx !== -1) {
253
+ out += str.slice(i, hdrEnd + 5) + '***' + str.slice(endIdx, closeIdx + 5);
254
+ i = closeIdx + 5;
255
+ continue;
256
+ }
257
+ }
258
+ }
259
+ let matched = false;
260
+ for (const tp of TOKEN_PREFIXES) {
261
+ const keep = tp.match(str, i);
262
+ if (keep !== null) {
263
+ let j = i + keep.length;
264
+ while (j < str.length && isTokenChar(str.charCodeAt(j)))
265
+ j++;
266
+ if (j - (i + keep.length) >= tp.minLen) {
267
+ out += tp.emit(keep);
268
+ i = j;
269
+ matched = true;
270
+ }
271
+ break;
272
+ }
273
+ }
274
+ if (matched)
275
+ continue;
276
+ // Bearer <token> / Authorization: Basic <token>
277
+ const prevIsWord = i > 0 && isAlnum(str.charCodeAt(i - 1));
278
+ if (!prevIsWord && ciStartsWith(str, 'bearer', i)) {
279
+ let j = i + 6;
280
+ let spaces = 0;
281
+ while (j < str.length && (str[j] === ' ' || str[j] === '\t')) {
282
+ j++;
283
+ spaces++;
284
+ }
285
+ if (spaces > 0) {
286
+ let k = j;
287
+ while (k < str.length && isTokenChar(str.charCodeAt(k)))
288
+ k++;
289
+ if (k - j >= 20) {
290
+ out += 'Bearer ***';
291
+ i = k;
292
+ continue;
293
+ }
294
+ }
295
+ }
296
+ if (!prevIsWord && ciStartsWith(str, 'basic', i)) {
297
+ let j = i + 5;
298
+ let spaces = 0;
299
+ while (j < str.length && (str[j] === ' ' || str[j] === '\t')) {
300
+ j++;
301
+ spaces++;
302
+ }
303
+ if (spaces > 0) {
304
+ let k = j;
305
+ while (k < str.length && isTokenChar(str.charCodeAt(k)))
306
+ k++;
307
+ if (k - j >= 20) {
308
+ out += 'Basic ***';
309
+ i = k;
310
+ continue;
311
+ }
312
+ }
313
+ }
314
+ out += str[i];
315
+ i++;
316
+ }
317
+ return out;
318
+ }
319
+ const SECRET_KEY_NAMES = [
320
+ 'api_key', 'api-key', 'apikey',
321
+ 'secret', 'password', 'token', 'auth',
322
+ 'private_key', 'private-key',
323
+ 'access_key', 'access-key',
324
+ 'session_secret',
153
325
  ];
326
+ function lowerRun(s) {
327
+ let out = '';
328
+ for (const c of s)
329
+ out += LOWER(c);
330
+ return out;
331
+ }
332
+ function isValueStopChar(c) {
333
+ return c === "'" || c === '"' || c === ' ' || c === '\t' || c === '\n' || c === '\r'
334
+ || c === ',' || c === '}' || c === ']';
335
+ }
336
+ /**
337
+ * Second redaction pass: `<secret-ish key>` followed by `:` or `=` gets its
338
+ * value collapsed to `***` unless the value already contains `***` (already
339
+ * handled by the token pass).
340
+ */
341
+ function scanKeyValueSecrets(str) {
342
+ let out = '';
343
+ let i = 0;
344
+ while (i < str.length) {
345
+ let keyHit = null;
346
+ if (i === 0 || !(isAlnum(str.charCodeAt(i - 1)) || str[i - 1] === '_' || str[i - 1] === '-')) {
347
+ const run = lowerRun(str.slice(i, i + 16));
348
+ for (const name of SECRET_KEY_NAMES) {
349
+ if (run.startsWith(name)) {
350
+ keyHit = { nameLen: name.length };
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ if (keyHit) {
356
+ let j = i + keyHit.nameLen;
357
+ // filler chars between key and separator: , space } ] ' " *
358
+ while (j < str.length && (str[j] === ',' || str[j] === ' ' || str[j] === '}' || str[j] === ']' || str[j] === "'" || str[j] === '"' || str[j] === '*'))
359
+ j++;
360
+ if (str[j] === ':' || str[j] === '=') {
361
+ j++; // separator
362
+ while (j < str.length && (str[j] === ' ' || str[j] === '\t'))
363
+ j++;
364
+ let openQuote = '';
365
+ if (str[j] === "'" || str[j] === '"') {
366
+ openQuote = str[j];
367
+ j++;
368
+ }
369
+ let k = j;
370
+ while (k < str.length && !isValueStopChar(str[k]))
371
+ k++;
372
+ const value = str.slice(j, k);
373
+ if (k - j >= 8 && !value.includes('***')) {
374
+ out += str.slice(i, j) + '***';
375
+ if (openQuote && k < str.length && str[k] === openQuote) {
376
+ out += openQuote;
377
+ k++;
378
+ }
379
+ else if (openQuote)
380
+ out += openQuote;
381
+ i = k;
382
+ continue;
383
+ }
384
+ }
385
+ }
386
+ out += str[i];
387
+ i++;
388
+ }
389
+ return out;
390
+ }
154
391
  export function redactLog(str) {
155
392
  if (!str || typeof str !== 'string')
156
393
  return str;
157
- let result = str;
158
- for (const { pattern, replace } of SECRET_PATTERNS) {
159
- result = result.replace(pattern, replace);
160
- }
161
- return result;
394
+ return scanKeyValueSecrets(scanSecretTokens(str));
162
395
  }
163
396
  const _origConsoleError = console.error;
164
397
  const _origConsoleLog = console.log;
@@ -179,24 +412,116 @@ export function raw(value) {
179
412
  return '';
180
413
  return String(value);
181
414
  }
415
+ // ── SHA-256 / HMAC (pure JS, sync — works in Node and browsers) ───
416
+ const SHA_K = new Uint32Array([
417
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
418
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
419
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
420
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
421
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
422
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
423
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
424
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
425
+ ]);
426
+ function sha256Bytes(msg) {
427
+ const len = msg.length;
428
+ const paddedLen = (((len + 9) + 63) >> 6) << 6;
429
+ const buf = new Uint8Array(paddedLen);
430
+ buf.set(msg);
431
+ buf[len] = 0x80;
432
+ const dv = new DataView(buf.buffer);
433
+ const bitLen = len * 8;
434
+ dv.setUint32(paddedLen - 8, Math.floor(bitLen / 0x100000000));
435
+ dv.setUint32(paddedLen - 4, bitLen >>> 0);
436
+ const rr = (x, n) => (x >>> n) | (x << (32 - n));
437
+ let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
438
+ let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
439
+ const w = new Uint32Array(64);
440
+ for (let off = 0; off < paddedLen; off += 64) {
441
+ for (let j = 0; j < 16; j++)
442
+ w[j] = dv.getUint32(off + j * 4);
443
+ for (let j = 16; j < 64; j++) {
444
+ const s0 = rr(w[j - 15], 7) ^ rr(w[j - 15], 18) ^ (w[j - 15] >>> 3);
445
+ const s1 = rr(w[j - 2], 17) ^ rr(w[j - 2], 19) ^ (w[j - 2] >>> 10);
446
+ w[j] = (w[j - 16] + s0 + w[j - 7] + s1) >>> 0;
447
+ }
448
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
449
+ for (let j = 0; j < 64; j++) {
450
+ const S1 = rr(e, 6) ^ rr(e, 11) ^ rr(e, 25);
451
+ const ch = (e & f) ^ (~e & g);
452
+ const t1 = (h + S1 + ch + SHA_K[j] + w[j]) >>> 0;
453
+ const S0 = rr(a, 2) ^ rr(a, 13) ^ rr(a, 22);
454
+ const maj = (a & b) ^ (a & c) ^ (b & c);
455
+ const t2 = (S0 + maj) >>> 0;
456
+ h = g;
457
+ g = f;
458
+ f = e;
459
+ e = (d + t1) >>> 0;
460
+ d = c;
461
+ c = b;
462
+ b = a;
463
+ a = (t1 + t2) >>> 0;
464
+ }
465
+ h0 = (h0 + a) >>> 0;
466
+ h1 = (h1 + b) >>> 0;
467
+ h2 = (h2 + c) >>> 0;
468
+ h3 = (h3 + d) >>> 0;
469
+ h4 = (h4 + e) >>> 0;
470
+ h5 = (h5 + f) >>> 0;
471
+ h6 = (h6 + g) >>> 0;
472
+ h7 = (h7 + h) >>> 0;
473
+ }
474
+ const out = new Uint8Array(32);
475
+ const odv = new DataView(out.buffer);
476
+ odv.setUint32(0, h0);
477
+ odv.setUint32(4, h1);
478
+ odv.setUint32(8, h2);
479
+ odv.setUint32(12, h3);
480
+ odv.setUint32(16, h4);
481
+ odv.setUint32(20, h5);
482
+ odv.setUint32(24, h6);
483
+ odv.setUint32(28, h7);
484
+ return out;
485
+ }
486
+ function concatBytes(a, b) {
487
+ const out = new Uint8Array(a.length + b.length);
488
+ out.set(a);
489
+ out.set(b, a.length);
490
+ return out;
491
+ }
492
+ function hmacSha256(keyBytes, message) {
493
+ const key = keyBytes.length > 64 ? sha256Bytes(keyBytes) : keyBytes;
494
+ const ipad = new Uint8Array(64);
495
+ const opad = new Uint8Array(64);
496
+ ipad.fill(0x36);
497
+ opad.fill(0x5c);
498
+ for (let i = 0; i < key.length; i++) {
499
+ ipad[i] ^= key[i];
500
+ opad[i] ^= key[i];
501
+ }
502
+ return sha256Bytes(concatBytes(opad, sha256Bytes(concatBytes(ipad, message))));
503
+ }
504
+ function toHex(bytes) {
505
+ let hex = '';
506
+ for (const b of bytes)
507
+ hex += b.toString(16).padStart(2, '0');
508
+ return hex;
509
+ }
510
+ function utf8(s) {
511
+ return new TextEncoder().encode(s);
512
+ }
182
513
  const __csrfSecrets = new Map();
183
514
  function csrfSecret(host) {
184
515
  if (!host)
185
516
  host = 'localhost';
186
517
  if (!__csrfSecrets.has(host)) {
187
- __csrfSecrets.set(host, Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
518
+ __csrfSecrets.set(host, randomToken(32));
188
519
  }
189
520
  return __csrfSecrets.get(host);
190
521
  }
191
- function csrfHmac(value, secret) {
192
- let h = 0;
193
- for (let i = 0; i < value.length; i++) {
194
- h = ((h << 5) - h + value.charCodeAt(i)) | 0;
195
- }
196
- for (let i = 0; i < secret.length; i++) {
197
- h = ((h << 5) - h + secret.charCodeAt(i)) | 0;
198
- }
199
- return (h >>> 0).toString(36);
522
+ /** HMAC-SHA256 over `value` keyed by `secret`, hex-encoded. */
523
+ export function csrfHmac(value, secret) {
524
+ return toHex(hmacSha256(utf8(secret), utf8(value)));
200
525
  }
201
526
  export function csrfToken(sessionId, host) {
202
527
  const secret = csrfSecret(host);
@@ -230,12 +555,94 @@ export function csrfGuard(request, host) {
230
555
  throw new Error('CSRF validation failed');
231
556
  }
232
557
  }
558
+ // ── Same-origin enforcement (default CSRF defense for actions/API) ──
559
+ /** Reads a header from either a plain record or a Headers-like object. */
560
+ function readHeader(headers, name) {
561
+ if (!headers)
562
+ return '';
563
+ const h = headers;
564
+ if (typeof h.get === 'function')
565
+ return String(h.get(name) ?? '');
566
+ return String(h[name] ?? '');
567
+ }
568
+ /** Extracts the authority (host[:port]) part of an absolute URL. */
569
+ export function urlAuthority(url) {
570
+ const schemeIdx = url.indexOf('://');
571
+ if (schemeIdx === -1)
572
+ return '';
573
+ let rest = url.slice(schemeIdx + 3);
574
+ let end = rest.length;
575
+ for (let i = 0; i < rest.length; i++) {
576
+ const c = rest[i];
577
+ if (c === '/' || c === '?' || c === '#') {
578
+ end = i;
579
+ break;
580
+ }
581
+ }
582
+ rest = rest.slice(0, end);
583
+ const at = rest.lastIndexOf('@');
584
+ if (at !== -1)
585
+ rest = rest.slice(at + 1);
586
+ return rest.toLowerCase();
587
+ }
588
+ function hostName(authority) {
589
+ const colon = authority.lastIndexOf(':');
590
+ if (colon === -1)
591
+ return authority;
592
+ // bare IPv6 without brackets has multiple colons — keep whole authority
593
+ let isIpv6 = false;
594
+ for (let i = 0; i < authority.length; i++) {
595
+ if (authority[i] === ':' && i !== colon) {
596
+ isIpv6 = true;
597
+ break;
598
+ }
599
+ }
600
+ return isIpv6 ? authority : authority.slice(0, colon);
601
+ }
602
+ const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
603
+ /**
604
+ * Default CSRF defense: unsafe methods must present an Origin or Referer
605
+ * whose authority matches the request Host. Browsers always attach Origin
606
+ * to cross-site POSTs, so mismatches are blocked; non-browser clients that
607
+ * send no Origin/Referer are allowed (they cannot be CSRF'd).
608
+ * Throws a 403-status error on mismatch.
609
+ */
610
+ export function assertSameOrigin(request) {
611
+ if (!request || typeof request !== 'object')
612
+ return;
613
+ const method = String(request.method || 'GET').toUpperCase();
614
+ if (!UNSAFE_METHODS.has(method))
615
+ return;
616
+ const headers = request.headers;
617
+ const origin = readHeader(headers, 'origin');
618
+ const source = origin || readHeader(headers, 'referer');
619
+ if (!source)
620
+ return; // non-browser client — no CSRF risk
621
+ const hostHeader = readHeader(headers, 'host').toLowerCase().trim();
622
+ if (!hostHeader)
623
+ return;
624
+ const sourceAuthority = urlAuthority(source);
625
+ if (sourceAuthority === hostHeader)
626
+ return;
627
+ // Tolerate one-sided port presence (e.g. proxy strips port from Host),
628
+ // but never a different hostname.
629
+ const srcHost = hostName(sourceAuthority);
630
+ if (srcHost && srcHost === hostName(hostHeader)) {
631
+ const srcHasPort = sourceAuthority.length > srcHost.length;
632
+ const dstHasPort = hostHeader.length > hostName(hostHeader).length;
633
+ if (srcHasPort !== dstHasPort)
634
+ return;
635
+ }
636
+ const err = new Error('Cross-origin request blocked');
637
+ err.status = 403;
638
+ throw err;
639
+ }
233
640
  const __cookieSecrets = new Map();
234
641
  function cookieSecret(host) {
235
642
  if (!host)
236
643
  host = 'localhost';
237
644
  if (!__cookieSecrets.has(host)) {
238
- __cookieSecrets.set(host, Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
645
+ __cookieSecrets.set(host, randomToken(32));
239
646
  }
240
647
  return __cookieSecrets.get(host);
241
648
  }
@@ -245,8 +652,12 @@ export async function signCookie(name, value, host) {
245
652
  const enc = new TextEncoder();
246
653
  const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
247
654
  const sig = await crypto.subtle.sign('HMAC', key, enc.encode(payload));
248
- const sigB64 = btoa(String.fromCharCode(...new Uint8Array(sig))).replace(/=+$/, '');
249
- return `${value}.${sigB64}`;
655
+ const sigChars = btoa(String.fromCharCode(...new Uint8Array(sig)));
656
+ // strip trailing '=' padding without regex
657
+ let sigEnd = sigChars.length;
658
+ while (sigEnd > 0 && sigChars[sigEnd - 1] === '=')
659
+ sigEnd--;
660
+ return `${value}.${sigChars.slice(0, sigEnd)}`;
250
661
  }
251
662
  export async function unsignCookie(name, signedValue, host) {
252
663
  if (!signedValue || typeof signedValue !== 'string')
@@ -316,7 +727,7 @@ export function securityHeaders(config = {}) {
316
727
  export function corsHeaders(security = {}, requestOrigin = '', host = '') {
317
728
  if (!requestOrigin)
318
729
  return {};
319
- const originHost = (requestOrigin || '').replace(/^https?:\/\//, '').split('/')[0].split(':')[0];
730
+ const originHost = stripScheme(requestOrigin).split('/')[0].split(':')[0];
320
731
  const localHost = (host || '').split(':')[0];
321
732
  if (originHost && localHost && originHost === localHost) {
322
733
  return {};
@@ -330,14 +741,24 @@ export function corsHeaders(security = {}, requestOrigin = '', host = '') {
330
741
  : allowedOrigins.includes(requestOrigin) ? requestOrigin : null;
331
742
  if (!origin)
332
743
  return {};
744
+ // Credentials are opt-in: `*` origins can never carry credentials, and
745
+ // defaulting this on silently exposed credentialed responses cross-origin.
746
+ const allowCredentials = cors.credentials === true && origin !== '*';
333
747
  return {
334
748
  'Access-Control-Allow-Origin': origin,
335
749
  'Access-Control-Allow-Methods': cors.methods || 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
336
750
  'Access-Control-Allow-Headers': cors.headers || 'Content-Type,Authorization,X-CSRF-Token',
337
- ...(cors.credentials !== false ? { 'Access-Control-Allow-Credentials': 'true' } : {}),
751
+ ...(allowCredentials ? { 'Access-Control-Allow-Credentials': 'true' } : {}),
338
752
  'Access-Control-Max-Age': String(cors.maxAge || 86400),
339
753
  };
340
754
  }
755
+ function stripScheme(url) {
756
+ if (url.startsWith('https://'))
757
+ return url.slice(8);
758
+ if (url.startsWith('http://'))
759
+ return url.slice(7);
760
+ return url;
761
+ }
341
762
  export function corsPreflight(request, security) {
342
763
  if ((request.method || 'GET').toUpperCase() !== 'OPTIONS')
343
764
  return false;
@@ -394,6 +815,8 @@ export function createRateLimiter(options = {}) {
394
815
  cleanupInterval.unref();
395
816
  function getClientIp(request) {
396
817
  const headers = request?.headers;
818
+ if (!options.trustProxy)
819
+ return 'unknown';
397
820
  const forwarded = headers?.['x-forwarded-for'];
398
821
  if (forwarded) {
399
822
  const ip = (typeof forwarded === 'string' ? forwarded.split(',')[0] : forwarded[0]).trim();
@@ -446,24 +869,27 @@ export function createRateLimiter(options = {}) {
446
869
  },
447
870
  };
448
871
  }
872
+ /**
873
+ * Resolves the client IP. Proxy headers (`x-forwarded-for`, `x-real-ip`) are
874
+ * only honored when `trustProxy` is explicitly enabled — otherwise any
875
+ * client could spoof its identity (and defeat rate limiting) with a header.
876
+ */
449
877
  export function getClientIp(request, trustProxy = false) {
878
+ if (!trustProxy)
879
+ return 'unknown';
450
880
  const headers = (request?.headers || {});
451
- if (trustProxy) {
452
- const forwarded = headers['x-forwarded-for'];
453
- if (forwarded) {
454
- const ips = (typeof forwarded === 'string' ? forwarded : String(forwarded)).split(',').map(s => s.trim());
455
- return ips[0] || 'unknown';
456
- }
457
- const realIp = headers['x-real-ip'];
458
- if (realIp)
459
- return typeof realIp === 'string' ? realIp : String(realIp);
460
- }
461
- if (headers['x-forwarded-for']) {
462
- const fwd = headers['x-forwarded-for'];
463
- return typeof fwd === 'string' ? fwd.split(',')[0].trim() : fwd[0];
881
+ const forwarded = headers['x-forwarded-for'];
882
+ if (forwarded) {
883
+ const ips = (typeof forwarded === 'string' ? forwarded : String(forwarded)).split(',').map(s => s.trim());
884
+ if (ips[0])
885
+ return ips[0];
464
886
  }
465
- if (headers['x-real-ip'])
466
- return typeof headers['x-real-ip'] === 'string' ? headers['x-real-ip'] : String(headers['x-real-ip']);
887
+ const realIp = headers['x-real-ip'];
888
+ if (realIp)
889
+ return typeof realIp === 'string' ? realIp : String(realIp);
890
+ const cfIp = headers['cf-connecting-ip'];
891
+ if (cfIp)
892
+ return typeof cfIp === 'string' ? cfIp : String(cfIp);
467
893
  return 'unknown';
468
894
  }
469
895
  export function getClientProtocol(request, trustProxy = false) {
@@ -472,7 +898,7 @@ export function getClientProtocol(request, trustProxy = false) {
472
898
  if (proto)
473
899
  return (typeof proto === 'string' ? proto.split(',')[0] : String(proto)).trim();
474
900
  }
475
- return request?.headers?.['x-forwarded-proto'] ? 'https' : 'http';
901
+ return 'http';
476
902
  }
477
903
  export function applyTrustProxy(ctx, trustProxy) {
478
904
  if (!ctx || !trustProxy)
@@ -1,4 +1,4 @@
1
- import { type SsrDataSink } from '@vesk/runtime/src/index-server';
1
+ import type { SsrDataSink } from '@vesk/runtime/src/resource';
2
2
  export declare function withSsrStore<T>(fn: () => T): T;
3
3
  export declare const ssrSink: SsrDataSink;
4
4
  //# sourceMappingURL=ssr-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-store.d.ts","sourceRoot":"","sources":["../src/ssr-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAI9E,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAI9C;AAED,eAAO,MAAM,OAAO,EAAE,WAgBrB,CAAC"}
1
+ {"version":3,"file":"ssr-store.d.ts","sourceRoot":"","sources":["../src/ssr-store.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAI9D,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAI9C;AAED,eAAO,MAAM,OAAO,EAAE,WAgBrB,CAAC"}
package/dist/ssr-store.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ // Import the type from its defining module (not the barrel): on a fresh
3
+ // checkout the compiler typechecks BEFORE @vesk/runtime's dist exists, and
4
+ // resolving types through the barrel then fails.
2
5
  import { setSsrSink } from '@vesk/runtime/src/index-server';
3
6
  const storage = new AsyncLocalStorage();
4
7
  export function withSsrStore(fn) {