@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.
- package/dist/api-routes.d.ts +6 -1
- package/dist/api-routes.d.ts.map +1 -1
- package/dist/api-routes.js +52 -7
- package/dist/client-codegen.d.ts +15 -0
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +73 -10
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +109 -1
- package/dist/md-inline.d.ts +17 -0
- package/dist/md-inline.d.ts.map +1 -0
- package/dist/md-inline.js +149 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +42 -7
- package/dist/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +48 -3
- package/dist/server-codegen.d.ts +1 -1
- package/dist/server-codegen.d.ts.map +1 -1
- package/dist/server-codegen.js +1 -1
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +4 -3
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +16 -8
- package/dist/server-utils.d.ts +31 -0
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +478 -52
- package/dist/ssr-store.d.ts +1 -1
- package/dist/ssr-store.d.ts.map +1 -1
- package/dist/ssr-store.js +3 -0
- package/dist/typecheck.d.ts +2 -0
- package/dist/typecheck.d.ts.map +1 -1
- package/dist/typecheck.js +452 -29
- package/dist/types.d.ts +4 -129
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/vesk-plugin.d.ts +2 -0
- package/dist/vesk-plugin.d.ts.map +1 -1
- package/dist/vesk-plugin.js +152 -0
- package/dist/vsk-tsx.d.ts +62 -0
- package/dist/vsk-tsx.d.ts.map +1 -1
- package/dist/vsk-tsx.js +844 -362
- package/package.json +2 -1
- package/dist/package.json +0 -15
package/dist/server-utils.js
CHANGED
|
@@ -134,31 +134,264 @@ export function isStatic(body) {
|
|
|
134
134
|
}
|
|
135
135
|
export function escapeHtml(str) {
|
|
136
136
|
return str
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
.
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
.split('&').join('&')
|
|
138
|
+
.split('<').join('<')
|
|
139
|
+
.split('>').join('>')
|
|
140
|
+
.split('"').join('"')
|
|
141
|
+
.split("'").join(''');
|
|
142
|
+
}
|
|
143
|
+
/** Escapes double quotes for embedding text inside a double-quoted attribute. */
|
|
144
|
+
export function quoteAttr(str) {
|
|
145
|
+
return str.split('"').join('"');
|
|
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
|
-
|
|
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,
|
|
518
|
+
__csrfSecrets.set(host, randomToken(32));
|
|
188
519
|
}
|
|
189
520
|
return __csrfSecrets.get(host);
|
|
190
521
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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,
|
|
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
|
|
249
|
-
|
|
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
|
|
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
|
-
...(
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return ips[0]
|
|
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
|
-
|
|
466
|
-
|
|
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
|
|
901
|
+
return 'http';
|
|
476
902
|
}
|
|
477
903
|
export function applyTrustProxy(ctx, trustProxy) {
|
|
478
904
|
if (!ctx || !trustProxy)
|
package/dist/ssr-store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
package/dist/ssr-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-store.d.ts","sourceRoot":"","sources":["../src/ssr-store.ts"],"names":[],"mappings":"
|
|
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) {
|