tw-framework 2.0.0 → 2.0.1

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.
@@ -6908,20 +6908,39 @@ function sha12(data) {
6908
6908
  async function md52(data) {
6909
6909
  return hashWithWebCrypto("md5", data);
6910
6910
  }
6911
+ function webcryptoHashName(algorithm) {
6912
+ const key2 = String(algorithm).toLowerCase().replace(/[-_]/g, "");
6913
+ const map2 = {
6914
+ md5: "MD5",
6915
+ sha1: "SHA-1",
6916
+ sha224: "SHA-224",
6917
+ sha256: "SHA-256",
6918
+ sha384: "SHA-384",
6919
+ sha512: "SHA-512"
6920
+ };
6921
+ return map2[key2] ?? algorithm;
6922
+ }
6911
6923
  async function hmac2(data, key2, algorithm = "sha256") {
6912
6924
  const encoder2 = new TextEncoder();
6913
6925
  const keyData = encoder2.encode(key2);
6914
6926
  const messageData = encoder2.encode(data);
6915
- const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: algorithm }, false, ["sign"]);
6927
+ const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: webcryptoHashName(algorithm) }, false, ["sign"]);
6916
6928
  const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
6917
6929
  const hashArray = Array.from(new Uint8Array(signature));
6918
6930
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
6919
6931
  }
6932
+ async function verifyHmac(data, key2, signature, algorithm = "sha256") {
6933
+ const expected = await hmac2(data, key2, algorithm);
6934
+ if (expected.length !== signature.length) return false;
6935
+ let diff2 = 0;
6936
+ for (let i = 0; i < expected.length; i++) diff2 |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
6937
+ return diff2 === 0;
6938
+ }
6920
6939
  async function pbkdf2(password, salt, iterations = 1e5, keyLength = 256) {
6921
6940
  const encoder2 = new TextEncoder();
6922
6941
  const keyMaterial = await crypto.subtle.importKey("raw", encoder2.encode(password), "PBKDF2", false, ["deriveBits"]);
6923
6942
  const derivedBits = await crypto.subtle.deriveBits(
6924
- { name: "PBKDF2", salt: encoder2.encode(salt), iterations, hash: "sha256" },
6943
+ { name: "PBKDF2", salt: encoder2.encode(salt), iterations, hash: "SHA-256" },
6925
6944
  keyMaterial,
6926
6945
  keyLength
6927
6946
  );
@@ -6966,9 +6985,16 @@ async function decryptString(encryptedData, password, salt) {
6966
6985
  const key2 = await deriveKey(password, salt);
6967
6986
  return decryptAES(encrypted, key2, iv);
6968
6987
  }
6988
+ async function generateRSASigningKeyPair(bits = 2048) {
6989
+ return crypto.subtle.generateKey(
6990
+ { name: "RSASSA-PKCS1-v1_5", modulusLength: bits, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
6991
+ true,
6992
+ ["sign", "verify"]
6993
+ );
6994
+ }
6969
6995
  async function generateRSAKeyPair(bits = 2048) {
6970
6996
  return crypto.subtle.generateKey(
6971
- { name: "RSA-OAEP", modulusLength: bits, publicExponent: new Uint8Array([1, 0, 1]), hash: "sha256" },
6997
+ { name: "RSA-OAEP", modulusLength: bits, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
6972
6998
  true,
6973
6999
  ["encrypt", "decrypt"]
6974
7000
  );
@@ -8416,36 +8442,6 @@ var init_etag = __esm({
8416
8442
  });
8417
8443
 
8418
8444
  // ../../packages/shared/tw/hashing/hmac.ts
8419
- function bufferToHex2(buffer) {
8420
- const bytes = new Uint8Array(buffer);
8421
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
8422
- }
8423
- function timingSafeEqual2(a, b) {
8424
- if (a.length !== b.length) return false;
8425
- let result = 0;
8426
- for (let i = 0; i < a.length; i++) {
8427
- result |= a.charCodeAt(i) ^ b.charCodeAt(i);
8428
- }
8429
- return result === 0;
8430
- }
8431
- async function hmac3(key2, message, algorithm = "SHA-256") {
8432
- const encoder2 = new TextEncoder();
8433
- const keyData = encoder2.encode(key2);
8434
- const messageData = encoder2.encode(message);
8435
- const cryptoKey = await crypto.subtle.importKey(
8436
- "raw",
8437
- keyData,
8438
- { name: "HMAC", hash: algorithm },
8439
- false,
8440
- ["sign"]
8441
- );
8442
- const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
8443
- return bufferToHex2(signature);
8444
- }
8445
- async function verifyHmac(key2, message, signature, algorithm = "SHA-256") {
8446
- const computed6 = await hmac3(key2, message, algorithm);
8447
- return timingSafeEqual2(computed6, signature);
8448
- }
8449
8445
  var init_hmac = __esm({
8450
8446
  "../../packages/shared/tw/hashing/hmac.ts"() {
8451
8447
  }
@@ -9110,6 +9106,7 @@ __export(tw_exports, {
9110
9106
  generatePIN: () => generatePIN,
9111
9107
  generateProofOfKnowledge: () => generateProofOfKnowledge,
9112
9108
  generateRSAKeyPair: () => generateRSAKeyPair,
9109
+ generateRSASigningKeyPair: () => generateRSASigningKeyPair,
9113
9110
  generateSecret: () => generateSecret,
9114
9111
  generateTOTP: () => generateTOTP,
9115
9112
  geolocation: () => geolocation,
@@ -9615,6 +9612,7 @@ __export(tw_exports, {
9615
9612
  zip: () => zip,
9616
9613
  zipWith: () => zipWith
9617
9614
  });
9615
+ import { createHmac } from "node:crypto";
9618
9616
  function generateETag(data) {
9619
9617
  let hash2 = 0;
9620
9618
  for (let i = 0; i < data.length; i++) {
@@ -9640,19 +9638,14 @@ function merkleRoot(hashes) {
9640
9638
  return level[0];
9641
9639
  }
9642
9640
  function hmacSign(data, key2) {
9643
- let h1 = 0, h22 = 0;
9644
- for (let i = 0; i < data.length; i++) {
9645
- h1 = (h1 << 5) - h1 + data.charCodeAt(i);
9646
- h1 |= 0;
9647
- }
9648
- for (let i = 0; i < key2.length; i++) {
9649
- h22 = (h22 << 5) - h22 + key2.charCodeAt(i);
9650
- h22 |= 0;
9651
- }
9652
- return (h1 ^ h22).toString(16);
9641
+ return createHmac("sha256", key2).update(data).digest("hex");
9653
9642
  }
9654
9643
  function hmacVerify(data, key2, signature) {
9655
- return hmacSign(data, key2) === signature;
9644
+ const expected = hmacSign(data, key2);
9645
+ if (expected.length !== signature.length) return false;
9646
+ let diff2 = 0;
9647
+ for (let i = 0; i < expected.length; i++) diff2 |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
9648
+ return diff2 === 0;
9656
9649
  }
9657
9650
  var HTML_TAGS;
9658
9651
  var init_tw = __esm({
@@ -9809,6 +9802,9 @@ function optimizedImageUrl(src, opts = {}) {
9809
9802
  if (opts.quality) p.set("q", String(opts.quality));
9810
9803
  p.set("f", opts.format ?? "auto");
9811
9804
  const qs = p.toString();
9805
+ if (/^https?:\/\//i.test(src)) {
9806
+ return `/_tw/img/_remote?src=${encodeURIComponent(src)}&${qs}`;
9807
+ }
9812
9808
  return `/_tw/img${src.startsWith("/") ? src : "/" + src}?${qs}`;
9813
9809
  }
9814
9810
  function imageAttributesFor(opts, config = DEFAULT_IMAGE_CONFIG) {
@@ -9816,7 +9812,7 @@ function imageAttributesFor(opts, config = DEFAULT_IMAGE_CONFIG) {
9816
9812
  const quality = opts.quality ?? config.quality;
9817
9813
  const format = config.formats[0] ?? "webp";
9818
9814
  const isRemote = /^https?:\/\//i.test(opts.src);
9819
- const src = isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}&w=${opts.width ?? ""}&h=${opts.height ?? ""}&q=${quality}&f=auto` : optimizedImageUrl(opts.src, { width: opts.width, height: opts.height, quality });
9815
+ const src = isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}${opts.width ? `&w=${opts.width}` : ""}${opts.height ? `&h=${opts.height}` : ""}&q=${quality}&f=auto` : optimizedImageUrl(opts.src, { width: opts.width, height: opts.height, quality });
9820
9816
  const widths = config.breakpoints.filter((bp) => width === 0 || bp <= width * 2);
9821
9817
  const srcset = widths.map(
9822
9818
  (bp) => isRemote ? `/_tw/img/_remote?src=${encodeURIComponent(opts.src)}&w=${bp}&q=${quality}&f=auto ${bp}w` : `${optimizedImageUrl(opts.src, { width: bp, quality })} ${bp}w`
@@ -9996,7 +9992,7 @@ function createImageHandler(options) {
9996
9992
  }
9997
9993
  if (!warnedNoSharp) {
9998
9994
  warnedNoSharp = true;
9999
- console.warn(" [tw/image] sharp not installed \u2014 serving original images (bun add sharp to enable resizing/WebP/AVIF)");
9995
+ console.warn(" [tw/image] sharp not installed \u2014 using the built-in converter (WebP/AVIF); install sharp for faster resizing");
10000
9996
  }
10001
9997
  return { data, contentType: "", viaSharp: false };
10002
9998
  }
@@ -10788,6 +10784,18 @@ function findGlobalError(rootNode2) {
10788
10784
  function findRootError(rootNode2) {
10789
10785
  return (rootNode2.files || []).find((f) => f.type === "error");
10790
10786
  }
10787
+ function findNearestNotFound(rootNode2, pathParts) {
10788
+ let found = (rootNode2.files || []).find((f) => f.type === "not-found");
10789
+ let node = rootNode2;
10790
+ for (const part of pathParts) {
10791
+ const child = (node.children || []).find((c) => c.segment === part);
10792
+ if (!child) break;
10793
+ const f = (child.files || []).find((x) => x.type === "not-found");
10794
+ if (f) found = f;
10795
+ node = child;
10796
+ }
10797
+ return found;
10798
+ }
10791
10799
  function findRootNotFound(rootNode2) {
10792
10800
  return rootNode2.files.find((f) => f.type === "not-found");
10793
10801
  }
@@ -228157,6 +228165,10 @@ function parseTWSelector(cursor, errors, state, parseBody) {
228157
228165
  while (!cursor.done) {
228158
228166
  const ct = cursor.peek();
228159
228167
  if (!ct) break;
228168
+ if (ct.type === "IDENT" && ct.value === "on") {
228169
+ const after2 = cursor.peek(1);
228170
+ if (after2 && after2.type === "COLON") break;
228171
+ }
228160
228172
  if (ct.type === "IDENT" && !nextPrevMinus && nextClass.length > 0 && (CLASS_BOUNDARY_BOOLEAN_ATTRS.has(ct.value) || inlineAttrStartsAt(cursor))) {
228161
228173
  break;
228162
228174
  }
@@ -228447,7 +228459,9 @@ function parseTWSelector(cursor, errors, state, parseBody) {
228447
228459
  const first = cursor.peek();
228448
228460
  const second = cursor.peek(1);
228449
228461
  const CONTROL_FLOW = /* @__PURE__ */ new Set(["for", "if", "while", "switch", "else", "try", "catch"]);
228450
- const isAttrBlock = first && (first.type === "IDENT" || first.type === "KEYWORD") && !CONTROL_FLOW.has(first.value) && HTML_ATTRS_ONLY.has(first.value) && second && (second.type === "STRING" || second.type === "NUMBER" || second.type === "IDENT" || second.type === "KEYWORD");
228462
+ const third = cursor.peek(2);
228463
+ const isPropBlock = second && second.type === "ASSIGN" && third && third.type === "LBRACE";
228464
+ const isAttrBlock = first && (first.type === "IDENT" || first.type === "KEYWORD") && !CONTROL_FLOW.has(first.value) && (HTML_ATTRS_ONLY.has(first.value) || isPropBlock) && second && (second.type === "STRING" || second.type === "NUMBER" || second.type === "IDENT" || second.type === "KEYWORD" || second.type === "ASSIGN");
228451
228465
  if (isAttrBlock) {
228452
228466
  while (!cursor.done) {
228453
228467
  const t3 = cursor.peek();
@@ -228455,6 +228469,34 @@ function parseTWSelector(cursor, errors, state, parseBody) {
228455
228469
  if (t3.type === "IDENT" || t3.type === "KEYWORD") {
228456
228470
  const attrName = t3.value;
228457
228471
  cursor.advance();
228472
+ if (cursor.peek() && (cursor.peek().type === "ASSIGN" || cursor.peek().type === "EQUALS")) {
228473
+ cursor.advance();
228474
+ cursor.skipWhitespace();
228475
+ if (cursor.peek() && cursor.peek().type === "LBRACE") {
228476
+ cursor.advance();
228477
+ let expr = "";
228478
+ let depth = 1;
228479
+ let prevWS = false;
228480
+ while (!cursor.done) {
228481
+ const tk = cursor.peek();
228482
+ if (!tk || tk.type === "EOF") break;
228483
+ if (tk.type === "LBRACE") depth++;
228484
+ else if (tk.type === "RBRACE") {
228485
+ depth--;
228486
+ if (depth === 0) {
228487
+ cursor.advance();
228488
+ break;
228489
+ }
228490
+ }
228491
+ if (expr && prevWS) expr += " ";
228492
+ expr += tk.raw ?? tk.value;
228493
+ prevWS = !!tk.followedByWhitespace;
228494
+ cursor.advance();
228495
+ }
228496
+ el.attrs.push(createAttribute(attrName, expr.trim(), t3.pos.line, t3.pos.col, true));
228497
+ continue;
228498
+ }
228499
+ }
228458
228500
  const vToken = cursor.peek();
228459
228501
  if (vToken && (vToken.type === "STRING" || vToken.type === "NUMBER")) {
228460
228502
  cursor.advance();
@@ -237200,13 +237242,17 @@ var init_types4 = __esm({
237200
237242
  var TW_VERSION, TW_GENERATOR_META;
237201
237243
  var init_version = __esm({
237202
237244
  "../../packages/compiler/tw/codegen/version.ts"() {
237203
- TW_VERSION = "2.0.0";
237245
+ TW_VERSION = "2.0.1";
237204
237246
  TW_GENERATOR_META = `<meta name="generator" content="TW Framework ${TW_VERSION}">`;
237205
237247
  }
237206
237248
  });
237207
237249
 
237208
237250
  // ../../packages/link/tw/index.ts
237251
+ function isDangerousHref(href) {
237252
+ return DANGEROUS_SCHEME.test(href.replace(/[\u0000-\u0020]/g, ""));
237253
+ }
237209
237254
  function isExternalHref(href) {
237255
+ if (isDangerousHref(href)) return false;
237210
237256
  return /^[a-z][a-z0-9+.-]*:/i.test(href) || // any scheme: http(s), mailto, tel, ...
237211
237257
  href.startsWith("//");
237212
237258
  }
@@ -237215,7 +237261,8 @@ function isWebExternal(href) {
237215
237261
  return true;
237216
237262
  }
237217
237263
  function linkAttributesFor(opts) {
237218
- const href = (opts.href ?? "").trim();
237264
+ const rawHref = (opts.href ?? "").trim();
237265
+ const href = isDangerousHref(rawHref) ? "#" : rawHref;
237219
237266
  const external = opts.external === true || isExternalHref(href);
237220
237267
  const attrs = {};
237221
237268
  if (external) {
@@ -237244,8 +237291,10 @@ function linkAttributesFor(opts) {
237244
237291
  if (opts.id) attrs["id"] = opts.id;
237245
237292
  return { href, external, attrs };
237246
237293
  }
237294
+ var DANGEROUS_SCHEME;
237247
237295
  var init_tw3 = __esm({
237248
237296
  "../../packages/link/tw/index.ts"() {
237297
+ DANGEROUS_SCHEME = /^(javascript|data|vbscript):/i;
237249
237298
  }
237250
237299
  });
237251
237300
 
@@ -237758,12 +237807,31 @@ var init_interpolate = __esm({
237758
237807
  });
237759
237808
 
237760
237809
  // ../../packages/compiler/tw/eval/evaluate.ts
237810
+ function setEvalBindings(bindings) {
237811
+ extraBindings = bindings ?? {};
237812
+ }
237761
237813
  function evaluate(expr, vars, sandbox) {
237762
237814
  if (!expr) return "";
237763
237815
  const sb = sandbox ?? defaultSandbox;
237764
237816
  if (vars[expr] !== void 0) {
237765
237817
  return vars[expr];
237766
237818
  }
237819
+ if (expr.startsWith("(") && expr.endsWith(")")) {
237820
+ let d = 0;
237821
+ let wraps = true;
237822
+ for (let i = 0; i < expr.length; i++) {
237823
+ const ch = expr[i];
237824
+ if (ch === "(") d++;
237825
+ else if (ch === ")") {
237826
+ d--;
237827
+ if (d === 0 && i < expr.length - 1) {
237828
+ wraps = false;
237829
+ break;
237830
+ }
237831
+ }
237832
+ }
237833
+ if (wraps && d === 0) return evaluate(expr.slice(1, -1).trim(), vars, sb);
237834
+ }
237767
237835
  if (/^-?\d+(\.\d+)?$/.test(expr)) return expr;
237768
237836
  if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
237769
237837
  return expr.slice(1, -1);
@@ -237771,11 +237839,54 @@ function evaluate(expr, vars, sandbox) {
237771
237839
  if (expr === "true") return "true";
237772
237840
  if (expr === "false") return "false";
237773
237841
  if (expr === "null" || expr === "undefined") return "";
237774
- const ternMatch = expr.match(/^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/);
237775
- if (ternMatch) {
237776
- const cond = isTruthy(resolve2(ternMatch[1].trim(), vars, sb));
237777
- return cond ? resolve2(ternMatch[2].trim(), vars, sb) : resolve2(ternMatch[3].trim(), vars, sb);
237842
+ {
237843
+ let depth = 0;
237844
+ let pending2 = 0;
237845
+ let q = -1;
237846
+ let quote = "";
237847
+ for (let i = 0; i < expr.length; i++) {
237848
+ const ch = expr[i];
237849
+ if (quote) {
237850
+ if (ch === "\\") {
237851
+ i++;
237852
+ continue;
237853
+ }
237854
+ if (ch === quote) quote = "";
237855
+ continue;
237856
+ }
237857
+ if (ch === "'" || ch === '"' || ch === "`") {
237858
+ quote = ch;
237859
+ continue;
237860
+ }
237861
+ if (ch === "(" || ch === "[" || ch === "{") {
237862
+ depth++;
237863
+ continue;
237864
+ }
237865
+ if (ch === ")" || ch === "]" || ch === "}") {
237866
+ depth--;
237867
+ continue;
237868
+ }
237869
+ if (depth !== 0) continue;
237870
+ if (ch === "?") {
237871
+ if (pending2 === 0) q = i;
237872
+ pending2++;
237873
+ continue;
237874
+ }
237875
+ if (ch === ":" && pending2 > 0) {
237876
+ pending2--;
237877
+ if (pending2 > 0) continue;
237878
+ const cond = expr.slice(0, q).trim();
237879
+ const cons = expr.slice(q + 1, i).trim();
237880
+ const alt = expr.slice(i + 1).trim();
237881
+ if (cond && cons && alt) {
237882
+ return isTruthy(resolve2(cond, vars, sb)) ? resolve2(cons, vars, sb) : resolve2(alt, vars, sb);
237883
+ }
237884
+ break;
237885
+ }
237886
+ }
237778
237887
  }
237888
+ const dyn = evalWithVars(expr, vars);
237889
+ if (dyn !== null) return dyn;
237779
237890
  const orParts = splitLogical(expr, "||");
237780
237891
  if (orParts.length > 1) {
237781
237892
  for (const part of orParts) {
@@ -237852,7 +237963,7 @@ function evaluate(expr, vars, sandbox) {
237852
237963
  return current2 !== void 0 && current2 !== null ? String(current2) : "";
237853
237964
  }
237854
237965
  const fnMatch = expr.match(/^(\w+)\((.*)\)$/);
237855
- if (fnMatch) {
237966
+ if (fnMatch && isBuiltinName(fnMatch[1])) {
237856
237967
  const fn = fnMatch[1];
237857
237968
  const arg = resolve2(fnMatch[2].trim(), vars, sb);
237858
237969
  return callBuiltin(fn, arg, sb);
@@ -237871,6 +237982,38 @@ function evaluate(expr, vars, sandbox) {
237871
237982
  }
237872
237983
  return expr;
237873
237984
  }
237985
+ function evalWithVars(expr, vars) {
237986
+ if (!expr || /[`\\]/.test(expr)) return null;
237987
+ if (!/^[\w\s.$[\]{}()'"`,+\-*/%?:<>=!&|]*$/.test(expr)) return null;
237988
+ for (const m of expr.matchAll(/\.([A-Za-z_$][\w$]*)\s*\(/g)) {
237989
+ if (!SAFE_MEMBERS.has(m[1])) return null;
237990
+ }
237991
+ const names = [];
237992
+ const vals = [];
237993
+ for (const k of Object.keys(vars)) {
237994
+ if (!/^[A-Za-z_$][\w$]*$/.test(k)) continue;
237995
+ if (k in extraBindings) continue;
237996
+ names.push(k);
237997
+ try {
237998
+ vals.push(JSON.parse(vars[k]));
237999
+ } catch {
238000
+ vals.push(vars[k]);
238001
+ }
238002
+ }
238003
+ for (const k of Object.keys(extraBindings)) {
238004
+ if (!/^[A-Za-z_$][\w$]*$/.test(k)) continue;
238005
+ names.push(k);
238006
+ vals.push(extraBindings[k]);
238007
+ }
238008
+ try {
238009
+ const fn = new Function(...names, `"use strict"; return (${expr});`);
238010
+ const out = fn(...vals);
238011
+ if (out === void 0 || out === null) return null;
238012
+ return typeof out === "object" ? JSON.stringify(out) : String(out);
238013
+ } catch {
238014
+ return null;
238015
+ }
238016
+ }
237874
238017
  function resolve2(token, vars, sandbox) {
237875
238018
  const trimmed = token.trim();
237876
238019
  if (!trimmed) return "";
@@ -238039,7 +238182,7 @@ function coerce(value, targetType) {
238039
238182
  return value;
238040
238183
  }
238041
238184
  }
238042
- var defaultSandbox;
238185
+ var defaultSandbox, extraBindings, SAFE_MEMBERS;
238043
238186
  var init_evaluate = __esm({
238044
238187
  "../../packages/compiler/tw/eval/evaluate.ts"() {
238045
238188
  init_sandbox();
@@ -238047,6 +238190,64 @@ var init_evaluate = __esm({
238047
238190
  init_builtins();
238048
238191
  init_interpolate();
238049
238192
  defaultSandbox = new Sandbox();
238193
+ extraBindings = {};
238194
+ SAFE_MEMBERS = /* @__PURE__ */ new Set([
238195
+ "length",
238196
+ "toUpperCase",
238197
+ "toLowerCase",
238198
+ "trim",
238199
+ "trimStart",
238200
+ "trimEnd",
238201
+ "slice",
238202
+ "substring",
238203
+ "substr",
238204
+ "charAt",
238205
+ "at",
238206
+ "includes",
238207
+ "indexOf",
238208
+ "lastIndexOf",
238209
+ "startsWith",
238210
+ "endsWith",
238211
+ "replace",
238212
+ "replaceAll",
238213
+ "split",
238214
+ "join",
238215
+ "concat",
238216
+ "repeat",
238217
+ "padStart",
238218
+ "padEnd",
238219
+ "toString",
238220
+ "toFixed",
238221
+ "map",
238222
+ "filter",
238223
+ "find",
238224
+ "some",
238225
+ "every",
238226
+ "reduce",
238227
+ "sort",
238228
+ "reverse",
238229
+ // Static helpers on the built-ins (JSON.stringify, Object.keys, Math.round).
238230
+ "stringify",
238231
+ "parse",
238232
+ "keys",
238233
+ "values",
238234
+ "entries",
238235
+ "from",
238236
+ "isArray",
238237
+ "isInteger",
238238
+ "isFinite",
238239
+ "abs",
238240
+ "ceil",
238241
+ "floor",
238242
+ "round",
238243
+ "max",
238244
+ "min",
238245
+ "pow",
238246
+ "sqrt",
238247
+ "random",
238248
+ "now",
238249
+ "toISOString"
238250
+ ]);
238050
238251
  }
238051
238252
  });
238052
238253
 
@@ -238201,9 +238402,78 @@ function callBuiltin(fn, arg, sandbox) {
238201
238402
  function applyFilter(value, filter, args, vars, sandbox) {
238202
238403
  return callBuiltin(filter, value, sandbox);
238203
238404
  }
238405
+ function isBuiltinName(fn) {
238406
+ return BUILTIN_NAMES.has(fn);
238407
+ }
238408
+ var BUILTIN_NAMES;
238204
238409
  var init_builtins = __esm({
238205
238410
  "../../packages/compiler/tw/eval/builtins.ts"() {
238206
238411
  init_evaluate();
238412
+ BUILTIN_NAMES = /* @__PURE__ */ new Set([
238413
+ "Boolean",
238414
+ "Number",
238415
+ "String",
238416
+ "abs",
238417
+ "base64decode",
238418
+ "base64encode",
238419
+ "bool",
238420
+ "camelCase",
238421
+ "capitalize",
238422
+ "ceil",
238423
+ "cos",
238424
+ "date",
238425
+ "decodeURI",
238426
+ "encodeURI",
238427
+ "endsWith",
238428
+ "exp",
238429
+ "first",
238430
+ "float",
238431
+ "floor",
238432
+ "includes",
238433
+ "indexOf",
238434
+ "int",
238435
+ "join",
238436
+ "json",
238437
+ "kebabCase",
238438
+ "last",
238439
+ "len",
238440
+ "length",
238441
+ "log",
238442
+ "lower",
238443
+ "lowercase",
238444
+ "match",
238445
+ "max",
238446
+ "min",
238447
+ "now",
238448
+ "num",
238449
+ "padEnd",
238450
+ "padStart",
238451
+ "parseFloat",
238452
+ "parseInt",
238453
+ "parseJSON",
238454
+ "pow",
238455
+ "random",
238456
+ "repeat",
238457
+ "replace",
238458
+ "reverse",
238459
+ "round",
238460
+ "sin",
238461
+ "size",
238462
+ "slice",
238463
+ "snakeCase",
238464
+ "split",
238465
+ "sqrt",
238466
+ "startsWith",
238467
+ "str",
238468
+ "stringify",
238469
+ "tan",
238470
+ "test",
238471
+ "time",
238472
+ "title",
238473
+ "trim",
238474
+ "upper",
238475
+ "uppercase"
238476
+ ]);
238207
238477
  }
238208
238478
  });
238209
238479
 
@@ -239252,16 +239522,61 @@ function addScope(css, scopeId) {
239252
239522
  out += prelude;
239253
239523
  return out;
239254
239524
  }
239255
- function interpolate2(expr, vars) {
239256
- const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
239257
- const out = masked.replace(/\{([^}]+)\}/g, (_, name) => {
239258
- const key2 = name.trim();
239525
+ function interpolateBraces(input, vars) {
239526
+ let out = "";
239527
+ let i = 0;
239528
+ while (i < input.length) {
239529
+ const ch = input[i];
239530
+ if (ch !== "{") {
239531
+ out += ch;
239532
+ i++;
239533
+ continue;
239534
+ }
239535
+ let depth = 1;
239536
+ let j = i + 1;
239537
+ let quote = "";
239538
+ while (j < input.length && depth > 0) {
239539
+ const c = input[j];
239540
+ if (quote) {
239541
+ if (c === "\\") {
239542
+ j += 2;
239543
+ continue;
239544
+ }
239545
+ if (c === quote) quote = "";
239546
+ j++;
239547
+ continue;
239548
+ }
239549
+ if (c === "'" || c === '"' || c === "`") {
239550
+ quote = c;
239551
+ j++;
239552
+ continue;
239553
+ }
239554
+ if (c === "{") depth++;
239555
+ else if (c === "}") {
239556
+ depth--;
239557
+ if (depth === 0) break;
239558
+ }
239559
+ j++;
239560
+ }
239561
+ if (depth !== 0) {
239562
+ out += ch;
239563
+ i++;
239564
+ continue;
239565
+ }
239566
+ const key2 = input.slice(i + 1, j).trim();
239259
239567
  if (/^[a-zA-Z_$][\w$]*$/.test(key2)) {
239260
- return vars[key2] ?? "";
239568
+ out += vars[key2] ?? "";
239569
+ } else {
239570
+ const val = evaluate(key2, vars);
239571
+ out += val === null ? "" : val;
239261
239572
  }
239262
- const val = evaluate(key2, vars);
239263
- return val === null ? "" : val;
239264
- });
239573
+ i = j + 1;
239574
+ }
239575
+ return out;
239576
+ }
239577
+ function interpolate2(expr, vars) {
239578
+ const masked = expr.replace(/\\\{/g, "O").replace(/\\\}/g, "C");
239579
+ const out = interpolateBraces(masked, vars);
239265
239580
  return out.replace(/\u0001O/g, "{").replace(/\u0001C/g, "}");
239266
239581
  }
239267
239582
  function isTruthy2(val) {
@@ -239740,7 +240055,21 @@ function renderBindings(bindings, ctx) {
239740
240055
  if (bindings.length === 0) return "";
239741
240056
  const parts = [];
239742
240057
  for (const binding of bindings) {
239743
- parts.push(`data-tw-bind-${binding.property}="${escapeAttr3(String(binding.expression ?? ""))}"`);
240058
+ const expr = String(binding.expression ?? "");
240059
+ let initial = "";
240060
+ try {
240061
+ if (/^[A-Za-z_$][\w$]*$/.test(expr)) {
240062
+ const v = ctx.stateVars[expr];
240063
+ if (v !== void 0 && v !== null) initial = String(v);
240064
+ } else if (expr) {
240065
+ initial = interpolate2("{" + expr + "}", ctx.stateVars);
240066
+ }
240067
+ } catch {
240068
+ }
240069
+ if (initial && !/[{}]/.test(initial)) {
240070
+ parts.push(`${binding.property}="${escapeAttr3(initial)}"`);
240071
+ }
240072
+ parts.push(`data-tw-bind-${binding.property}="${escapeAttr3(expr)}"`);
239744
240073
  }
239745
240074
  return parts.join(" ");
239746
240075
  }
@@ -239855,8 +240184,13 @@ function generateComponent(comp, ctx) {
239855
240184
  const savedStateVars = { ...ctx.stateVars };
239856
240185
  for (const prop of comp.props) {
239857
240186
  if (typeof prop.value === "string") {
239858
- const resolved = interpolate2(prop.value, ctx.stateVars);
239859
- ctx.stateVars[prop.name] = resolved;
240187
+ const raw = String(prop.value);
240188
+ const head = raw.split(/[.[]/)[0];
240189
+ if (!/[{}]/.test(raw) && Object.prototype.hasOwnProperty.call(ctx.stateVars, head)) {
240190
+ ctx.stateVars[prop.name] = interpolate2("{" + raw + "}", ctx.stateVars);
240191
+ } else {
240192
+ ctx.stateVars[prop.name] = interpolate2(raw, ctx.stateVars);
240193
+ }
239860
240194
  }
239861
240195
  }
239862
240196
  const componentProgram = componentRegistry.get(comp.name);
@@ -239950,6 +240284,14 @@ function generateFor(node, ctx) {
239950
240284
  }
239951
240285
  }
239952
240286
  }
240287
+ if (items.length === 0 && !/^\s*\[/.test(iterableStr)) {
240288
+ try {
240289
+ const evaluated = evaluate(String(node.iterable ?? "").replace(/^\{/, "").replace(/\}$/, "").trim(), ctx.stateVars);
240290
+ const parsed = JSON.parse(evaluated);
240291
+ if (Array.isArray(parsed)) items = parsed;
240292
+ } catch {
240293
+ }
240294
+ }
239953
240295
  for (let i = 0; i < items.length; i++) {
239954
240296
  const childCtx = { ...ctx };
239955
240297
  childCtx.stateVars = { ...ctx.stateVars };
@@ -243756,16 +244098,22 @@ function generateWithLayoutChain(layoutPrograms, pageProgram, stateVars) {
243756
244098
  }
243757
244099
  };
243758
244100
  let pageTitle = "TW Page";
244101
+ let pageDescription = "";
243759
244102
  for (const dir of pageProgram.directives || []) {
243760
- if (dir.type === "PageDirective" && dir.key === "title") {
243761
- const t3 = dir.value;
243762
- if (t3 != null && String(t3) !== "") pageTitle = String(t3);
244103
+ if (dir.type === "PageDirective") {
244104
+ if (dir.key === "title") {
244105
+ const t3 = dir.value;
244106
+ if (t3 != null && String(t3) !== "") pageTitle = String(t3);
244107
+ } else if (dir.key === "description") {
244108
+ const d = dir.value;
244109
+ if (d != null) pageDescription = String(d);
244110
+ }
243763
244111
  }
243764
244112
  }
243765
244113
  const pageCtx = createContext("ssr");
243766
244114
  seedCtx(pageCtx);
243767
244115
  try {
243768
- pageCtx.stateVars["page"] = JSON.stringify({ title: pageTitle });
244116
+ pageCtx.stateVars["page"] = JSON.stringify({ title: pageTitle, description: pageDescription });
243769
244117
  } catch {
243770
244118
  }
243771
244119
  {
@@ -243921,6 +244269,10 @@ function generate(program, stateVars, genOpts2) {
243921
244269
  const startTime = performance.now();
243922
244270
  const ctx = createContext("ssr");
243923
244271
  if (stateVars) ctx.stateVars = stateVars;
244272
+ if (genOpts2?.imports) {
244273
+ ctx.imports = genOpts2.imports;
244274
+ setEvalBindings(genOpts2.imports);
244275
+ } else setEvalBindings({});
243924
244276
  if (genOpts2 && genOpts2.scopedStyles === false) ctx.scopedStyles = false;
243925
244277
  if (genOpts2?.scopeId) ctx.scopeId = genOpts2.scopeId;
243926
244278
  if (genOpts2?.cssPrefix) ctx.cssPrefix = genOpts2.cssPrefix;
@@ -244005,6 +244357,7 @@ var init_vdom = __esm({
244005
244357
  init_types4();
244006
244358
  init_html2();
244007
244359
  init_builtin_components();
244360
+ init_evaluate();
244008
244361
  }
244009
244362
  });
244010
244363
 
@@ -244345,6 +244698,7 @@ var init_codes = __esm({
244345
244698
  TW095: { message: "Invalid for-loop iterable -- use {varName} or a valid comma-separated literal", severity: "error", category: "syntax" },
244346
244699
  TW096: { message: "Unknown state variable in interpolation (renders empty)", severity: "warning", category: "semantic" },
244347
244700
  TW097: { message: "while with a comparison renders its body ONCE at build time (state cannot mutate during SSR) -- use for loops for repetition", severity: "warning", category: "semantic" },
244701
+ TW098: { message: "Interpolation cannot be evaluated at build time -- it renders as its source text", severity: "warning", category: "semantic" },
244348
244702
  // Server blocks (TW099-TW101)
244349
244703
  TW099: { message: "Unknown server modifier (only ':defer' is supported)", severity: "error", category: "syntax" },
244350
244704
  TW100: { message: "Expected '{' after 'server'", severity: "error", category: "syntax" },
@@ -244432,6 +244786,7 @@ function checkUnknownTags(program, filePath, diags) {
244432
244786
  forEachNode(program, (node, ctx) => {
244433
244787
  if (!isElement(node)) return;
244434
244788
  if (node.tag[0] === node.tag[0]?.toUpperCase()) return;
244789
+ if (BUILTIN_COMPONENT_TAGS.has(node.tag)) return;
244435
244790
  const parent = ctx?.parent;
244436
244791
  if (parent && (parent.type === "Element" && typeof parent.tag === "string" && parent.tag[0] === parent.tag[0]?.toUpperCase() || parent.type === "Component")) return;
244437
244792
  if (HTML_ELEMENTS2.has(node.tag.toLowerCase())) return;
@@ -244786,6 +245141,59 @@ function checkUnknownStateVars(program, filePath, diags) {
244786
245141
  "null",
244787
245142
  "undefined"
244788
245143
  ]);
245144
+ const EVALUABLE_METHODS = /* @__PURE__ */ new Set([
245145
+ "length",
245146
+ "toUpperCase",
245147
+ "toLowerCase",
245148
+ "trim",
245149
+ "trimStart",
245150
+ "trimEnd",
245151
+ "slice",
245152
+ "substring",
245153
+ "substr",
245154
+ "charAt",
245155
+ "at",
245156
+ "includes",
245157
+ "indexOf",
245158
+ "lastIndexOf",
245159
+ "startsWith",
245160
+ "endsWith",
245161
+ "replace",
245162
+ "replaceAll",
245163
+ "split",
245164
+ "join",
245165
+ "concat",
245166
+ "repeat",
245167
+ "padStart",
245168
+ "padEnd",
245169
+ "toString",
245170
+ "toFixed",
245171
+ "map",
245172
+ "filter",
245173
+ "find",
245174
+ "some",
245175
+ "every",
245176
+ "reduce",
245177
+ "sort",
245178
+ "reverse",
245179
+ "stringify",
245180
+ "parse",
245181
+ "keys",
245182
+ "values",
245183
+ "entries",
245184
+ "from",
245185
+ "isArray",
245186
+ "round",
245187
+ "floor",
245188
+ "ceil",
245189
+ "abs",
245190
+ "max",
245191
+ "min",
245192
+ "pow",
245193
+ "sqrt",
245194
+ "random",
245195
+ "now"
245196
+ ]);
244789
245197
  for (const node of texts) {
244790
245198
  const text = String(node.value ?? "");
244791
245199
  if (!text.includes("{")) continue;
@@ -244801,6 +245209,22 @@ function checkUnknownStateVars(program, filePath, diags) {
244801
245209
  [`Declare it in the state block, or check the spelling`]
244802
245210
  ));
244803
245211
  }
245212
+ for (const m of text.matchAll(/\{([^{}]+)\}/g)) {
245213
+ const expr = m[1].trim();
245214
+ if (/^[A-Za-z_$][\w$]*$/.test(expr)) continue;
245215
+ for (const c of expr.matchAll(/([A-Za-z_$][\w$]*)\s*\(/g)) {
245216
+ const fn = c[1];
245217
+ if (known.has(fn) || BUILTINS2.has(fn) || EVALUABLE_METHODS.has(fn)) continue;
245218
+ diags.push(createDiagnostic(
245219
+ "TW098",
245220
+ node.line,
245221
+ node.col,
245222
+ filePath,
245223
+ `{${expr}}`,
245224
+ [`Cannot evaluate this at build time -- it renders as its source. Check the name, or simplify the expression`]
245225
+ ));
245226
+ }
245227
+ }
244804
245228
  }
244805
245229
  }
244806
245230
  function checkWhileComparisons(program, filePath, diags) {
@@ -244833,7 +245257,7 @@ function checkDeprecatedImports(program, filePath, diags) {
244833
245257
  }
244834
245258
  }
244835
245259
  }
244836
- var HTML_ELEMENTS2, HTML_ATTRIBUTES2, GLOBAL_ATTRIBUTES2, CSS_PROPERTIES2, CSS_ALIASES2, EVENT_TYPES2, ARIA_ROLES2, ARIA_ATTRIBUTES2, DEPRECATED_IMPORTS;
245260
+ var HTML_ELEMENTS2, HTML_ATTRIBUTES2, GLOBAL_ATTRIBUTES2, CSS_PROPERTIES2, CSS_ALIASES2, EVENT_TYPES2, ARIA_ROLES2, ARIA_ATTRIBUTES2, BUILTIN_COMPONENT_TAGS, DEPRECATED_IMPORTS;
244837
245261
  var init_rules = __esm({
244838
245262
  "../../packages/compiler/tw/diagnostics/rules.ts"() {
244839
245263
  init_visitors();
@@ -245697,6 +246121,7 @@ var init_rules = __esm({
245697
246121
  "aria-valuenow",
245698
246122
  "aria-valuetext"
245699
246123
  ]);
246124
+ BUILTIN_COMPONENT_TAGS = /* @__PURE__ */ new Set(["optImage", "twImage", "twLink", "twImg"]);
245700
246125
  DEPRECATED_IMPORTS = [
245701
246126
  { source: "utils/string/transform", name: "toHex", replacement: "stringToHex", removal: "2.1.0" },
245702
246127
  { source: "utils/string/transform", name: "fromHex", replacement: "hexToString", removal: "2.1.0" },
@@ -256009,7 +256434,8 @@ function genOpts(opts, scopeId) {
256009
256434
  scopedStyles: opts?.compiler?.scopedStyles,
256010
256435
  scopeId,
256011
256436
  cssPrefix: opts?.cssPrefix,
256012
- cssImportPaths: opts?.cssImportPaths
256437
+ cssImportPaths: opts?.cssImportPaths,
256438
+ imports: opts?.imports
256013
256439
  };
256014
256440
  }
256015
256441
  function scopeIdFor(filePath) {
@@ -256283,7 +256709,7 @@ __export(twm_rules_exports, {
256283
256709
  isRuleDsl: () => isRuleDsl,
256284
256710
  parseRules: () => parseRules
256285
256711
  });
256286
- import { createHmac, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
256712
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
256287
256713
  function isRuleDsl(source) {
256288
256714
  const stripped = source.split("\n").map((l) => l.replace(/\/\/.*$/, "").trim()).filter((l) => l.length > 0).join("\n");
256289
256715
  return stripped.startsWith('rule "') || stripped.startsWith("rule '");
@@ -256381,14 +256807,14 @@ function verifyJwt_hs256(token, secret) {
256381
256807
  return false;
256382
256808
  }
256383
256809
  if (header?.alg !== "HS256") return false;
256384
- const expected = createHmac("sha256", secret).update(`${h3}.${p}`).digest();
256810
+ const expected = createHmac2("sha256", secret).update(`${h3}.${p}`).digest();
256385
256811
  let got;
256386
256812
  try {
256387
256813
  got = b64urlDecode(sig);
256388
256814
  } catch {
256389
256815
  return false;
256390
256816
  }
256391
- if (expected.length !== got.length || !timingSafeEqual3(expected, got)) return false;
256817
+ if (expected.length !== got.length || !timingSafeEqual2(expected, got)) return false;
256392
256818
  if (typeof payload?.exp === "number" && payload.exp * 1e3 < Date.now()) return false;
256393
256819
  return true;
256394
256820
  }
@@ -256764,6 +257190,32 @@ var init_twm_rules = __esm({
256764
257190
  });
256765
257191
 
256766
257192
  // ../../packages/runtime/tw/dependency-graph.ts
257193
+ function flushPendingSubs() {
257194
+ if (isFlushing || !pendingSubs) return;
257195
+ isFlushing = true;
257196
+ try {
257197
+ let guard = 0;
257198
+ while (pendingSubs && pendingSubs.size > 0 && guard++ < 1e5) {
257199
+ const batchOf = pendingSubs;
257200
+ pendingSubs = /* @__PURE__ */ new Set();
257201
+ for (const sub of batchOf) {
257202
+ try {
257203
+ sub();
257204
+ } catch (e) {
257205
+ console.error("[TW Reactivity] Signal subscriber error:", e);
257206
+ }
257207
+ }
257208
+ }
257209
+ } finally {
257210
+ isFlushing = false;
257211
+ pendingSubs = null;
257212
+ }
257213
+ }
257214
+ function enqueueSubs(subs) {
257215
+ if (!pendingSubs) pendingSubs = /* @__PURE__ */ new Set();
257216
+ for (const s of subs) pendingSubs.add(s);
257217
+ if (batchDepth === 0) flushPendingSubs();
257218
+ }
256767
257219
  function startBatch() {
256768
257220
  batchDepth++;
256769
257221
  }
@@ -256773,13 +257225,7 @@ function endBatch() {
256773
257225
  batchDepth = 0;
256774
257226
  const effects = Array.from(batchedEffects);
256775
257227
  batchedEffects.clear();
256776
- for (const eff of effects) {
256777
- try {
256778
- eff();
256779
- } catch (e) {
256780
- console.error("[TW Reactivity] Batch effect error:", e);
256781
- }
256782
- }
257228
+ enqueueSubs(effects);
256783
257229
  }
256784
257230
  }
256785
257231
  function batch(fn) {
@@ -256961,12 +257407,14 @@ function isComputed(value) {
256961
257407
  function createScope3() {
256962
257408
  return new ReactiveScope();
256963
257409
  }
256964
- var currentEffect, batchDepth, batchedEffects, SignalImpl, ComputedImpl, watchEffect, currentCleanup, ReactiveScope;
257410
+ var currentEffect, batchDepth, batchedEffects, pendingSubs, isFlushing, SignalImpl, ComputedImpl, watchEffect, currentCleanup, ReactiveScope;
256965
257411
  var init_dependency_graph = __esm({
256966
257412
  "../../packages/runtime/tw/dependency-graph.ts"() {
256967
257413
  currentEffect = null;
256968
257414
  batchDepth = 0;
256969
257415
  batchedEffects = /* @__PURE__ */ new Set();
257416
+ pendingSubs = null;
257417
+ isFlushing = false;
256970
257418
  SignalImpl = class {
256971
257419
  value;
256972
257420
  subscribers = /* @__PURE__ */ new Set();
@@ -256985,19 +257433,9 @@ var init_dependency_graph = __esm({
256985
257433
  if (this.equals(this.value, value)) return;
256986
257434
  this.value = value;
256987
257435
  if (batchDepth > 0) {
256988
- for (const sub of this.subscribers) {
256989
- batchedEffects.add(sub);
256990
- }
256991
- } else {
256992
- const subs = Array.from(this.subscribers);
256993
- for (const sub of subs) {
256994
- try {
256995
- sub();
256996
- } catch (e) {
256997
- console.error("[TW Reactivity] Signal subscriber error:", e);
256998
- }
256999
- }
257436
+ for (const sub of this.subscribers) batchedEffects.add(sub);
257000
257437
  }
257438
+ enqueueSubs(this.subscribers);
257001
257439
  }
257002
257440
  update(fn) {
257003
257441
  this.set(fn(this.value));
@@ -257063,14 +257501,11 @@ var init_dependency_graph = __esm({
257063
257501
  return this.cachedValue;
257064
257502
  }
257065
257503
  notify() {
257066
- const subs = Array.from(this.subscribers);
257067
- for (const sub of subs) {
257068
- try {
257069
- sub();
257070
- } catch (e) {
257071
- console.error("[TW Reactivity] Computed subscriber error:", e);
257072
- }
257504
+ if (batchDepth > 0) {
257505
+ for (const sub of this.subscribers) batchedEffects.add(sub);
257506
+ return;
257073
257507
  }
257508
+ enqueueSubs(this.subscribers);
257074
257509
  }
257075
257510
  set(_value) {
257076
257511
  throw new Error("Cannot set a computed signal");
@@ -260255,10 +260690,12 @@ var init_diff = __esm({
260255
260690
  "../../packages/runtime/tw/diff.ts"() {
260256
260691
  init_vdom2();
260257
260692
  HTML_ESCAPE_MAP = {
260258
- "&": "&",
260259
- "<": "<",
260260
- ">": ">",
260261
- '"': '"',
260693
+ // These four mapped to THEMSELVES, so a <script> tag passed through
260694
+ // untouched -- only ' was actually escaped.
260695
+ "&": "&amp;",
260696
+ "<": "&lt;",
260697
+ ">": "&gt;",
260698
+ '"': "&quot;",
260262
260699
  "'": "&#39;"
260263
260700
  };
260264
260701
  HTML_ESCAPE_REGEX = /[&<>"']/g;
@@ -263788,8 +264225,19 @@ async function hydrateElementById(id, component, root = typeof document !== "und
263788
264225
  );
263789
264226
  }
263790
264227
  function markForHydration(element, componentId, props) {
264228
+ if (!element || typeof componentId !== "string") return;
263791
264229
  element.setAttribute(HYDRATE_ATTR, componentId);
263792
- element.setAttribute("data-tw-props", JSON.stringify(props));
264230
+ let serialised = null;
264231
+ if (props && typeof props === "object") {
264232
+ try {
264233
+ serialised = JSON.stringify(props);
264234
+ } catch {
264235
+ serialised = null;
264236
+ }
264237
+ }
264238
+ if (serialised !== null && serialised !== void 0) {
264239
+ element.setAttribute("data-tw-props", serialised);
264240
+ }
263793
264241
  }
263794
264242
  function unmarkForHydration(element) {
263795
264243
  element.removeAttribute(HYDRATE_ATTR);
@@ -267089,15 +267537,21 @@ function usePathname(opts) {
267089
267537
  function matchPathPattern(path, pattern) {
267090
267538
  const pSeg = path.split("/").filter(Boolean);
267091
267539
  const tSeg = pattern.split("/").filter(Boolean);
267092
- if (pSeg.length !== tSeg.length) return { matches: false, params: {}, score: 0 };
267540
+ const catchAllAt = tSeg.findIndex((t3) => t3.startsWith("[...") || t3.startsWith(":..."));
267541
+ if (catchAllAt === -1) {
267542
+ if (pSeg.length !== tSeg.length) return { matches: false, params: {}, score: 0 };
267543
+ } else if (pSeg.length < tSeg.length) {
267544
+ return { matches: false, params: {}, score: 0 };
267545
+ }
267093
267546
  const params = {};
267094
267547
  let literal = 0;
267095
267548
  for (let i = 0; i < tSeg.length; i++) {
267096
267549
  const t3 = tSeg[i];
267097
267550
  const isParam = t3.startsWith("[") && t3.endsWith("]") || t3.startsWith(":");
267098
267551
  if (isParam) {
267099
- const name = t3.startsWith("[") ? t3.slice(1, -1).replace(/\.\.\.$/, "") : t3.slice(1);
267100
- params[name] = decodeURIComponent(pSeg[i]);
267552
+ const name = t3.startsWith("[") ? t3.slice(1, -1).replace(/^\.\.\./, "") : t3.slice(1);
267553
+ const isCatchAll = t3.startsWith("[...") || t3.startsWith(":...");
267554
+ params[name] = isCatchAll ? pSeg.slice(i).map((seg) => decodeURIComponent(seg)).join("/") : decodeURIComponent(pSeg[i]);
267101
267555
  } else if (t3 !== pSeg[i]) {
267102
267556
  return { matches: false, params: {}, score: 0 };
267103
267557
  } else {
@@ -267912,7 +268366,11 @@ function defineStore(idOrDefinition, maybeDefinition) {
267912
268366
  const getterDeps = Object.values(reactiveState);
267913
268367
  for (const key2 of Object.keys(getters)) {
267914
268368
  const getter = getters[key2];
267915
- getterValues[key2] = computed4(getterDeps, getter);
268369
+ getterValues[key2] = computed4(getterDeps, () => {
268370
+ const snap = {};
268371
+ for (const k of Object.keys(reactiveState)) snap[k] = reactiveState[k].value;
268372
+ return getter.call(snap, snap);
268373
+ });
267916
268374
  }
267917
268375
  const subscribers = /* @__PURE__ */ new Set();
267918
268376
  const actionListeners = /* @__PURE__ */ new Set();
@@ -267955,7 +268413,7 @@ function defineStore(idOrDefinition, maybeDefinition) {
267955
268413
  const fn = actions[key2];
267956
268414
  boundActions[key2] = (...args) => {
267957
268415
  actionListeners.forEach((cb) => cb({ name: key2, args }));
267958
- return fn.call(ctx, stateProxy, ...args);
268416
+ return fn.call(stateProxy, stateProxy, ...args);
267959
268417
  };
267960
268418
  }
267961
268419
  for (const rv of Object.values(reactiveState)) {
@@ -268067,9 +268525,10 @@ var init_store = __esm({
268067
268525
  };
268068
268526
  }
268069
268527
  _notify() {
268528
+ const current2 = this.value ?? this._value;
268070
268529
  for (const l of this._listeners) {
268071
268530
  try {
268072
- l();
268531
+ l(current2);
268073
268532
  } catch {
268074
268533
  }
268075
268534
  }
@@ -269075,7 +269534,8 @@ var init_virtual_list = __esm({
269075
269534
  return this.container.scrollTop;
269076
269535
  }
269077
269536
  getFirstVisibleIndex() {
269078
- return this.visibleRange.start;
269537
+ const last = Math.max(0, this.options?.itemCount ?? 0 - 1);
269538
+ return Math.min(Math.max(this.visibleRange.start, 0), last);
269079
269539
  }
269080
269540
  getLastVisibleIndex() {
269081
269541
  return this.visibleRange.end;
@@ -271924,6 +272384,12 @@ var init_xss_detector = __esm({
271924
272384
  { name: "SQL comment", regex: /--\s/g, severity: "medium", recommendation: "Check for SQL comment injection" },
271925
272385
  { name: "Stacked queries", regex: /;\s*(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)/gi, severity: "high", recommendation: "Prevent stacked queries" },
271926
272386
  { name: "UNION injection", regex: /UNION\s+SELECT/gi, severity: "critical", recommendation: "Filter UNION SELECT from user input" },
272387
+ // Comment-only injections (`admin'--`, `admin'#`, `admin'/*`): a quote
272388
+ // followed by a comment truncates the query and bypasses the login
272389
+ // check, with no OR/UNION for the other patterns to catch.
272390
+ { name: "Comment-terminated injection", regex: /['"`]\s*(--|#|\/\*)/g, severity: "high", recommendation: "A quote followed by a comment truncates the query -- use parameterized queries" },
272391
+ // Parenthesised boolean injection: `') OR ('1'='1`.
272392
+ { name: "Parenthesised boolean injection", regex: /['"`]?\s*\)\s*(OR|AND)\s*\(?\s*['"`]?\s*\d/i, severity: "high", recommendation: "Use parameterized queries instead of building boolean expressions" },
271927
272393
  { name: "Boolean-based injection", regex: /OR\s+1\s*=\s*1/gi, severity: "high", recommendation: "Use parameterized queries to prevent boolean injection" },
271928
272394
  { name: "Time-based injection", regex: /WAITFOR\s+DELAY/gi, severity: "high", recommendation: "Filter WAITFOR DELAY from user input" },
271929
272395
  { name: "DROP statement", regex: /DROP\s+(TABLE|DATABASE)/gi, severity: "critical", recommendation: "Prevent DROP statements in queries" },
@@ -272454,6 +272920,7 @@ var init_logger4 = __esm({
272454
272920
  }
272455
272921
  /** Records an audit event. */
272456
272922
  log(event) {
272923
+ event = { type: "system", actor: "unknown", resource: "unknown", ...event };
272457
272924
  const fullEvent = {
272458
272925
  ...event,
272459
272926
  id: crypto.randomUUID(),
@@ -273470,6 +273937,13 @@ var FILE_SIGNATURES, DANGEROUS_EXTENSIONS, DANGEROUS_MIME_TYPES, ContentSecurity
273470
273937
  var init_protector = __esm({
273471
273938
  "../../packages/security/tw/content-security/protector.ts"() {
273472
273939
  FILE_SIGNATURES = [
273940
+ // Executables and HTML: without these, content renamed to an image extension
273941
+ // had no detected type, the signature check was skipped, and the upload was
273942
+ // accepted as risk "low" -- the exact rename bypass the check exists for.
273943
+ { ext: ["exe", "dll", "sys"], mime: "application/x-msdownload", signature: [77, 90] },
273944
+ { ext: ["elf", "so", "bin"], mime: "application/x-executable", signature: [127, 69, 76, 70] },
273945
+ { ext: ["html", "htm", "xhtml"], mime: "text/html", signature: [60, 104, 116, 109, 108] },
273946
+ { ext: ["html", "htm"], mime: "text/html", signature: [60, 33, 68, 79, 67, 84, 89, 80, 69] },
273473
273947
  { ext: ["jpg", "jpeg"], mime: "image/jpeg", signature: [255, 216, 255] },
273474
273948
  { ext: ["png"], mime: "image/png", signature: [137, 80, 78, 71, 13, 10, 26, 10] },
273475
273949
  { ext: ["gif"], mime: "image/gif", signature: [71, 73, 70, 56] },
@@ -273521,10 +273995,39 @@ var init_protector = __esm({
273521
273995
  ".htpasswd",
273522
273996
  ".svg",
273523
273997
  // Can contain JavaScript
273524
- ".xml"
273998
+ ".xml",
273525
273999
  // Can contain XXE
274000
+ // Active-content types the set used to let through while still blocking .svg
274001
+ // "because it can contain JavaScript" -- .html/.js are script-capable by
274002
+ // definition, so an accepted upload served from our own origin is stored XSS.
274003
+ ".html",
274004
+ ".htm",
274005
+ ".xhtml",
274006
+ ".shtml",
274007
+ ".js",
274008
+ ".mjs",
274009
+ ".cjs",
274010
+ ".jsx",
274011
+ ".ts",
274012
+ ".tsx",
274013
+ ".mhtml",
274014
+ ".hta",
274015
+ ".jsp",
274016
+ ".asp",
274017
+ ".aspx",
274018
+ ".cgi",
274019
+ ".pl"
273526
274020
  ]);
273527
274021
  DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
274022
+ // Active-content types: they can carry script (svg, xhtml, xml) or be
274023
+ // rendered as a document (html, pdf). Blocking .svg by extension while
274024
+ // reporting isContentTypeSafe("image/svg+xml") === true was inconsistent.
274025
+ "image/svg+xml",
274026
+ "text/html",
274027
+ "application/xhtml+xml",
274028
+ "application/pdf",
274029
+ "text/xml",
274030
+ "application/xml",
273528
274031
  "application/javascript",
273529
274032
  "text/javascript",
273530
274033
  "application/x-javascript",
@@ -273579,7 +274082,9 @@ var init_protector = __esm({
273579
274082
  reason: "Empty file",
273580
274083
  sanitizedFilename: sanitized,
273581
274084
  detectedType: "unknown",
273582
- risk: "low"
274085
+ // Unknown content is not low risk -- it is content whose type we could
274086
+ // not establish, which is what a rename bypass looks like.
274087
+ risk: "medium"
273583
274088
  };
273584
274089
  }
273585
274090
  if (this.blockedExtensions.has(ext)) {
@@ -274742,6 +275247,7 @@ var init_nonce_manager = __esm({
274742
275247
  validate(nonce) {
274743
275248
  const entry = this.nonces.get(nonce);
274744
275249
  if (!entry) return false;
275250
+ if (entry.used) return false;
274745
275251
  if (Date.now() > entry.expiresAt) {
274746
275252
  this.nonces.delete(nonce);
274747
275253
  return false;
@@ -275534,7 +276040,15 @@ var init_token_manager = __esm({
275534
276040
  rotateAfterUse;
275535
276041
  maxTokens;
275536
276042
  tokens = /* @__PURE__ */ new Map();
276043
+ /** Tokens this manager issued and already consumed. A rotated token is
276044
+ * deleted from `tokens`, so without this marker a replay found no entry and
276045
+ * fell through to the stateless branch, which accepted it -- defeating the
276046
+ * documented single-use guarantee of `rotateAfterUse`. */
276047
+ consumedTokens = /* @__PURE__ */ new Set();
275537
276048
  constructor(options) {
276049
+ if (!options || typeof options.secret !== "string" || options.secret.length === 0) {
276050
+ throw new Error("createCSRFManager: options.secret is required (the HMAC key used to sign tokens)");
276051
+ }
275538
276052
  this.secret = options.secret;
275539
276053
  this.tokenLength = options.tokenLength ?? 32;
275540
276054
  this.ttl = options.ttl ?? 36e5;
@@ -275634,12 +276148,16 @@ var init_token_manager = __esm({
275634
276148
  }
275635
276149
  entry.used = true;
275636
276150
  if (this.rotateAfterUse) {
276151
+ this.consumedTokens.add(headerPart);
275637
276152
  this.tokens.delete(headerPart);
275638
276153
  const rotated = await this.generate(sessionId ?? void 0);
275639
276154
  return { valid: true, rotated };
275640
276155
  }
275641
276156
  return { valid: true };
275642
276157
  }
276158
+ if (this.consumedTokens.has(headerPart)) {
276159
+ return { valid: false, reason: "Token already used" };
276160
+ }
275643
276161
  const expectedSig = await hmacSign3(
275644
276162
  this.secret,
275645
276163
  `${headerPart}.${sessionId ?? ""}`
@@ -281709,6 +282227,26 @@ var init_render_pipeline = __esm({
281709
282227
  };
281710
282228
  }
281711
282229
  console.error(`[tw] server block failed for ${pageFile}: ${err2?.message ?? err2}`);
282230
+ try {
282231
+ const errFile = ctx.error ?? findRootError(tree);
282232
+ if (errFile) {
282233
+ const compiled = this.compileFile(errFile);
282234
+ let eh = String(compiled.html || "");
282235
+ const em = eh.match(/<body[^>]*>([\s\S]*)<\/body>/i);
282236
+ eh = (em ? em[1] : eh).replace(/<\/?html[^>]*>/gi, "").replace(/<\/?body[^>]*>/gi, "");
282237
+ return {
282238
+ html: this.assembleHTML(eh, compiled.css, "", "Error"),
282239
+ css: "",
282240
+ js: "",
282241
+ status: 500,
282242
+ headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "private, no-store" },
282243
+ fromCache: false,
282244
+ durationMs: 0,
282245
+ serverBlocks: true
282246
+ };
282247
+ }
282248
+ } catch {
282249
+ }
281712
282250
  return {
281713
282251
  html: "<!DOCTYPE html><html><head><title>Server Error</title></head><body><h1>Internal Server Error</h1></body></html>",
281714
282252
  css: "",
@@ -281774,7 +282312,7 @@ var init_render_pipeline = __esm({
281774
282312
  }
281775
282313
  const match = matchRoute2(tree, pathname);
281776
282314
  if (!match) {
281777
- const rootNotFound = findRootNotFound(tree);
282315
+ const rootNotFound = findNearestNotFound(tree, pathname.split("/").filter(Boolean));
281778
282316
  if (rootNotFound) {
281779
282317
  const compiled = this.compileFile(rootNotFound);
281780
282318
  let nf = String(compiled.html || "");
@@ -281840,6 +282378,7 @@ var init_render_pipeline = __esm({
281840
282378
  }
281841
282379
  let bodyHTML = pageCompiled.html;
281842
282380
  let allCSS = pageCompiled.css;
282381
+ const parallelSlots = [];
281843
282382
  try {
281844
282383
  const parentNode = match?.node;
281845
282384
  if (parentNode && Array.isArray(parentNode.children)) {
@@ -281850,9 +282389,7 @@ var init_render_pipeline = __esm({
281850
282389
  const sbm = slotBody.match(/<body[^>]*>([\s\S]*)<\/body>/i);
281851
282390
  if (sbm) slotBody = sbm[1];
281852
282391
  slotBody = slotBody.replace(/<\/?html[^>]*>/gi, "").replace(/<\/?body[^>]*>/gi, "");
281853
- const comp = slotName.charAt(0).toUpperCase() + slotName.slice(1);
281854
- const slotRe = new RegExp(`<${comp}(s[^>]*)?>([sS]*?)</${comp}>|<${comp}(s[^>]*)?/>`, "gi");
281855
- bodyHTML = bodyHTML.replace(slotRe, slotBody);
282392
+ parallelSlots.push({ comp: slotName.charAt(0).toUpperCase() + slotName.slice(1), body: slotBody });
281856
282393
  if (slotCompiled.css) allCSS = slotCompiled.css + "\n" + allCSS;
281857
282394
  }
281858
282395
  }
@@ -281871,6 +282408,11 @@ var init_render_pipeline = __esm({
281871
282408
  allJS = templateCompiled.js + "\n" + allJS;
281872
282409
  bodyHTML = this.wrapInLayout(templateCompiled.html, bodyHTML);
281873
282410
  }
282411
+ for (const { comp, body } of parallelSlots) {
282412
+ const slotRe = new RegExp(`<${comp}(\\s[^>]*)?>([\\s\\S]*?)<\\/${comp}>|<${comp}(\\s[^>]*)?/>`, "gi");
282413
+ bodyHTML = bodyHTML.replace(slotRe, body);
282414
+ bodyHTML = bodyHTML.replace(new RegExp(`<!--\\s*TW: unknown component '${comp}'[\\s\\S]*?-->`, "gi"), body);
282415
+ }
281874
282416
  let layoutTitle;
281875
282417
  const liftedParts = [];
281876
282418
  {
@@ -282289,8 +282831,6 @@ ${bodyHTML}`;
282289
282831
  };
282290
282832
  let tags = "";
282291
282833
  const description = pick2("description");
282292
- if (description) tags += ` <meta name="description" content="${esc4(description)}">
282293
- `;
282294
282834
  const keywords = pick2("keywords");
282295
282835
  if (keywords) tags += ` <meta name="keywords" content="${esc4(keywords)}">
282296
282836
  `;
@@ -284472,6 +285012,7 @@ var init_static = __esm({
284472
285012
  if (!existsSync15(filePath)) {
284473
285013
  filePath = join19(this.root, safePath + ".html");
284474
285014
  }
285015
+ let notFoundFallback = false;
284475
285016
  if (!existsSync15(filePath)) {
284476
285017
  filePath = join19(this.root, safePath, this.indexFile);
284477
285018
  }
@@ -284483,6 +285024,7 @@ var init_static = __esm({
284483
285024
  }
284484
285025
  } else {
284485
285026
  filePath = join19(this.root, "404.html");
285027
+ notFoundFallback = true;
284486
285028
  if (!existsSync15(filePath)) {
284487
285029
  return new Response("Not Found", { status: 404, headers: { "Content-Type": "text/plain" } });
284488
285030
  }
@@ -284550,12 +285092,12 @@ var init_static = __esm({
284550
285092
  const gzHeaders = { ...headers, "Content-Encoding": "gzip", "Content-Length": String(gz.length), Vary: "Accept-Encoding" };
284551
285093
  delete gzHeaders["Content-Length"];
284552
285094
  if (this.lastModified) delete gzHeaders["Last-Modified"];
284553
- return new Response(gz, { headers: gzHeaders });
285095
+ return new Response(gz, { headers: gzHeaders, status: notFoundFallback ? 404 : 200 });
284554
285096
  }
284555
285097
  } catch {
284556
285098
  }
284557
285099
  }
284558
- return new Response(bytes, { headers });
285100
+ return new Response(bytes, { headers, status: notFoundFallback ? 404 : 200 });
284559
285101
  }
284560
285102
  /**
284561
285103
  * Parse a `Range: bytes=...` header against a known content length.
@@ -285879,17 +286421,17 @@ async function hs256(payload, secret, maxAge) {
285879
286421
  const header = { alg: "HS256", typ: "JWT" };
285880
286422
  const body = { ...payload, iat: now, exp: now + maxAge };
285881
286423
  const data = enc(header) + "." + enc(body);
285882
- const { createHmac: createHmac2 } = await import("node:crypto");
285883
- const sig = createHmac2("sha256", secret).update(data).digest("base64url");
286424
+ const { createHmac: createHmac3 } = await import("node:crypto");
286425
+ const sig = createHmac3("sha256", secret).update(data).digest("base64url");
285884
286426
  return data + "." + sig;
285885
286427
  }
285886
286428
  async function verifyHs256(token, secret) {
285887
286429
  const parts = token.split(".");
285888
286430
  if (parts.length !== 3) return null;
285889
- const { createHmac: createHmac2, timingSafeEqual: timingSafeEqual4 } = await import("node:crypto");
285890
- const expected = createHmac2("sha256", secret).update(parts[0] + "." + parts[1]).digest("base64url");
286431
+ const { createHmac: createHmac3, timingSafeEqual: timingSafeEqual3 } = await import("node:crypto");
286432
+ const expected = createHmac3("sha256", secret).update(parts[0] + "." + parts[1]).digest("base64url");
285891
286433
  const a = Buffer.from(expected), b = Buffer.from(parts[2]);
285892
- if (a.length !== b.length || !timingSafeEqual4(a, b)) return null;
286434
+ if (a.length !== b.length || !timingSafeEqual3(a, b)) return null;
285893
286435
  try {
285894
286436
  const body = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
285895
286437
  if (typeof body.exp === "number" && body.exp < Math.floor(Date.now() / 1e3)) return null;
@@ -287194,6 +287736,19 @@ ${err2.stack ?? ""}`);
287194
287736
  const isrLookupPath = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
287195
287737
  if (this.staticHandler && !this.isrRoutes.has(isrLookupPath)) {
287196
287738
  let staticResponse = await this.staticHandler.serve(pathname, request);
287739
+ if (staticResponse.status !== 404) {
287740
+ const reqMethod = request.method.toUpperCase();
287741
+ const allow = "GET, HEAD, OPTIONS";
287742
+ if (reqMethod === "TRACE") {
287743
+ return new Response("Method Not Allowed", { status: 405, headers: { Allow: allow } });
287744
+ }
287745
+ if (reqMethod === "OPTIONS") {
287746
+ return new Response(null, { status: 204, headers: { Allow: allow } });
287747
+ }
287748
+ if (reqMethod !== "GET" && reqMethod !== "HEAD") {
287749
+ return new Response("Method Not Allowed", { status: 405, headers: { Allow: allow } });
287750
+ }
287751
+ }
287197
287752
  if (staticResponse && this.cspEnabled() && (staticResponse.headers.get("content-type") || "").includes("text/html")) {
287198
287753
  try {
287199
287754
  let buf = Buffer.from(await staticResponse.arrayBuffer());