tina4-nodejs 3.13.149 → 3.13.151

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 (60) hide show
  1. package/CLAUDE.md +2 -2
  2. package/package.json +5 -9
  3. package/packages/cli/dist/bin.js +1074 -521
  4. package/packages/core/dist/index.js +1071 -516
  5. package/packages/core/public/bundled-component-provenance.json +2 -2
  6. package/packages/core/public/js/tina4-dev-admin-split.html +202 -0
  7. package/packages/core/public/js/tina4-dev-admin.min.js +147 -138
  8. package/packages/core/src/aiClient.ts +2 -1
  9. package/packages/core/src/api.ts +2 -1
  10. package/packages/core/src/auth.ts +1 -1
  11. package/packages/core/src/cache.ts +6 -1
  12. package/packages/core/src/devAdmin.ts +209 -123
  13. package/packages/core/src/dispatchPipeline.ts +4 -0
  14. package/packages/core/src/docs.ts +5 -3
  15. package/packages/core/src/errorOverlay.ts +18 -9
  16. package/packages/core/src/index.ts +2 -1
  17. package/packages/core/src/mcp.ts +53 -16
  18. package/packages/core/src/messenger.ts +22 -30
  19. package/packages/core/src/metrics.ts +5 -1
  20. package/packages/core/src/middleware.ts +8 -1
  21. package/packages/core/src/optionalPackage.ts +2 -2
  22. package/packages/core/src/plan.ts +4 -4
  23. package/packages/core/src/push.ts +1 -1
  24. package/packages/core/src/request.ts +185 -25
  25. package/packages/core/src/router.ts +3 -2
  26. package/packages/core/src/server.ts +32 -10
  27. package/packages/core/src/session.ts +44 -0
  28. package/packages/core/src/slashTrim.ts +39 -0
  29. package/packages/core/src/wsdl.ts +66 -48
  30. package/packages/core/templates/crud/form.twig +5 -5
  31. package/packages/core/templates/crud/table.twig +2 -2
  32. package/packages/core/templates/errors/302.twig +4 -3
  33. package/packages/frond/dist/index.js +206 -64
  34. package/packages/frond/src/engine.ts +278 -69
  35. package/packages/frond/src/index.ts +1 -1
  36. package/packages/orm/dist/index.js +1055 -502
  37. package/packages/orm/src/crudRender.ts +5 -5
  38. package/packages/orm/src/crudTable.ts +1 -1
  39. package/packages/orm/src/database.ts +15 -29
  40. package/packages/orm/src/databaseResult.ts +11 -1
  41. package/packages/orm/src/realtime/index.ts +1 -0
  42. package/packages/orm/src/realtime/realtime.ts +2 -1
  43. package/packages/orm/src/realtime/s3Client.ts +276 -0
  44. package/packages/orm/src/realtime/storage.ts +19 -60
  45. package/packages/orm/src/sqlTranslator.ts +58 -42
  46. package/packages/swagger/dist/index.js +25 -4
  47. package/packages/swagger/src/generator.ts +7 -4
  48. package/packages/swagger/src/ui.ts +2 -1
  49. package/types/core/src/devAdmin.d.ts +40 -0
  50. package/types/core/src/index.d.ts +2 -1
  51. package/types/core/src/middleware.d.ts +1 -0
  52. package/types/core/src/optionalPackage.d.ts +2 -2
  53. package/types/core/src/request.d.ts +5 -1
  54. package/types/core/src/session.d.ts +25 -0
  55. package/types/core/src/slashTrim.d.ts +6 -0
  56. package/types/frond/src/engine.d.ts +11 -0
  57. package/types/frond/src/index.d.ts +1 -1
  58. package/types/orm/src/realtime/index.d.ts +1 -0
  59. package/types/orm/src/realtime/s3Client.d.ts +48 -0
  60. package/types/orm/src/realtime/storage.d.ts +9 -7
@@ -51,6 +51,12 @@ var init_types = __esm({
51
51
  });
52
52
 
53
53
  // src/databaseResult.ts
54
+ function stripTypeArgs(colType) {
55
+ const open = colType.indexOf("(");
56
+ if (open < 0) return colType;
57
+ const close = colType.indexOf(")", open);
58
+ return close < 0 ? colType : colType.slice(0, open) + colType.slice(close + 1);
59
+ }
54
60
  var DatabaseResult;
55
61
  var init_databaseResult = __esm({
56
62
  "src/databaseResult.ts"() {
@@ -233,7 +239,7 @@ var init_databaseResult = __esm({
233
239
  const [size, decimals] = this._parseTypeSize(colType);
234
240
  return {
235
241
  name: col.name,
236
- type: colType.replace(/\(.*\)/, ""),
242
+ type: stripTypeArgs(colType),
237
243
  size,
238
244
  decimals,
239
245
  nullable: col.nullable ?? true,
@@ -790,12 +796,49 @@ var init_point = __esm({
790
796
  });
791
797
 
792
798
  // src/sqlTranslator.ts
793
- var SQLTranslator, QueryCache;
799
+ function skipQuoted(sql, i, backslashEscapes) {
800
+ const c = sql[i];
801
+ const escapeString = c === "'" && (sql[i - 1] === "E" || sql[i - 1] === "e") && !isIdentChar(sql[i - 2]);
802
+ const backslash = c !== "`" && (escapeString || backslashEscapes);
803
+ const n = sql.length;
804
+ i++;
805
+ while (i < n) {
806
+ if (backslash && sql[i] === "\\") {
807
+ i += 2;
808
+ continue;
809
+ }
810
+ if (sql[i] === c) {
811
+ if (sql[i + 1] === c) {
812
+ i += 2;
813
+ continue;
814
+ }
815
+ return i + 1;
816
+ }
817
+ i++;
818
+ }
819
+ return i;
820
+ }
821
+ function skipLineComment(sql, i) {
822
+ while (i < sql.length && sql[i] !== "\n") i++;
823
+ return i;
824
+ }
825
+ function skipBlockComment(sql, i) {
826
+ const end = sql.indexOf("*/", i + 2);
827
+ return end === -1 ? sql.length : end + 2;
828
+ }
829
+ function skipDollarQuote(sql, i) {
830
+ const tag = sql.slice(i).match(/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/);
831
+ if (!tag) return i;
832
+ const end = sql.indexOf(tag[0], i + tag[0].length);
833
+ return end === -1 ? sql.length : end + tag[0].length;
834
+ }
835
+ var isIdentChar, SQLTranslator, QueryCache;
794
836
  var init_sqlTranslator = __esm({
795
837
  "src/sqlTranslator.ts"() {
796
838
  "use strict";
797
839
  init_databaseUrl();
798
840
  init_point();
841
+ isIdentChar = (ch) => !!ch && /[A-Za-z0-9_]/.test(ch);
799
842
  SQLTranslator = class _SQLTranslator {
800
843
  static SPATIAL_ENGINES = /* @__PURE__ */ new Set(["postgres", "postgresql"]);
801
844
  static SPATIAL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -1076,47 +1119,26 @@ var init_sqlTranslator = __esm({
1076
1119
  static placeholderPositions(sql, options = {}) {
1077
1120
  const positions = [];
1078
1121
  const n = sql.length;
1079
- const isIdent = (ch) => !!ch && /[A-Za-z0-9_]/.test(ch);
1080
1122
  let i = 0;
1081
1123
  while (i < n) {
1082
1124
  const c = sql[i];
1083
1125
  const next = sql[i + 1];
1084
1126
  if (c === "'" || c === '"' || c === "`") {
1085
- const prev = sql[i - 1];
1086
- const escapeString = c === "'" && (prev === "E" || prev === "e") && !isIdent(sql[i - 2]);
1087
- const backslash = c !== "`" && (escapeString || options.backslashEscapes === true);
1088
- i++;
1089
- while (i < n) {
1090
- if (backslash && sql[i] === "\\") {
1091
- i += 2;
1092
- continue;
1093
- }
1094
- if (sql[i] === c) {
1095
- if (sql[i + 1] === c) {
1096
- i += 2;
1097
- continue;
1098
- }
1099
- i++;
1100
- break;
1101
- }
1102
- i++;
1103
- }
1127
+ i = skipQuoted(sql, i, options.backslashEscapes === true);
1104
1128
  continue;
1105
1129
  }
1106
1130
  if (c === "-" && next === "-") {
1107
- while (i < n && sql[i] !== "\n") i++;
1131
+ i = skipLineComment(sql, i);
1108
1132
  continue;
1109
1133
  }
1110
1134
  if (c === "/" && next === "*") {
1111
- const end = sql.indexOf("*/", i + 2);
1112
- i = end === -1 ? n : end + 2;
1135
+ i = skipBlockComment(sql, i);
1113
1136
  continue;
1114
1137
  }
1115
- if (c === "$" && !isIdent(sql[i - 1])) {
1116
- const tag = sql.slice(i).match(/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/);
1117
- if (tag) {
1118
- const end = sql.indexOf(tag[0], i + tag[0].length);
1119
- i = end === -1 ? n : end + tag[0].length;
1138
+ if (c === "$" && !isIdentChar(sql[i - 1])) {
1139
+ const past = skipDollarQuote(sql, i);
1140
+ if (past !== i) {
1141
+ i = past;
1120
1142
  continue;
1121
1143
  }
1122
1144
  }
@@ -1149,7 +1171,7 @@ var init_sqlTranslator = __esm({
1149
1171
  * → { sql: "INSERT INTO t (x) VALUES (1)", columns: ["id", "name"] }
1150
1172
  */
1151
1173
  static parseReturning(sql) {
1152
- const m = sql.match(/\bRETURNING\s+(.+)$/i);
1174
+ const m = sql.match(/\bRETURNING\s+(\S.*)$/i);
1153
1175
  if (!m) return { sql, columns: [] };
1154
1176
  const columns = m[1].split(",").map((c) => c.trim());
1155
1177
  return {
@@ -1203,7 +1225,12 @@ var init_sqlTranslator = __esm({
1203
1225
  // The `d` flag records group indices, so the head can be sliced at the exact
1204
1226
  // start of the VALUES group rather than by hunting for a parenthesis (the
1205
1227
  // column list has parentheses too).
1206
- static INSERT_VALUES = /^\s*INSERT\s+INTO\s+.+?\s+VALUES\s*\(([^()]*)\)\s*$/dis;
1228
+ static INSERT_VALUES = (
1229
+ // ReDoS-safe: the table ref matches `\S(?:[\s\S]*?\S)?` (bounded by non-space
1230
+ // at both ends) so the separator `\s+` before VALUES can't overlap it the way
1231
+ // the old `.+?\s+` did (polynomial on `INSERT INTO ` + a long space run).
1232
+ /^\s*INSERT\s+INTO\s+\S(?:[\s\S]*?\S)?\s+VALUES\s*\(([^()]*)\)\s*$/dis
1233
+ );
1207
1234
  /**
1208
1235
  * Engines whose lastInsertId reports the FIRST generated id of a multi-row
1209
1236
  * INSERT rather than the last. Verified live, not assumed: a 3-row insert
@@ -1678,6 +1705,30 @@ var init_sqlDialect = __esm({
1678
1705
  }
1679
1706
  });
1680
1707
 
1708
+ // ../core/src/slashTrim.ts
1709
+ function rtrimSlashes(s) {
1710
+ let end = s.length;
1711
+ while (end > 0 && s.charCodeAt(end - 1) === SLASH) end--;
1712
+ return end === s.length ? s : s.slice(0, end);
1713
+ }
1714
+ function trimChar(s, code) {
1715
+ let start2 = 0;
1716
+ let end = s.length;
1717
+ while (start2 < end && s.charCodeAt(start2) === code) start2++;
1718
+ while (end > start2 && s.charCodeAt(end - 1) === code) end--;
1719
+ return start2 === 0 && end === s.length ? s : s.slice(start2, end);
1720
+ }
1721
+ function trimSlashes(s) {
1722
+ return trimChar(s, SLASH);
1723
+ }
1724
+ var SLASH;
1725
+ var init_slashTrim = __esm({
1726
+ "../core/src/slashTrim.ts"() {
1727
+ "use strict";
1728
+ SLASH = 47;
1729
+ }
1730
+ });
1731
+
1681
1732
  // ../core/src/constants.ts
1682
1733
  var HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, HTTP_NO_CONTENT, HTTP_MOVED, HTTP_REDIRECT, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_CONFLICT, HTTP_GONE, HTTP_UNPROCESSABLE, HTTP_TOO_MANY, HTTP_SERVER_ERROR, HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, APPLICATION_JSON, APPLICATION_XML, APPLICATION_FORM, APPLICATION_OCTET, TEXT_HTML, TEXT_PLAIN, TEXT_CSV, TEXT_XML;
1683
1734
  var init_constants = __esm({
@@ -2766,7 +2817,7 @@ function resolveAlgorithm(algorithm) {
2766
2817
  return chosen;
2767
2818
  }
2768
2819
  function base64urlEncode(data) {
2769
- return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2820
+ return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
2770
2821
  }
2771
2822
  function base64urlDecode(str) {
2772
2823
  let s = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -3183,6 +3234,7 @@ function mayStore(req2, vary, cacheControl, setCookie) {
3183
3234
  if (requestHeader(req2, "authorization") !== void 0) return sharedCacheAllowed(cacheControl);
3184
3235
  if (requestHeader(req2, "cookie") !== void 0) return sharedCacheAllowed(cacheControl);
3185
3236
  if (hasSetCookie(setCookie)) return sharedCacheAllowed(cacheControl);
3237
+ if (req2.session?.isFresh && !req2.session.isFresh()) return sharedCacheAllowed(cacheControl);
3186
3238
  return true;
3187
3239
  }
3188
3240
  function varyMatches(entry, req2) {
@@ -4535,7 +4587,7 @@ function coerceParam(raw, type2) {
4535
4587
  }
4536
4588
  function joinGroupPath(prefix, path10) {
4537
4589
  const full = `${prefix}/${path10.replace(/^\/+/, "")}`;
4538
- const trimmed = `/${full.replace(/^\/+|\/+$/g, "")}`;
4590
+ const trimmed = `/${trimSlashes(full)}`;
4539
4591
  return trimmed.replace(/\/+/g, "/");
4540
4592
  }
4541
4593
  async function resolveStringMiddleware(spec) {
@@ -4600,6 +4652,7 @@ var WsRouteRef, RouteRef, Router, RouteGroup, TINA4_DEFAULT_ROUTER, globalRouter
4600
4652
  var init_router = __esm({
4601
4653
  "../core/src/router.ts"() {
4602
4654
  "use strict";
4655
+ init_slashTrim();
4603
4656
  init_dotenv();
4604
4657
  init_middleware();
4605
4658
  WsRouteRef = class {
@@ -4809,7 +4862,7 @@ var init_router = __esm({
4809
4862
  const direct = this.matchRoute(routes, path10);
4810
4863
  if (direct) return direct;
4811
4864
  if (isTrailingSlashRedirectEnabled() && path10.length > 1 && path10.endsWith("/")) {
4812
- const stripped = path10.replace(/\/+$/, "");
4865
+ const stripped = rtrimSlashes(path10);
4813
4866
  if (stripped.length > 0) {
4814
4867
  const retry = this.matchRoute(routes, stripped);
4815
4868
  if (retry) return retry;
@@ -5488,9 +5541,11 @@ __export(engine_exports, {
5488
5541
  escapeHtml: () => escapeHtml,
5489
5542
  expressionFormCache: () => expressionFormCache,
5490
5543
  filterChainCache: () => filterChainCache,
5544
+ frondRange: () => frondRange,
5491
5545
  pathParseCache: () => pathParseCache,
5492
5546
  setCspNonceProvider: () => setCspNonceProvider,
5493
- setFormTokenSessionId: () => setFormTokenSessionId
5547
+ setFormTokenSessionId: () => setFormTokenSessionId,
5548
+ setFormTokenSessionProvider: () => setFormTokenSessionProvider
5494
5549
  });
5495
5550
  import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
5496
5551
  import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync4, realpathSync } from "node:fs";
@@ -5575,6 +5630,14 @@ function renderDump(value) {
5575
5630
  const escaped = dumped.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5576
5631
  return new SafeString(`<pre>${escaped}</pre>`);
5577
5632
  }
5633
+ function stripTags(input) {
5634
+ let s = input, prev;
5635
+ do {
5636
+ prev = s;
5637
+ s = s.replace(STRIP_TAGS_RE, "");
5638
+ } while (s !== prev);
5639
+ return s;
5640
+ }
5578
5641
  function liveAttr(value) {
5579
5642
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5580
5643
  }
@@ -5592,35 +5655,58 @@ function sweepExpiredCache(cache) {
5592
5655
  if (expiresAt2 <= now) cache.delete(key);
5593
5656
  }
5594
5657
  }
5595
- function tokenize(source) {
5658
+ function extractRawBlocks(source) {
5596
5659
  const rawBlocks = [];
5597
- source = source.replace(RAW_BLOCK_RE, (_match, content) => {
5660
+ let out = "";
5661
+ let pos = 0;
5662
+ RAW_OPEN_RE.lastIndex = 0;
5663
+ let om;
5664
+ while ((om = RAW_OPEN_RE.exec(source)) !== null) {
5665
+ const openStart = om.index;
5666
+ const openEnd = RAW_OPEN_RE.lastIndex;
5667
+ RAW_CLOSE_RE.lastIndex = openEnd;
5668
+ const cm = RAW_CLOSE_RE.exec(source);
5669
+ if (cm === null) break;
5598
5670
  const idx = rawBlocks.length;
5599
- rawBlocks.push(content);
5600
- return `\0RAW_${idx}\0`;
5601
- });
5671
+ rawBlocks.push(source.slice(openEnd, cm.index));
5672
+ out += source.slice(pos, openStart) + `\0RAW_${idx}\0`;
5673
+ pos = RAW_CLOSE_RE.lastIndex;
5674
+ RAW_OPEN_RE.lastIndex = pos;
5675
+ }
5676
+ out += source.slice(pos);
5677
+ return { source: out, rawBlocks };
5678
+ }
5679
+ function tokenize(rawSource) {
5680
+ const { source, rawBlocks } = extractRawBlocks(rawSource);
5602
5681
  const tokens = [];
5682
+ const n = source.length;
5603
5683
  let pos = 0;
5604
- TOKEN_RE.lastIndex = 0;
5605
- let m;
5606
- while ((m = TOKEN_RE.exec(source)) !== null) {
5607
- const start2 = m.index;
5608
- if (start2 > pos) {
5609
- tokens.push(["TEXT", source.slice(pos, start2)]);
5684
+ let scan = 0;
5685
+ const exhausted = { "%}": false, "}}": false, "#}": false };
5686
+ while (scan < n) {
5687
+ const brace = source.indexOf("{", scan);
5688
+ if (brace === -1) break;
5689
+ const kind = TAG_KINDS[source[brace + 1]];
5690
+ if (!kind) {
5691
+ scan = brace + 1;
5692
+ continue;
5610
5693
  }
5611
- const raw = m[0];
5612
- if (raw.startsWith("{#")) {
5613
- tokens.push(["COMMENT", raw]);
5614
- } else if (raw.startsWith("{{")) {
5615
- tokens.push(["VAR", raw]);
5616
- } else if (raw.startsWith("{%")) {
5617
- tokens.push(["BLOCK", raw]);
5694
+ if (exhausted[kind.close]) {
5695
+ scan = brace + 1;
5696
+ continue;
5618
5697
  }
5619
- pos = m.index + raw.length;
5620
- }
5621
- if (pos < source.length) {
5622
- tokens.push(["TEXT", source.slice(pos)]);
5698
+ const close = source.indexOf(kind.close, brace + 2);
5699
+ if (close === -1) {
5700
+ exhausted[kind.close] = true;
5701
+ scan = brace + 1;
5702
+ continue;
5703
+ }
5704
+ if (brace > pos) tokens.push(["TEXT", source.slice(pos, brace)]);
5705
+ tokens.push([kind.type, source.slice(brace, close + 2)]);
5706
+ pos = close + 2;
5707
+ scan = close + 2;
5623
5708
  }
5709
+ if (pos < n) tokens.push(["TEXT", source.slice(pos)]);
5624
5710
  if (rawBlocks.length > 0) {
5625
5711
  for (let i = 0; i < tokens.length; i++) {
5626
5712
  if (tokens[i][0] === "TEXT" && tokens[i][1].includes("\0RAW_")) {
@@ -5946,26 +6032,123 @@ function evalConcatOrComparison(expr, context) {
5946
6032
  }
5947
6033
  return EXPR_NOT_MATCHED;
5948
6034
  }
6035
+ function topLevelPositions(expr) {
6036
+ const positions = [];
6037
+ let inQuote = null;
6038
+ let depth = 0;
6039
+ for (let pos = 0; pos < expr.length; pos++) {
6040
+ const ch = expr[pos];
6041
+ if (inQuote) {
6042
+ if (ch === inQuote) inQuote = null;
6043
+ } else if (ch === '"' || ch === "'") {
6044
+ inQuote = ch;
6045
+ } else if ("([{".includes(ch)) {
6046
+ depth++;
6047
+ } else if (")]}".includes(ch)) {
6048
+ depth--;
6049
+ } else if (depth === 0) {
6050
+ positions.push(pos);
6051
+ }
6052
+ }
6053
+ return positions;
6054
+ }
6055
+ function followsOperand(expr, pos) {
6056
+ return /[\w)\]'"]$/.test(expr.slice(0, pos).trimEnd());
6057
+ }
6058
+ function topLevelArithmeticOperators(expr) {
6059
+ const operators = [];
6060
+ let skipUntil = -1;
6061
+ for (const pos of topLevelPositions(expr)) {
6062
+ if (pos < skipUntil || !"+-*/%".includes(expr[pos])) continue;
6063
+ const pair = expr.slice(pos, pos + 2);
6064
+ const op = pair === "**" || pair === "//" ? pair : expr[pos];
6065
+ skipUntil = pos + op.length;
6066
+ if (followsOperand(expr, pos)) operators.push([pos, op]);
6067
+ }
6068
+ return operators;
6069
+ }
6070
+ function splitArithmetic(expr) {
6071
+ const operators = topLevelArithmeticOperators(expr);
6072
+ for (const level of ARITHMETIC_LEVELS) {
6073
+ const matches = operators.filter(([, op]) => level.includes(op));
6074
+ if (matches.length > 0) return level[0] === "**" ? matches[0] : matches[matches.length - 1];
6075
+ }
6076
+ return null;
6077
+ }
5949
6078
  function evalArithmeticExpression(expr, context) {
5950
- for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
5951
- const pos = findOutsideQuotes(expr, op);
5952
- if (pos >= 0) {
5953
- const left = expr.slice(0, pos).trim();
5954
- const right = expr.slice(pos + op.length).trim();
5955
- const lVal = evalExpr(left, context);
5956
- const rVal = evalExpr(right, context);
5957
- try {
5958
- let lNum = lVal != null ? Number(lVal) : 0;
5959
- let rNum = rVal != null ? Number(rVal) : 0;
5960
- if (isNaN(lNum)) lNum = 0;
5961
- if (isNaN(rNum)) rNum = 0;
5962
- return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
5963
- } catch {
5964
- return null;
5965
- }
6079
+ const split = splitArithmetic(expr);
6080
+ if (!split) return EXPR_NOT_MATCHED;
6081
+ const [pos, op] = split;
6082
+ const lVal = evalExpr(expr.slice(0, pos).trim(), context);
6083
+ const rVal = evalExpr(expr.slice(pos + op.length).trim(), context);
6084
+ let lNum = lVal != null ? Number(lVal) : 0;
6085
+ let rNum = rVal != null ? Number(rVal) : 0;
6086
+ if (isNaN(lNum)) lNum = 0;
6087
+ if (isNaN(rNum)) rNum = 0;
6088
+ return ARITHMETIC_OPERATIONS[op](lNum, rNum);
6089
+ }
6090
+ function frondRange(start2, end, step = 1) {
6091
+ const numeric = (value) => typeof value === "number" || typeof value === "string" && NUMERIC_RE.test(value);
6092
+ if (!numeric(start2) || !numeric(end) || !numeric(step)) return [];
6093
+ const first = Math.trunc(Number(start2));
6094
+ const last = Math.trunc(Number(end));
6095
+ const stride = Math.abs(Math.trunc(Number(step)));
6096
+ if (stride === 0) return [];
6097
+ const values = [];
6098
+ if (first <= last) for (let value = first; value <= last; value += stride) values.push(value);
6099
+ else for (let value = first; value >= last; value -= stride) values.push(value);
6100
+ return values;
6101
+ }
6102
+ function findRangeOperator(expr) {
6103
+ let pos = findOutsideQuotes(expr, "..");
6104
+ while (pos >= 0) {
6105
+ const partOfEllipsis = expr.startsWith("...", pos) || pos > 0 && expr[pos - 1] === ".";
6106
+ if (pos > 0 && !partOfEllipsis) return findOutsideQuotes(expr.slice(pos + 2), "|") >= 0 ? -1 : pos;
6107
+ const next = findOutsideQuotes(expr.slice(pos + 2), "..");
6108
+ pos = next >= 0 ? pos + 2 + next : -1;
6109
+ }
6110
+ return -1;
6111
+ }
6112
+ function evalRangeExpression(expr, context) {
6113
+ const pos = findRangeOperator(expr);
6114
+ if (pos < 0) return EXPR_NOT_MATCHED;
6115
+ return frondRange(evalExpr(expr.slice(0, pos).trim(), context), evalExpr(expr.slice(pos + 2).trim(), context));
6116
+ }
6117
+ function closingBracketIndex(expr) {
6118
+ let depth = 0;
6119
+ let inQuote = null;
6120
+ for (let i = 0; i < expr.length; i++) {
6121
+ const ch = expr[i];
6122
+ if (inQuote) {
6123
+ if (ch === inQuote) inQuote = null;
6124
+ } else if (ch === '"' || ch === "'") {
6125
+ inQuote = ch;
6126
+ } else if ("([{".includes(ch)) {
6127
+ depth++;
6128
+ } else if (")]}".includes(ch) && --depth === 0) {
6129
+ return i;
5966
6130
  }
5967
6131
  }
5968
- return EXPR_NOT_MATCHED;
6132
+ return -1;
6133
+ }
6134
+ function mapLiteralKey(rawKey, context) {
6135
+ const quoted = rawKey.length >= 2 && (rawKey[0] === '"' || rawKey[0] === "'") && rawKey.endsWith(rawKey[0]);
6136
+ if (quoted) return rawKey.slice(1, -1);
6137
+ return BARE_KEY_RE.test(rawKey) ? rawKey : String(evalExpr(rawKey, context));
6138
+ }
6139
+ function evalMapLiteral(inner, context) {
6140
+ const map = {};
6141
+ for (const pair of inner === "" ? [] : splitArgs(inner)) {
6142
+ const colon = findOutsideQuotes(pair, ":");
6143
+ if (colon > 0) map[mapLiteralKey(pair.slice(0, colon).trim(), context)] = evalExpr(pair.slice(colon + 1).trim(), context);
6144
+ }
6145
+ return map;
6146
+ }
6147
+ function evalCollectionLiteral(expr, context) {
6148
+ if (expr[0] !== "[" && expr[0] !== "{" || closingBracketIndex(expr) !== expr.length - 1) return EXPR_NOT_MATCHED;
6149
+ const inner = expr.slice(1, -1).trim();
6150
+ if (expr[0] === "{") return evalMapLiteral(inner, context);
6151
+ return inner === "" ? [] : splitArgs(inner).map((item) => evalExpr(item.trim(), context));
5969
6152
  }
5970
6153
  function evalFilterExpression(expr, context) {
5971
6154
  if (findOutsideQuotes(expr, "|") >= 0) {
@@ -6387,12 +6570,12 @@ function splitArgs(raw) {
6387
6570
  current += ch;
6388
6571
  continue;
6389
6572
  }
6390
- if (ch === "(" || ch === "[") {
6573
+ if (ch === "(" || ch === "[" || ch === "{") {
6391
6574
  depth++;
6392
6575
  current += ch;
6393
6576
  continue;
6394
6577
  }
6395
- if (ch === ")" || ch === "]") {
6578
+ if (ch === ")" || ch === "]" || ch === "}") {
6396
6579
  depth--;
6397
6580
  current += ch;
6398
6581
  continue;
@@ -6557,11 +6740,14 @@ function formatMatch(match, flags, width, prec, type2, arg) {
6557
6740
  return padFormattedValue(formatValue(type2, precision, arg), flags, width);
6558
6741
  }
6559
6742
  function _b64url(data) {
6560
- return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6743
+ return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
6561
6744
  }
6562
6745
  function setFormTokenSessionId(sessionId) {
6563
6746
  _formTokenSessionId = sessionId || "";
6564
6747
  }
6748
+ function setFormTokenSessionProvider(fn) {
6749
+ _formTokenSessionProvider = fn;
6750
+ }
6565
6751
  function setCspNonceProvider(fn) {
6566
6752
  _cspNonceProvider = fn;
6567
6753
  }
@@ -6586,8 +6772,9 @@ function _buildFormTokenJwt(descriptor = "") {
6586
6772
  payload.context = descriptor;
6587
6773
  }
6588
6774
  }
6589
- if (_formTokenSessionId) {
6590
- payload.session_id = _formTokenSessionId;
6775
+ const sessionId = _formTokenSessionProvider() || _formTokenSessionId;
6776
+ if (sessionId) {
6777
+ payload.session_id = sessionId;
6591
6778
  }
6592
6779
  const h2 = _b64url(Buffer.from(JSON.stringify(header)));
6593
6780
  const p = _b64url(Buffer.from(JSON.stringify(payload)));
@@ -6603,7 +6790,7 @@ function _generateFormToken(descriptor = "") {
6603
6790
  function _generateFormTokenValue(descriptor = "") {
6604
6791
  return new SafeString(_buildFormTokenJwt(descriptor));
6605
6792
  }
6606
- var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FAST_FILTERS, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, JS_SAFE_RE, MEDIA_TYPE_RE, SANDBOX_EXPR_KEYWORDS, SANDBOX_IDENT_RE, SANDBOX_STRING_RE, UNSAFE_KEYS, BUILTIN_FILTERS, _formTokenSessionId, _cspNonceProvider, Frond;
6793
+ var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FAST_FILTERS, FORMAT_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, RAW_OPEN_RE, RAW_CLOSE_RE, TAG_KINDS, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, ARITHMETIC_LEVELS, BARE_KEY_RE, EXPR_EVALUATORS, expressionFormCache, VarRef, JS_SAFE_RE, MEDIA_TYPE_RE, SANDBOX_EXPR_KEYWORDS, SANDBOX_IDENT_RE, SANDBOX_STRING_RE, UNSAFE_KEYS, BUILTIN_FILTERS, _formTokenSessionId, _formTokenSessionProvider, _cspNonceProvider, Frond;
6607
6794
  var init_engine = __esm({
6608
6795
  "../frond/src/engine.ts"() {
6609
6796
  "use strict";
@@ -6705,7 +6892,7 @@ var init_engine = __esm({
6705
6892
  int: (value) => value ? parseInt(String(value), 10) || 0 : 0,
6706
6893
  float: (value) => value ? parseFloat(String(value)) || 0 : 0,
6707
6894
  abs: (value) => typeof value === "number" ? Math.abs(value) : value,
6708
- striptags: (value) => String(value).replace(STRIP_TAGS_RE, ""),
6895
+ striptags: (value) => stripTags(String(value)),
6709
6896
  first: (value) => Array.isArray(value) ? value[0] ?? null : null,
6710
6897
  last: (value) => Array.isArray(value) ? value[value.length - 1] ?? null : null,
6711
6898
  keys: (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : [],
@@ -6719,20 +6906,23 @@ var init_engine = __esm({
6719
6906
  filter: (value) => Array.isArray(value) ? value.filter(Boolean) : value
6720
6907
  };
6721
6908
  FORMAT_RE = /%%|%([-+ 0]*)(\d+)?(?:\.(\d+))?([sdifFeEgGxXob])/g;
6722
- LEADING_WS_RE = /^\s+/;
6723
- TRAILING_WS_RE = /\s+$/;
6724
6909
  THOUSANDS_RE = /\B(?=(\d{3})+(?!\d))/g;
6725
6910
  LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
6726
6911
  LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
6727
6912
  LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
6728
- EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
6729
- EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
6913
+ EXTENDS_RE = /\{%[-\s]*extends\s+["']([^"']+)["']\s*[-]?%\}/;
6914
+ EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["']([^"']+)["']\s*[-]?%\}/g;
6730
6915
  filterChainCache = /* @__PURE__ */ new Map();
6731
6916
  pathParseCache = /* @__PURE__ */ new Map();
6732
6917
  TEMPLATE_CACHE_MAX = 256;
6733
6918
  MEMO_CACHE_MAX = 1024;
6734
- TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
6735
- RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
6919
+ RAW_OPEN_RE = /\{%-?\s*raw\s*-?%\}/g;
6920
+ RAW_CLOSE_RE = /\{%-?\s*endraw\s*-?%\}/g;
6921
+ TAG_KINDS = {
6922
+ "%": { close: "%}", type: "BLOCK" },
6923
+ "{": { close: "}}", type: "VAR" },
6924
+ "#": { close: "#}", type: "COMMENT" }
6925
+ };
6736
6926
  EXPR_NOT_MATCHED = /* @__PURE__ */ Symbol("frond-expression-not-matched");
6737
6927
  ARITHMETIC_OPERATIONS = {
6738
6928
  "+": (left, right) => left + right,
@@ -6743,10 +6933,14 @@ var init_engine = __esm({
6743
6933
  "%": (left, right) => right !== 0 ? left % right : 0,
6744
6934
  "**": (left, right) => left ** right
6745
6935
  };
6936
+ ARITHMETIC_LEVELS = [["+", "-"], ["*", "//", "/", "%"], ["**"]];
6937
+ BARE_KEY_RE = /^[A-Za-z_]\w*$/;
6746
6938
  EXPR_EVALUATORS = [
6939
+ evalCollectionLiteral,
6747
6940
  evalPrimary,
6748
6941
  evalConditional,
6749
6942
  evalConcatOrComparison,
6943
+ evalRangeExpression,
6750
6944
  evalArithmeticExpression,
6751
6945
  evalFilterExpression,
6752
6946
  evalFunctionExpression
@@ -6802,8 +6996,8 @@ var init_engine = __esm({
6802
6996
  },
6803
6997
  title: (v) => String(v).replace(TITLE_WORD_RE, (c) => c.toUpperCase()),
6804
6998
  trim: (v) => String(v).trim(),
6805
- ltrim: (v) => String(v).replace(LEADING_WS_RE, ""),
6806
- rtrim: (v) => String(v).replace(TRAILING_WS_RE, ""),
6999
+ ltrim: (v) => String(v).trimStart(),
7000
+ rtrim: (v) => String(v).trimEnd(),
6807
7001
  length: (v) => {
6808
7002
  if (Array.isArray(v)) return v.length;
6809
7003
  if (typeof v === "string") return v.length;
@@ -6844,7 +7038,7 @@ var init_engine = __esm({
6844
7038
  safe: (v) => v,
6845
7039
  escape: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
6846
7040
  e: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
6847
- striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
7041
+ striptags: (v) => stripTags(String(v)),
6848
7042
  nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
6849
7043
  abs: (v) => typeof v === "number" ? Math.abs(v) : v,
6850
7044
  round: (v, decimals) => {
@@ -6957,6 +7151,7 @@ var init_engine = __esm({
6957
7151
  js_escape: (v) => new SafeString(jsEscape(v))
6958
7152
  };
6959
7153
  _formTokenSessionId = "";
7154
+ _formTokenSessionProvider = () => "";
6960
7155
  _cspNonceProvider = () => "";
6961
7156
  Frond = class _Frond {
6962
7157
  // ── Class-level registries ──────────────────────────────────
@@ -7110,6 +7305,7 @@ var init_engine = __esm({
7110
7305
  this.globals.form_token = (descriptor) => _generateFormToken(descriptor || "");
7111
7306
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
7112
7307
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
7308
+ this.globals.range = frondRange;
7113
7309
  this.globals.dump = (value) => renderDump(value);
7114
7310
  this.globals.csp_nonce = () => _cspNonceProvider();
7115
7311
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
@@ -7446,20 +7642,20 @@ var init_engine = __esm({
7446
7642
  }
7447
7643
  renderVarToken(tokens, index, context, output) {
7448
7644
  const [content, stripB, stripA] = stripTag(tokens[index][1]);
7449
- if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
7645
+ if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].trimEnd();
7450
7646
  const result = this.evalVar(content, context);
7451
7647
  output.push(result !== null && result !== void 0 ? String(result) : "");
7452
7648
  if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
7453
- tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
7649
+ tokens[index + 1] = ["TEXT", tokens[index + 1][1].trimStart()];
7454
7650
  }
7455
7651
  return index + 1;
7456
7652
  }
7457
7653
  renderBlockToken(tokens, index, context, output) {
7458
7654
  const [content, stripB, stripA] = stripTag(tokens[index][1]);
7459
- if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
7655
+ if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].trimEnd();
7460
7656
  const tag = content.split(/\s+/)[0] || "";
7461
7657
  if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
7462
- tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
7658
+ tokens[index + 1] = ["TEXT", tokens[index + 1][1].trimStart()];
7463
7659
  }
7464
7660
  let next;
7465
7661
  if (tag === "if" && this.tagPermitted(tag)) {
@@ -7476,7 +7672,7 @@ var init_engine = __esm({
7476
7672
  next = block.next;
7477
7673
  }
7478
7674
  if (stripA && next < tokens.length && tokens[next][0] === "TEXT") {
7479
- tokens[next] = ["TEXT", tokens[next][1].replace(LEADING_WS_RE, "")];
7675
+ tokens[next] = ["TEXT", tokens[next][1].trimStart()];
7480
7676
  }
7481
7677
  return next;
7482
7678
  }
@@ -7735,7 +7931,7 @@ var init_engine = __esm({
7735
7931
  pushIfBranch(branches, condition, branchTokens, stripBefore) {
7736
7932
  if (stripBefore && branchTokens.length > 0 && branchTokens[branchTokens.length - 1][0] === "TEXT") {
7737
7933
  const last = branchTokens[branchTokens.length - 1];
7738
- branchTokens[branchTokens.length - 1] = ["TEXT", last[1].replace(TRAILING_WS_RE, "")];
7934
+ branchTokens[branchTokens.length - 1] = ["TEXT", last[1].trimEnd()];
7739
7935
  }
7740
7936
  branches.push([condition, branchTokens]);
7741
7937
  }
@@ -7763,7 +7959,7 @@ var init_engine = __esm({
7763
7959
  } else if (tag === "endif") {
7764
7960
  this.pushIfBranch(branches, currentCond, currentTokens, tagStripB);
7765
7961
  if (tagStripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
7766
- tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
7962
+ tokens[i + 1] = ["TEXT", tokens[i + 1][1].trimStart()];
7767
7963
  }
7768
7964
  return { branches, next: i + 1 };
7769
7965
  } else if ((tag === "elseif" || tag === "elif") && depth === 0) {
@@ -9412,7 +9608,8 @@ var init_middleware = __esm({
9412
9608
  "Content-Security-Policy": resolveCspHeader(currentCspNonce()),
9413
9609
  "Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
9414
9610
  "X-XSS-Protection": "0",
9415
- "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
9611
+ "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()",
9612
+ "Cross-Origin-Opener-Policy": process.env.TINA4_COOP ?? "same-origin-allow-popups"
9416
9613
  };
9417
9614
  }
9418
9615
  static beforeSecurity(req2, res) {
@@ -9429,7 +9626,7 @@ var init_middleware = __esm({
9429
9626
  if (process.env.TINA4_CSP === void 0) {
9430
9627
  _SecurityHeadersMiddleware.warnCspDefaultOnce();
9431
9628
  }
9432
- for (const name of ["Content-Security-Policy", "Referrer-Policy", "X-XSS-Protection", "Permissions-Policy"]) {
9629
+ for (const name of ["Content-Security-Policy", "Referrer-Policy", "X-XSS-Protection", "Permissions-Policy", "Cross-Origin-Opener-Policy"]) {
9433
9630
  res.header(name, canonical[name]);
9434
9631
  }
9435
9632
  return [req2, res];
@@ -9513,7 +9710,7 @@ var init_middleware = __esm({
9513
9710
  });
9514
9711
 
9515
9712
  // ../core/src/request.ts
9516
- import { mkdirSync as mkdirSync3, realpathSync as realpathSync2, writeFileSync as writeFileSync2 } from "node:fs";
9713
+ import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, ftruncateSync, lstatSync, mkdirSync as mkdirSync3, openSync as openSync3, realpathSync as realpathSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
9517
9714
  import { dirname as dirname3, join as join6 } from "node:path";
9518
9715
  function makeCaseInsensitiveHeaders(raw) {
9519
9716
  return new Proxy(raw, {
@@ -9549,7 +9746,8 @@ function createRequest(req2) {
9549
9746
  try {
9550
9747
  const url = new URL(req2.url ?? "/", `${proto}://${host}`);
9551
9748
  for (const [key, value] of url.searchParams) {
9552
- query[key] = value;
9749
+ if (isUnsafeFieldName(key)) continue;
9750
+ Reflect.set(query, key, value);
9553
9751
  }
9554
9752
  path10 = url.pathname;
9555
9753
  queryString = url.search.replace(/^\?/, "");
@@ -9561,7 +9759,8 @@ function createRequest(req2) {
9561
9759
  queryString = questionIdx >= 0 ? rawTarget.slice(questionIdx + 1) : "";
9562
9760
  try {
9563
9761
  for (const [key, value] of new URLSearchParams(queryString)) {
9564
- query[key] = value;
9762
+ if (isUnsafeFieldName(key)) continue;
9763
+ Reflect.set(query, key, value);
9565
9764
  }
9566
9765
  } catch {
9567
9766
  }
@@ -9580,7 +9779,8 @@ function createRequest(req2) {
9580
9779
  if (cookieHeader) {
9581
9780
  for (const pair of cookieHeader.split(";")) {
9582
9781
  const [k, ...v] = pair.trim().split("=");
9583
- if (k) cookies[k.trim()] = v.join("=").trim();
9782
+ const name = k ? k.trim() : "";
9783
+ if (name && !isUnsafeFieldName(name)) Reflect.set(cookies, name, v.join("=").trim());
9584
9784
  }
9585
9785
  }
9586
9786
  w.cookies = cookies;
@@ -9671,13 +9871,7 @@ async function parseBody(req2) {
9671
9871
  req2.body = str;
9672
9872
  }
9673
9873
  } else if (contentType.includes("application/x-www-form-urlencoded")) {
9674
- const str = raw.toString("utf-8");
9675
- const params = new URLSearchParams(str);
9676
- const obj = {};
9677
- for (const [key, value] of params) {
9678
- obj[key] = value;
9679
- }
9680
- req2.body = obj;
9874
+ req2.body = nestFormFields([...new URLSearchParams(raw.toString("utf-8"))]);
9681
9875
  } else {
9682
9876
  req2.body = raw.toString("utf-8");
9683
9877
  }
@@ -9687,7 +9881,7 @@ function extractBoundary(contentType) {
9687
9881
  return match ? match[1] ?? match[2] : null;
9688
9882
  }
9689
9883
  function parseMultipart(body, boundary) {
9690
- const fields = {};
9884
+ const pairs = [];
9691
9885
  const files = {};
9692
9886
  const delimiter2 = Buffer.from(`--${boundary}`);
9693
9887
  const closeDelimiter = Buffer.from(`--${boundary}--`);
@@ -9695,7 +9889,7 @@ function parseMultipart(body, boundary) {
9695
9889
  const doubleCrlf = Buffer.from("\r\n\r\n");
9696
9890
  let offset = 0;
9697
9891
  const firstIdx = bufferIndexOf(body, delimiter2, offset);
9698
- if (firstIdx === -1) return { fields, files };
9892
+ if (firstIdx === -1) return { fields: {}, files };
9699
9893
  offset = firstIdx + delimiter2.length;
9700
9894
  if (body[offset] === 13 && body[offset + 1] === 10) {
9701
9895
  offset += 2;
@@ -9719,14 +9913,15 @@ function parseMultipart(body, boundary) {
9719
9913
  content: Buffer.from(content),
9720
9914
  size: content.length
9721
9915
  };
9722
- if (files[disposition.name]) {
9916
+ if (isUnsafeFieldName(disposition.name)) {
9917
+ } else if (files[disposition.name]) {
9723
9918
  const existing = files[disposition.name];
9724
- files[disposition.name] = Array.isArray(existing) ? [...existing, file] : [existing, file];
9919
+ Reflect.set(files, disposition.name, Array.isArray(existing) ? [...existing, file] : [existing, file]);
9725
9920
  } else {
9726
- files[disposition.name] = file;
9921
+ Reflect.set(files, disposition.name, file);
9727
9922
  }
9728
9923
  } else if (disposition.name) {
9729
- fields[disposition.name] = content.toString("utf-8");
9924
+ pairs.push([disposition.name, content.toString("utf-8")]);
9730
9925
  }
9731
9926
  offset = nextDelimIdx + delimiter2.length;
9732
9927
  if (body[offset] === 45 && body[offset + 1] === 45) {
@@ -9736,7 +9931,7 @@ function parseMultipart(body, boundary) {
9736
9931
  offset += 2;
9737
9932
  }
9738
9933
  }
9739
- return { fields, files };
9934
+ return { fields: nestFormFields(pairs), files };
9740
9935
  }
9741
9936
  function saveUpload(file, targetDir, filename) {
9742
9937
  const raw = String(filename ?? file.filename ?? "");
@@ -9752,7 +9947,21 @@ function saveUpload(file, targetDir, filename) {
9752
9947
  if (realParent !== realDir) {
9753
9948
  throw new Error(`refusing to write outside ${targetDir}: ${JSON.stringify(raw)}`);
9754
9949
  }
9755
- writeFileSync2(dest, file.content);
9950
+ if (lstatSync(dest, { throwIfNoEntry: false })?.isSymbolicLink()) unlinkSync3(dest);
9951
+ const flags = constants2.O_WRONLY | constants2.O_CREAT | (constants2.O_NOFOLLOW ?? 0);
9952
+ let fd = openSync3(dest, flags, 384);
9953
+ try {
9954
+ if (fstatSync2(fd).nlink > 1) {
9955
+ closeSync3(fd);
9956
+ fd = -1;
9957
+ unlinkSync3(dest);
9958
+ fd = openSync3(dest, flags | constants2.O_EXCL, 384);
9959
+ }
9960
+ ftruncateSync(fd, 0);
9961
+ writeFileSync2(fd, file.content);
9962
+ } finally {
9963
+ if (fd >= 0) closeSync3(fd);
9964
+ }
9756
9965
  return dest;
9757
9966
  }
9758
9967
  function bufferIndexOf(haystack, needle, offset) {
@@ -9777,10 +9986,98 @@ function parseDisposition(headers) {
9777
9986
  };
9778
9987
  }
9779
9988
  function parsePartContentType(headers) {
9780
- const match = headers.match(/Content-Type:\s*(.+?)(?:\r?\n|$)/i);
9781
- return match?.[1]?.trim() ?? null;
9989
+ const match = headers.match(/Content-Type:([^\r\n]*)/i);
9990
+ const value = match?.[1]?.trim();
9991
+ return value ? value : null;
9992
+ }
9993
+ function nestFormFields(pairs) {
9994
+ const kept = pairs.filter(([name]) => !/(^|[[\]])__proto__([[\]]|$)/.test(name));
9995
+ if (kept.length > FORM_FIELD_LIMIT) return Object.fromEntries(kept);
9996
+ const nested = /* @__PURE__ */ new WeakSet();
9997
+ const node = () => {
9998
+ const created = /* @__PURE__ */ Object.create(null);
9999
+ nested.add(created);
10000
+ return created;
10001
+ };
10002
+ const isNode = (value) => typeof value === "object" && value !== null && nested.has(value);
10003
+ const own = (params, key) => Object.prototype.hasOwnProperty.call(params, key) ? params[key] : void 0;
10004
+ const list = (params, key) => {
10005
+ let current = own(params, key);
10006
+ if (current === void 0) {
10007
+ current = [];
10008
+ Reflect.set(params, key, current);
10009
+ }
10010
+ if (!Array.isArray(current)) throw new RangeError(`expected an array for '${key}'`);
10011
+ return current;
10012
+ };
10013
+ const object = (params, key) => {
10014
+ let current = own(params, key);
10015
+ if (current === void 0) {
10016
+ current = node();
10017
+ Reflect.set(params, key, current);
10018
+ }
10019
+ if (!isNode(current)) throw new RangeError(`expected an object for '${key}'`);
10020
+ return current;
10021
+ };
10022
+ const hasKey = (params, key) => {
10023
+ if (key.includes("[]")) return false;
10024
+ let level = params;
10025
+ for (const part of key.split(/[[\]]+/).filter(Boolean)) {
10026
+ if (!isNode(level) || own(level, part) === void 0) return false;
10027
+ level = level[part];
10028
+ }
10029
+ return true;
10030
+ };
10031
+ const splitName = (name, depth) => {
10032
+ if (depth === 0) {
10033
+ const start2 = name.indexOf("[", 1);
10034
+ return start2 === -1 ? [name, ""] : [name.slice(0, start2), name.slice(start2)];
10035
+ }
10036
+ if (name.startsWith("[]")) return ["[]", name.slice(2)];
10037
+ const close = name.indexOf("]", 1);
10038
+ return name.startsWith("[") && close !== -1 ? [name.slice(1, close), name.slice(close + 1)] : [name, ""];
10039
+ };
10040
+ const putListItem = (items, after, value, depth) => {
10041
+ let childKey = after[2] === "[" && after.endsWith("]") ? after.slice(3, -1) : "";
10042
+ if (childKey === "" || childKey.includes("[") || childKey.includes("]")) childKey = after.slice(2);
10043
+ const last = items[items.length - 1];
10044
+ if (isNode(last) && !hasKey(last, childKey)) put2(last, childKey, value, depth + 1);
10045
+ else items.push(put2(node(), childKey, value, depth + 1));
10046
+ };
10047
+ const put2 = (params, name, value, depth) => {
10048
+ const [key, after] = splitName(name, depth);
10049
+ if (key === "") return void 0;
10050
+ if (after === "" && key === "[]" && depth !== 0) return [value];
10051
+ if (after === "") Reflect.set(params, key, value);
10052
+ else if (after === "[") Reflect.set(params, name, value);
10053
+ else if (after === "[]") list(params, key).push(value);
10054
+ else if (after.startsWith("[]")) putListItem(list(params, key), after, value, depth);
10055
+ else put2(object(params, key), after, value, depth + 1);
10056
+ return params;
10057
+ };
10058
+ const tooDeep = (name) => {
10059
+ let count = 0;
10060
+ for (let at = name.indexOf("["); at !== -1; at = name.indexOf("[", at + 1)) {
10061
+ if (++count >= FORM_DEPTH_LIMIT) return true;
10062
+ }
10063
+ return false;
10064
+ };
10065
+ const fields = node();
10066
+ for (const [name, value] of kept) {
10067
+ if (name.length > FORM_NAME_LIMIT || tooDeep(name)) {
10068
+ Reflect.set(fields, name, value);
10069
+ continue;
10070
+ }
10071
+ try {
10072
+ put2(fields, name, value, 0);
10073
+ } catch (error2) {
10074
+ if (!(error2 instanceof RangeError)) throw error2;
10075
+ Reflect.set(fields, name, value);
10076
+ }
10077
+ }
10078
+ return fields;
9782
10079
  }
9783
- var DEFAULT_MAX_UPLOAD_SIZE, warnedLimits, PayloadTooLargeError;
10080
+ var DEFAULT_MAX_UPLOAD_SIZE, warnedLimits, PayloadTooLargeError, FORM_DEPTH_LIMIT, FORM_FIELD_LIMIT, FORM_NAME_LIMIT, isUnsafeFieldName;
9784
10081
  var init_request = __esm({
9785
10082
  "../core/src/request.ts"() {
9786
10083
  "use strict";
@@ -9795,6 +10092,10 @@ var init_request = __esm({
9795
10092
  this.name = "PayloadTooLargeError";
9796
10093
  }
9797
10094
  };
10095
+ FORM_DEPTH_LIMIT = 32;
10096
+ FORM_FIELD_LIMIT = 4096;
10097
+ FORM_NAME_LIMIT = 1024;
10098
+ isUnsafeFieldName = (key) => key === "__proto__" || key === "constructor" || key === "prototype";
9798
10099
  }
9799
10100
  });
9800
10101
 
@@ -10301,7 +10602,7 @@ var init_routeDiscovery = __esm({
10301
10602
 
10302
10603
  // ../core/src/portTakeover.ts
10303
10604
  import { execFileSync } from "node:child_process";
10304
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "node:fs";
10605
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
10305
10606
  import { join as join8 } from "node:path";
10306
10607
  function isTruthy2(value) {
10307
10608
  return ["true", "1", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
@@ -10355,7 +10656,7 @@ function readPidfile(port, baseDir) {
10355
10656
  }
10356
10657
  function removePidfile(port, baseDir) {
10357
10658
  try {
10358
- unlinkSync3(pidfilePath(port, baseDir));
10659
+ unlinkSync4(pidfilePath(port, baseDir));
10359
10660
  } catch {
10360
10661
  }
10361
10662
  }
@@ -10456,21 +10757,6 @@ var init_portTakeover = __esm({
10456
10757
  }
10457
10758
  });
10458
10759
 
10459
- // ../core/src/etag.ts
10460
- function etagMatches(ifNoneMatch, etag) {
10461
- const strip = (tag) => tag.trim().replace(/^W\//, "");
10462
- const target = strip(etag);
10463
- return ifNoneMatch.split(",").some((candidate) => {
10464
- const trimmed = candidate.trim();
10465
- return trimmed === "*" || strip(trimmed) === target;
10466
- });
10467
- }
10468
- var init_etag = __esm({
10469
- "../core/src/etag.ts"() {
10470
- "use strict";
10471
- }
10472
- });
10473
-
10474
10760
  // ../core/src/sessionHandlers/syncBridge.ts
10475
10761
  import { Worker } from "node:worker_threads";
10476
10762
  function getBridge(key, workerSource, workerData) {
@@ -11839,14 +12125,18 @@ __export(session_exports, {
11839
12125
  Session: () => Session,
11840
12126
  VALID_SESSION_BACKENDS: () => VALID_SESSION_BACKENDS,
11841
12127
  buildSessionCookie: () => buildSessionCookie,
12128
+ currentFormTokenSessionId: () => currentFormTokenSessionId,
11842
12129
  isSecureScheme: () => isSecureScheme,
11843
12130
  isValidSessionId: () => isValidSessionId,
12131
+ runWithFormTokenSession: () => runWithFormTokenSession,
11844
12132
  sessionCookieName: () => sessionCookieName,
11845
12133
  sessionCookieToSend: () => sessionCookieToSend,
11846
- sessionStrictMode: () => sessionStrictMode
12134
+ sessionStrictMode: () => sessionStrictMode,
12135
+ setFormTokenSession: () => setFormTokenSession
11847
12136
  });
12137
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
11848
12138
  import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
11849
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "node:fs";
12139
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readdirSync as readdirSync3 } from "node:fs";
11850
12140
  import { join as join9 } from "node:path";
11851
12141
  function isValidSessionId(sessionId) {
11852
12142
  return typeof sessionId === "string" && SESSION_ID_PATTERN.test(sessionId);
@@ -11912,7 +12202,21 @@ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
11912
12202
  const socketEncrypted = rawReq.socket?.encrypted === true;
11913
12203
  return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
11914
12204
  }
11915
- var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
12205
+ function runWithFormTokenSession(fn) {
12206
+ return sessionStore.run({ session: null }, fn);
12207
+ }
12208
+ function setFormTokenSession(session) {
12209
+ const store = sessionStore.getStore();
12210
+ if (store) store.session = session;
12211
+ }
12212
+ function currentFormTokenSessionId() {
12213
+ const session = sessionStore.getStore()?.session;
12214
+ const sid = session?.getSessionId();
12215
+ if (!session || !sid) return "";
12216
+ if (session.isFresh()) session.set("_tina4_csrf", true);
12217
+ return sid;
12218
+ }
12219
+ var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session, sessionStore;
11916
12220
  var init_session = __esm({
11917
12221
  "../core/src/session.ts"() {
11918
12222
  "use strict";
@@ -11996,7 +12300,7 @@ var init_session = __esm({
11996
12300
  const wrapper = JSON.parse(raw);
11997
12301
  if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
11998
12302
  try {
11999
- unlinkSync4(filePath);
12303
+ unlinkSync5(filePath);
12000
12304
  } catch {
12001
12305
  }
12002
12306
  return null;
@@ -12015,7 +12319,7 @@ var init_session = __esm({
12015
12319
  destroy(sessionId) {
12016
12320
  const filePath = this.filePath(sessionId);
12017
12321
  try {
12018
- if (existsSync8(filePath)) unlinkSync4(filePath);
12322
+ if (existsSync8(filePath)) unlinkSync5(filePath);
12019
12323
  } catch {
12020
12324
  }
12021
12325
  }
@@ -12031,11 +12335,11 @@ var init_session = __esm({
12031
12335
  const raw = readFileSync6(fullPath, "utf-8");
12032
12336
  const wrapper = JSON.parse(raw);
12033
12337
  if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
12034
- unlinkSync4(fullPath);
12338
+ unlinkSync5(fullPath);
12035
12339
  }
12036
12340
  } catch {
12037
12341
  try {
12038
- unlinkSync4(fullPath);
12342
+ unlinkSync5(fullPath);
12039
12343
  } catch {
12040
12344
  }
12041
12345
  }
@@ -12555,6 +12859,22 @@ var init_session = __esm({
12555
12859
  this.loaded = /* @__PURE__ */ new Map();
12556
12860
  }
12557
12861
  };
12862
+ sessionStore = new AsyncLocalStorage3();
12863
+ }
12864
+ });
12865
+
12866
+ // ../core/src/etag.ts
12867
+ function etagMatches(ifNoneMatch, etag) {
12868
+ const strip = (tag) => tag.trim().replace(/^W\//, "");
12869
+ const target = strip(etag);
12870
+ return ifNoneMatch.split(",").some((candidate) => {
12871
+ const trimmed = candidate.trim();
12872
+ return trimmed === "*" || strip(trimmed) === target;
12873
+ });
12874
+ }
12875
+ var init_etag = __esm({
12876
+ "../core/src/etag.ts"() {
12877
+ "use strict";
12558
12878
  }
12559
12879
  });
12560
12880
 
@@ -12708,6 +13028,7 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
12708
13028
  return;
12709
13029
  }
12710
13030
  req2.session = sess;
13031
+ setFormTokenSession(sess);
12711
13032
  const origEnd = rawRes.end.bind(rawRes);
12712
13033
  let finalised = false;
12713
13034
  rawRes.end = function(...args) {
@@ -12736,6 +13057,7 @@ var _resetRequestCaches, COMPRESSIBLE_PREFIXES;
12736
13057
  var init_dispatchPipeline = __esm({
12737
13058
  "../core/src/dispatchPipeline.ts"() {
12738
13059
  "use strict";
13060
+ init_session();
12739
13061
  init_logger();
12740
13062
  init_etag();
12741
13063
  COMPRESSIBLE_PREFIXES = [
@@ -12885,14 +13207,21 @@ function parseStack(stack) {
12885
13207
  const frames = [];
12886
13208
  const lines = stack.split("\n");
12887
13209
  for (const line of lines) {
12888
- let match = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
12889
- if (match) {
12890
- frames.push({ func: match[1], file: match[2], line: parseInt(match[3], 10), column: parseInt(match[4], 10) });
12891
- continue;
13210
+ const t = line.trim();
13211
+ if (!t.startsWith("at ")) continue;
13212
+ const rest = t.slice(3);
13213
+ let func = "{anonymous}";
13214
+ let loc = rest;
13215
+ if (rest.endsWith(")")) {
13216
+ const open = rest.lastIndexOf(" (");
13217
+ if (open >= 0) {
13218
+ func = rest.slice(0, open);
13219
+ loc = rest.slice(open + 2, -1);
13220
+ }
12892
13221
  }
12893
- match = line.match(/^\s*at\s+(.+?):(\d+):(\d+)/);
12894
- if (match) {
12895
- frames.push({ func: "{anonymous}", file: match[1], line: parseInt(match[2], 10), column: parseInt(match[3], 10) });
13222
+ const lc = loc.match(/:(\d+):(\d+)$/);
13223
+ if (lc && lc.index !== void 0) {
13224
+ frames.push({ func, file: loc.slice(0, lc.index), line: parseInt(lc[1], 10), column: parseInt(lc[2], 10) });
12896
13225
  }
12897
13226
  }
12898
13227
  return frames;
@@ -13217,7 +13546,7 @@ var init_rateLimiter = __esm({
13217
13546
  });
13218
13547
 
13219
13548
  // ../core/src/devMailbox.ts
13220
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync5, existsSync as existsSync9 } from "node:fs";
13549
+ import { mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync6, existsSync as existsSync9 } from "node:fs";
13221
13550
  import { join as join11 } from "node:path";
13222
13551
  import { randomUUID } from "node:crypto";
13223
13552
  var DevMailbox;
@@ -13349,7 +13678,7 @@ var init_devMailbox = __esm({
13349
13678
  const filePath = join11(this.mailboxDir, folder, `${msgId}.json`);
13350
13679
  if (existsSync9(filePath)) {
13351
13680
  try {
13352
- unlinkSync5(filePath);
13681
+ unlinkSync6(filePath);
13353
13682
  deleted = true;
13354
13683
  } catch {
13355
13684
  }
@@ -13368,7 +13697,7 @@ var init_devMailbox = __esm({
13368
13697
  if (existsSync9(dir)) {
13369
13698
  const files = readdirSync4(dir).filter((file) => file.endsWith(".json"));
13370
13699
  for (const file of files) {
13371
- unlinkSync5(join11(dir, file));
13700
+ unlinkSync6(join11(dir, file));
13372
13701
  }
13373
13702
  }
13374
13703
  } catch {
@@ -13512,7 +13841,7 @@ function enginePath() {
13512
13841
  function runEngine(target) {
13513
13842
  const binary = enginePath();
13514
13843
  if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
13515
- const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
13844
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json", "--no-history"], {
13516
13845
  encoding: "utf8",
13517
13846
  timeout: 6e4,
13518
13847
  maxBuffer: 64 * 1024 * 1024
@@ -16173,21 +16502,45 @@ function safePath(projectRoot3, relPath) {
16173
16502
  if (resolved !== projectRoot3 && !resolved.startsWith(rootPrefix)) {
16174
16503
  throw new Error(`Path escapes project directory: ${relPath}`);
16175
16504
  }
16176
- let real = null;
16505
+ let realRoot;
16177
16506
  try {
16178
- real = fs4.realpathSync(resolved);
16507
+ realRoot = fs4.realpathSync(projectRoot3);
16179
16508
  } catch {
16180
- real = null;
16509
+ return resolved;
16181
16510
  }
16182
- if (real !== null) {
16183
- const realRoot = fs4.realpathSync(projectRoot3);
16184
- const realPrefix = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
16185
- if (real !== realRoot && !real.startsWith(realPrefix)) {
16186
- throw new Error(`Path escapes project directory: ${relPath}`);
16187
- }
16511
+ const real = realPathOf(resolved);
16512
+ const realPrefix = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
16513
+ if (real === null || real !== realRoot && !real.startsWith(realPrefix)) {
16514
+ throw new Error(`Path escapes project directory: ${relPath}`);
16188
16515
  }
16189
16516
  return resolved;
16190
16517
  }
16518
+ function realPathOf(p, hops = 0) {
16519
+ let probe = p;
16520
+ while (!fs4.existsSync(probe) && !isSymlink(probe) && path3.dirname(probe) !== probe) {
16521
+ probe = path3.dirname(probe);
16522
+ }
16523
+ try {
16524
+ return fs4.realpathSync.native(probe);
16525
+ } catch {
16526
+ if (hops >= 40) return null;
16527
+ let target;
16528
+ try {
16529
+ target = fs4.readlinkSync(probe);
16530
+ } catch {
16531
+ return null;
16532
+ }
16533
+ const base = path3.isAbsolute(target) ? target : path3.dirname(probe) + path3.sep + target;
16534
+ return realPathOf(base + p.slice(probe.length), hops + 1);
16535
+ }
16536
+ }
16537
+ function isSymlink(p) {
16538
+ try {
16539
+ return fs4.lstatSync(p).isSymbolicLink();
16540
+ } catch {
16541
+ return false;
16542
+ }
16543
+ }
16191
16544
  function redactEnv(key, value) {
16192
16545
  const sensitive = ["secret", "password", "token", "key", "credential", "api_key"];
16193
16546
  if (sensitive.some((s) => key.toLowerCase().includes(s))) {
@@ -17032,8 +17385,10 @@ function registerDevTools(server) {
17032
17385
  return { error: `Invalid path ${JSON.stringify(rel)}: ${proseErr}` };
17033
17386
  }
17034
17387
  rel = normalizeCoderPath(projectRoot4, rel);
17035
- const resolved = path3.resolve(projectRoot4, rel);
17036
- if (!resolved.startsWith(projectRoot4)) {
17388
+ let resolved;
17389
+ try {
17390
+ resolved = safePath(projectRoot4, rel);
17391
+ } catch {
17037
17392
  return { error: `Path escapes project directory: ${rel}` };
17038
17393
  }
17039
17394
  if (!fs4.existsSync(resolved) || !fs4.statSync(resolved).isFile()) {
@@ -17330,6 +17685,7 @@ var req, PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNA
17330
17685
  var init_mcp = __esm({
17331
17686
  "../core/src/mcp.ts"() {
17332
17687
  "use strict";
17688
+ init_slashTrim();
17333
17689
  init_version();
17334
17690
  req = createRequire2(import.meta.url);
17335
17691
  PARSE_ERROR = -32700;
@@ -17356,7 +17712,7 @@ var init_mcp = __esm({
17356
17712
  // threaded, so this in-process map is the whole coordination mechanism.
17357
17713
  _sseChannels = /* @__PURE__ */ new Map();
17358
17714
  constructor(mcpPath, name = "Tina4 MCP", version = "1.0.0") {
17359
- this.path = mcpPath.replace(/\/+$/, "");
17715
+ this.path = rtrimSlashes(mcpPath);
17360
17716
  this.name = name;
17361
17717
  this.version = version;
17362
17718
  _McpServer._instances.push(this);
@@ -21140,7 +21496,7 @@ var init_job = __esm({
21140
21496
  });
21141
21497
 
21142
21498
  // ../core/src/queueBackends/liteBackend.ts
21143
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
21499
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync7, existsSync as existsSync16 } from "node:fs";
21144
21500
  import { join as join18 } from "node:path";
21145
21501
  import { randomUUID as randomUUID5 } from "node:crypto";
21146
21502
  var LiteBackend;
@@ -21310,7 +21666,7 @@ var init_liteBackend = __esm({
21310
21666
  }
21311
21667
  if (record.availableAt && record.availableAt > now) continue;
21312
21668
  try {
21313
- unlinkSync6(filePath);
21669
+ unlinkSync7(filePath);
21314
21670
  } catch {
21315
21671
  continue;
21316
21672
  }
@@ -21344,10 +21700,10 @@ var init_liteBackend = __esm({
21344
21700
  job.priority = job.priority ?? 0;
21345
21701
  this.writeReserved(queue, job);
21346
21702
  try {
21347
- unlinkSync6(filePath);
21703
+ unlinkSync7(filePath);
21348
21704
  } catch {
21349
21705
  try {
21350
- unlinkSync6(this.reservedPath(queue, job.id));
21706
+ unlinkSync7(this.reservedPath(queue, job.id));
21351
21707
  } catch {
21352
21708
  }
21353
21709
  continue;
@@ -21369,10 +21725,10 @@ var init_liteBackend = __esm({
21369
21725
  job.priority = job.priority ?? 0;
21370
21726
  this.writeReserved(queue, job);
21371
21727
  try {
21372
- unlinkSync6(filePath);
21728
+ unlinkSync7(filePath);
21373
21729
  } catch {
21374
21730
  try {
21375
- unlinkSync6(this.reservedPath(queue, job.id));
21731
+ unlinkSync7(this.reservedPath(queue, job.id));
21376
21732
  } catch {
21377
21733
  }
21378
21734
  continue;
@@ -21387,7 +21743,7 @@ var init_liteBackend = __esm({
21387
21743
  */
21388
21744
  clearReservation(queue, jobId) {
21389
21745
  try {
21390
- unlinkSync6(this.reservedPath(queue, jobId));
21746
+ unlinkSync7(this.reservedPath(queue, jobId));
21391
21747
  } catch {
21392
21748
  }
21393
21749
  }
@@ -21430,7 +21786,7 @@ var init_liteBackend = __esm({
21430
21786
  try {
21431
21787
  const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21432
21788
  for (const file of files) {
21433
- unlinkSync6(join18(dir, file));
21789
+ unlinkSync7(join18(dir, file));
21434
21790
  count++;
21435
21791
  }
21436
21792
  } catch {
@@ -21440,7 +21796,7 @@ var init_liteBackend = __esm({
21440
21796
  if (existsSync16(failedDir)) {
21441
21797
  const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21442
21798
  for (const file of files) {
21443
- unlinkSync6(join18(failedDir, file));
21799
+ unlinkSync7(join18(failedDir, file));
21444
21800
  count++;
21445
21801
  }
21446
21802
  }
@@ -21451,7 +21807,7 @@ var init_liteBackend = __esm({
21451
21807
  if (existsSync16(reservedDir)) {
21452
21808
  const files = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21453
21809
  for (const file of files) {
21454
- unlinkSync6(join18(reservedDir, file));
21810
+ unlinkSync7(join18(reservedDir, file));
21455
21811
  count++;
21456
21812
  }
21457
21813
  }
@@ -21509,7 +21865,7 @@ var init_liteBackend = __esm({
21509
21865
  const prefix = this.nextPrefix();
21510
21866
  const queueDir = join18(this.basePath, q);
21511
21867
  writeFileSync9(join18(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
21512
- unlinkSync6(filePath);
21868
+ unlinkSync7(filePath);
21513
21869
  return true;
21514
21870
  }
21515
21871
  }
@@ -21545,7 +21901,7 @@ var init_liteBackend = __esm({
21545
21901
  const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21546
21902
  for (const file of files) {
21547
21903
  try {
21548
- unlinkSync6(join18(failedDir, file));
21904
+ unlinkSync7(join18(failedDir, file));
21549
21905
  count++;
21550
21906
  } catch {
21551
21907
  }
@@ -21560,7 +21916,7 @@ var init_liteBackend = __esm({
21560
21916
  try {
21561
21917
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
21562
21918
  if (job.status === status2) {
21563
- unlinkSync6(join18(dir, file));
21919
+ unlinkSync7(join18(dir, file));
21564
21920
  count++;
21565
21921
  }
21566
21922
  } catch {
@@ -21595,7 +21951,7 @@ var init_liteBackend = __esm({
21595
21951
  job.delayUntil = null;
21596
21952
  const prefix = this.nextPrefix();
21597
21953
  writeFileSync9(join18(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
21598
- unlinkSync6(filePath);
21954
+ unlinkSync7(filePath);
21599
21955
  count++;
21600
21956
  } catch {
21601
21957
  }
@@ -21627,10 +21983,10 @@ var init_liteBackend = __esm({
21627
21983
  job.priority = job.priority ?? 0;
21628
21984
  this.writeReserved(queue, job);
21629
21985
  try {
21630
- unlinkSync6(filePath);
21986
+ unlinkSync7(filePath);
21631
21987
  } catch {
21632
21988
  try {
21633
- unlinkSync6(this.reservedPath(queue, job.id));
21989
+ unlinkSync7(this.reservedPath(queue, job.id));
21634
21990
  } catch {
21635
21991
  }
21636
21992
  continue;
@@ -21732,7 +22088,7 @@ var init_liteBackend = __esm({
21732
22088
  retryJob(queue, job, delaySeconds) {
21733
22089
  this.clearReservation(queue, job.id);
21734
22090
  try {
21735
- unlinkSync6(join18(this.ensureFailedDir(queue), `${job.id}.queue-data`));
22091
+ unlinkSync7(join18(this.ensureFailedDir(queue), `${job.id}.queue-data`));
21736
22092
  } catch {
21737
22093
  }
21738
22094
  job.attempts = (job.attempts || 0) + 1;
@@ -22282,7 +22638,8 @@ function archiveDir() {
22282
22638
  return p;
22283
22639
  }
22284
22640
  function slugify(title) {
22285
- const slug = (title || "").trim().toLowerCase().replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
22641
+ const dashed = (title || "").trim().toLowerCase().replace(/[^A-Za-z0-9_-]+/g, "-");
22642
+ const slug = trimChar(dashed, 45);
22286
22643
  return slug.slice(0, 80) || `plan-${Math.floor(Date.now() / 1e3)}`;
22287
22644
  }
22288
22645
  function parse(text) {
@@ -22419,6 +22776,7 @@ var PLAN_DIR, CURRENT_FILE, ARCHIVE_SUBDIR, STEP_RE, Plan;
22419
22776
  var init_plan = __esm({
22420
22777
  "../core/src/plan.ts"() {
22421
22778
  "use strict";
22779
+ init_slashTrim();
22422
22780
  PLAN_DIR = "plan";
22423
22781
  CURRENT_FILE = ".current";
22424
22782
  ARCHIVE_SUBDIR = "done";
@@ -22690,7 +23048,7 @@ var init_plan = __esm({
22690
23048
  const existing = (currentPlan.steps || []).map((s) => s.text || "");
22691
23049
  const title = currentPlan.title || target;
22692
23050
  const goal = currentPlan.goal || "";
22693
- const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11437/api/chat";
23051
+ const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11435/api/chat";
22694
23052
  const aiModel = process.env.TINA4_AI_MODEL || "qwen2.5-coder:14b";
22695
23053
  const prompts = fleshPrompt(title, goal, existing, prompt);
22696
23054
  const fetched = await fetchFleshReply(aiUrl, aiModel, prompts);
@@ -23658,7 +24016,7 @@ function relativePath(absPath, projectRoot3, frameworkRoots) {
23658
24016
  }
23659
24017
  function tokenise(text) {
23660
24018
  if (!text) return [];
23661
- const expanded = text.replace(CAMEL_SPLIT, (_m, a, b, c, d) => a ? `${a} ${b}` : `${c} ${d}`).toLowerCase();
24019
+ const expanded = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ").toLowerCase();
23662
24020
  return expanded.split(/[\s_\-./:,;()\[\]{}\\]+/).filter((p) => p.length > 0);
23663
24021
  }
23664
24022
  function summariseDoc(doc) {
@@ -23889,7 +24247,7 @@ function returnFromSignature(signature) {
23889
24247
  const rest = signature.slice(close + 1).trim();
23890
24248
  return rest.startsWith(":") ? rest.slice(1).trim() : "";
23891
24249
  }
23892
- var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
24250
+ var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
23893
24251
  var init_docs = __esm({
23894
24252
  "../core/src/docs.ts"() {
23895
24253
  "use strict";
@@ -24028,7 +24386,6 @@ var init_docs = __esm({
24028
24386
  __FILENAME = fileURLToPath5(import.meta.url);
24029
24387
  __DIRNAME = path8.dirname(__FILENAME);
24030
24388
  USER_DIRS = ["src/orm", "src/routes", "src/app", "src/services", "src/models"];
24031
- CAMEL_SPLIT = /([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
24032
24389
  FENCE_RE = new RegExp(
24033
24390
  String.fromCharCode(96, 96, 96) + "[a-zA-Z0-9_+-]*\\n([\\s\\S]*?)\\n" + String.fromCharCode(96, 96, 96),
24034
24391
  "g"
@@ -24402,7 +24759,7 @@ ${end}
24402
24759
 
24403
24760
  // ../core/src/devAdmin.ts
24404
24761
  import { cpus as osCpus } from "node:os";
24405
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync13, mkdirSync as mkdirSync14, copyFileSync as copyFileSync2, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24762
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync13, mkdirSync as mkdirSync14, copyFileSync as copyFileSync2, statSync as statSync15, openSync as openSync5, closeSync as closeSync5, fstatSync as fstatSync3, fchmodSync as fchmodSync2, ftruncateSync as ftruncateSync2, constants as constants4, realpathSync as realpathSync6, lstatSync as lstatSync3 } from "node:fs";
24406
24763
  import { join as join22, dirname as dirname11, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24407
24764
  import { fileURLToPath as fileURLToPath6 } from "node:url";
24408
24765
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
@@ -24601,8 +24958,41 @@ function quoteTableRef(name) {
24601
24958
  }
24602
24959
  return parts.map((p) => `"${p}"`).join(".");
24603
24960
  }
24961
+ function parseSeedRequest(url, body) {
24962
+ const b = body ?? {};
24963
+ const table2 = String(url.searchParams.get("table") ?? b.table ?? "");
24964
+ const count = parseInt(String(url.searchParams.get("count") ?? b.count ?? "10"), 10) || 10;
24965
+ const seedRaw = url.searchParams.get("seed") ?? b.seed;
24966
+ const seed = seedRaw !== void 0 && seedRaw !== null && String(seedRaw) !== "" ? Number.isNaN(parseInt(String(seedRaw), 10)) ? void 0 : parseInt(String(seedRaw), 10) : void 0;
24967
+ const clear = String(url.searchParams.get("clear") ?? b.clear ?? "") === "true" || b.clear === true;
24968
+ const strict = String(url.searchParams.get("strict") ?? b.strict ?? "") === "true" || b.strict === true;
24969
+ return { table: table2, count, seed, clear, strict };
24970
+ }
24971
+ function buildSeedFieldMap(columns, fake) {
24972
+ const rules = [
24973
+ [(n) => n.includes("email"), () => fake.email()],
24974
+ [(n) => n.includes("name"), () => fake.name()],
24975
+ [(n) => n.includes("phone"), () => fake.phone()],
24976
+ [(n) => n.includes("address") || n.includes("city") || n.includes("country"), () => fake.address()],
24977
+ [(n) => n.includes("url") || n.includes("website"), () => fake.url()],
24978
+ [(_n, t) => t.includes("int"), () => fake.integer(1, 1e3)],
24979
+ [(_n, t) => /real|float|double|numeric|decimal/.test(t), () => fake.numeric(0, 1e3, 2)],
24980
+ [(_n, t) => t.includes("bool"), () => fake.boolean()],
24981
+ [(_n, t) => t.includes("date") || t.includes("time"), () => fake.date()]
24982
+ ];
24983
+ const sentence = () => fake.sentence(3);
24984
+ const fieldMap = {};
24985
+ for (const col of columns) {
24986
+ if (col.name.toLowerCase() === "id" || col.primaryKey === true) continue;
24987
+ const name = col.name.toLowerCase();
24988
+ const type2 = col.type.toLowerCase();
24989
+ const rule = rules.find(([matches]) => matches(name, type2));
24990
+ fieldMap[col.name] = rule ? rule[1] : sentence;
24991
+ }
24992
+ return fieldMap;
24993
+ }
24604
24994
  function supervisorBaseUrl() {
24605
- const explicit = (process.env.TINA4_SUPERVISOR_URL ?? "").replace(/\/+$/, "");
24995
+ const explicit = rtrimSlashes(process.env.TINA4_SUPERVISOR_URL ?? "");
24606
24996
  if (explicit) return explicit;
24607
24997
  const agentPort = (process.env.TINA4_AGENT_PORT ?? "").trim();
24608
24998
  if (/^\d+$/.test(agentPort)) return `http://127.0.0.1:${parseInt(agentPort, 10)}`;
@@ -24610,6 +25000,42 @@ function supervisorBaseUrl() {
24610
25000
  if (/^\d+$/.test(fwPort)) return `http://127.0.0.1:${parseInt(fwPort, 10) + 2e3}`;
24611
25001
  return "http://127.0.0.1:9145";
24612
25002
  }
25003
+ function buildSupervisorBody(method, body) {
25004
+ if (method !== "POST" && method !== "PUT" && method !== "PATCH" && method !== "DELETE") return void 0;
25005
+ if (body === void 0 || body === null) return void 0;
25006
+ if (typeof body === "string") return body;
25007
+ if (typeof body !== "object") return void 0;
25008
+ let outBody = body;
25009
+ if (!Array.isArray(body)) {
25010
+ const pf = body.plan_file;
25011
+ if (typeof pf === "string" && pf && !pf.includes("/")) outBody = { ...body, plan_file: `plan/${pf}` };
25012
+ }
25013
+ return JSON.stringify(outBody);
25014
+ }
25015
+ async function streamEventStream(res, upstream, timer) {
25016
+ res.raw.writeHead(upstream.status || 200, {
25017
+ "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream",
25018
+ "Cache-Control": "no-cache",
25019
+ Connection: "keep-alive"
25020
+ });
25021
+ if (typeof res.raw.flushHeaders === "function") res.raw.flushHeaders();
25022
+ if (!upstream.body) {
25023
+ res.raw.end();
25024
+ clearTimeout(timer);
25025
+ return;
25026
+ }
25027
+ const reader = upstream.body.getReader();
25028
+ try {
25029
+ while (true) {
25030
+ const { done, value } = await reader.read();
25031
+ if (done) break;
25032
+ if (value) res.raw.write(Buffer.from(value));
25033
+ }
25034
+ } finally {
25035
+ clearTimeout(timer);
25036
+ res.raw.end();
25037
+ }
25038
+ }
24613
25039
  async function proxyToSupervisor(req2, res, downstreamPath) {
24614
25040
  const base = supervisorBaseUrl();
24615
25041
  let qs = "";
@@ -24620,24 +25046,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
24620
25046
  }
24621
25047
  const target = `${base}${downstreamPath}${qs}`;
24622
25048
  const method = (req2.method ?? "GET").toUpperCase();
24623
- let bodyText;
24624
- if (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") {
24625
- const body = req2.body;
24626
- if (body !== void 0 && body !== null) {
24627
- if (typeof body === "string") {
24628
- bodyText = body;
24629
- } else if (typeof body === "object") {
24630
- let outBody = body;
24631
- if (!Array.isArray(body)) {
24632
- const pf = body.plan_file;
24633
- if (typeof pf === "string" && pf && !pf.includes("/")) {
24634
- outBody = { ...body, plan_file: `plan/${pf}` };
24635
- }
24636
- }
24637
- bodyText = JSON.stringify(outBody);
24638
- }
24639
- }
24640
- }
25049
+ const bodyText = buildSupervisorBody(method, req2.body);
24641
25050
  const timeoutMs = downstreamPath === "/execute" || downstreamPath === "/chat" ? 6e5 : 3e4;
24642
25051
  const ctrl = new AbortController();
24643
25052
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -24663,30 +25072,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
24663
25072
  }
24664
25073
  const ct = (upstream.headers.get("content-type") ?? "").toLowerCase();
24665
25074
  if (ct.includes("text/event-stream")) {
24666
- res.raw.writeHead(upstream.status || 200, {
24667
- "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream",
24668
- "Cache-Control": "no-cache",
24669
- Connection: "keep-alive"
24670
- });
24671
- if (typeof res.raw.flushHeaders === "function") {
24672
- res.raw.flushHeaders();
24673
- }
24674
- if (!upstream.body) {
24675
- res.raw.end();
24676
- clearTimeout(timer);
24677
- return;
24678
- }
24679
- const reader = upstream.body.getReader();
24680
- try {
24681
- while (true) {
24682
- const { done, value } = await reader.read();
24683
- if (done) break;
24684
- if (value) res.raw.write(Buffer.from(value));
24685
- }
24686
- } finally {
24687
- clearTimeout(timer);
24688
- res.raw.end();
24689
- }
25075
+ await streamEventStream(res, upstream, timer);
24690
25076
  return;
24691
25077
  }
24692
25078
  clearTimeout(timer);
@@ -24803,13 +25189,13 @@ function handleGalleryDeploy(router) {
24803
25189
  res.json({ deployed: name, files: copied });
24804
25190
  };
24805
25191
  }
24806
- function realPathOf(target) {
25192
+ function realPathOf2(target) {
24807
25193
  let existing = target;
24808
25194
  const tail = [];
24809
25195
  for (; ; ) {
24810
25196
  let present = false;
24811
25197
  try {
24812
- lstatSync(existing);
25198
+ lstatSync3(existing);
24813
25199
  present = true;
24814
25200
  } catch {
24815
25201
  }
@@ -24821,9 +25207,9 @@ function realPathOf(target) {
24821
25207
  }
24822
25208
  function safeJoin(projectRoot3, rel, extraRoots = []) {
24823
25209
  try {
24824
- const root = realPathOf(resolve12(projectRoot3));
24825
- const resolved = realPathOf(resolve12(root, rel));
24826
- const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf(resolve12(r)))];
25210
+ const root = realPathOf2(resolve12(projectRoot3));
25211
+ const resolved = realPathOf2(resolve12(root, rel));
25212
+ const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf2(resolve12(r)))];
24827
25213
  for (const r of roots) {
24828
25214
  if (resolved === r || resolved.startsWith(r.endsWith(sep6) ? r : r + sep6)) return resolved;
24829
25215
  }
@@ -24832,7 +25218,7 @@ function safeJoin(projectRoot3, rel, extraRoots = []) {
24832
25218
  return null;
24833
25219
  }
24834
25220
  function resolvedRelative(projectRoot3, resolvedPath) {
24835
- return relative9(realPathOf(resolve12(projectRoot3)), resolvedPath);
25221
+ return relative9(realPathOf2(resolve12(projectRoot3)), resolvedPath);
24836
25222
  }
24837
25223
  function devFilesHidden(name) {
24838
25224
  if (DEV_FILES_IGNORED.has(name)) return true;
@@ -24860,7 +25246,7 @@ async function devGitInfo(root) {
24860
25246
  if (!inside || inside.trim() !== "true") return empty;
24861
25247
  const branch = (git(["rev-parse", "--abbrev-ref", "HEAD"]) ?? "").trim();
24862
25248
  const topRaw = git(["rev-parse", "--show-toplevel"]);
24863
- const gitRoot = topRaw && topRaw.trim() !== "" ? topRaw.trim().replace(/\\/g, "/").replace(/\/+$/, "") : null;
25249
+ const gitRoot = topRaw && topRaw.trim() !== "" ? rtrimSlashes(topRaw.trim().replace(/\\/g, "/")) : null;
24864
25250
  const status2 = /* @__PURE__ */ new Map();
24865
25251
  const porcelain = git(["status", "--porcelain", "-uall"]);
24866
25252
  if (porcelain) {
@@ -24879,6 +25265,44 @@ async function devGitInfo(root) {
24879
25265
  return empty;
24880
25266
  }
24881
25267
  }
25268
+ function devGitLabel(gitPath, isDir, gitStatus) {
25269
+ const code = gitStatus.get(gitPath);
25270
+ if (code !== void 0) return devGitStatusLabel(code);
25271
+ if (isDir) {
25272
+ const prefix = gitPath + "/";
25273
+ for (const [gf, gc] of gitStatus) {
25274
+ if (gf.startsWith(prefix)) return gc === "??" ? "untracked" : "modified";
25275
+ }
25276
+ }
25277
+ return "clean";
25278
+ }
25279
+ function devDirHasVisibleChild(full) {
25280
+ try {
25281
+ for (const c of readdirSync13(full)) {
25282
+ if (!devFilesHidden(c)) return true;
25283
+ }
25284
+ } catch {
25285
+ }
25286
+ return false;
25287
+ }
25288
+ function buildFileEntry(name, full, entryRel, cwdInGit, gitStatus) {
25289
+ let isDir = false;
25290
+ let size = null;
25291
+ try {
25292
+ const st = statSync15(full);
25293
+ isDir = st.isDirectory();
25294
+ if (!isDir) size = st.size;
25295
+ } catch {
25296
+ }
25297
+ return {
25298
+ name,
25299
+ path: entryRel,
25300
+ is_dir: isDir,
25301
+ has_children: isDir ? devDirHasVisibleChild(full) : null,
25302
+ git_status: devGitLabel(cwdInGit + entryRel, isDir, gitStatus),
25303
+ size
25304
+ };
25305
+ }
24882
25306
  function devAdminLanguage(rel) {
24883
25307
  const base = (rel.split(/[\\/]/).pop() ?? "").toLowerCase();
24884
25308
  if (base === "dockerfile" || base === "dockerfile.dev" || base === "dockerfile.prod") {
@@ -24893,14 +25317,14 @@ function devAdminLanguage(rel) {
24893
25317
  function readDevFile(target) {
24894
25318
  let fd;
24895
25319
  try {
24896
- fd = openSync4(target, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
24897
- if (!fstatSync2(fd).isFile()) return null;
25320
+ fd = openSync5(target, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
25321
+ if (!fstatSync3(fd).isFile()) return null;
24898
25322
  return readFileSync18(fd);
24899
25323
  } catch (error2) {
24900
25324
  if (["ENOENT", "ENOTDIR", "ELOOP"].includes(error2.code ?? "")) return null;
24901
25325
  throw error2;
24902
25326
  } finally {
24903
- if (fd !== void 0) closeSync4(fd);
25327
+ if (fd !== void 0) closeSync5(fd);
24904
25328
  }
24905
25329
  }
24906
25330
  function mcpSecureEqual(expected, provided) {
@@ -25127,10 +25551,11 @@ function toolbarJs() {
25127
25551
  connect();
25128
25552
  })();`;
25129
25553
  }
25130
- var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
25554
+ var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleDevAdminSplit, handleToolbarCss, handleToolbarJs;
25131
25555
  var init_devAdmin = __esm({
25132
25556
  "../core/src/devAdmin.ts"() {
25133
25557
  "use strict";
25558
+ init_slashTrim();
25134
25559
  init_devMailbox();
25135
25560
  init_upstreamResponse();
25136
25561
  init_dotenv();
@@ -25597,6 +26022,7 @@ var init_devAdmin = __esm({
25597
26022
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
25598
26023
  // JS asset
25599
26024
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
26025
+ { method: "GET", pattern: "/__dev/split", handler: handleDevAdminSplit },
25600
26026
  // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
25601
26027
  // CSP-clean (no inline style=, onclick=, or <script>) under a strict
25602
26028
  // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
@@ -25963,13 +26389,7 @@ var init_devAdmin = __esm({
25963
26389
  };
25964
26390
  handleSeed = async (req2, res) => {
25965
26391
  const url = new URL(req2.url ?? "/", "http://localhost");
25966
- const body = req2.body ?? {};
25967
- const table2 = url.searchParams.get("table") ?? body?.table ?? "";
25968
- const count = parseInt(String(url.searchParams.get("count") ?? body?.count ?? "10"), 10) || 10;
25969
- const seedRaw = url.searchParams.get("seed") ?? body?.seed;
25970
- const seed = seedRaw !== void 0 && seedRaw !== null && String(seedRaw) !== "" ? Number.isNaN(parseInt(String(seedRaw), 10)) ? void 0 : parseInt(String(seedRaw), 10) : void 0;
25971
- const clear = String(url.searchParams.get("clear") ?? body?.clear ?? "") === "true" || body?.clear === true;
25972
- const strict = String(url.searchParams.get("strict") ?? body?.strict ?? "") === "true" || body?.strict === true;
26392
+ const { table: table2, count, seed, clear, strict } = parseSeedRequest(url, req2.body);
25973
26393
  if (!table2) {
25974
26394
  res.json({ error: "Missing table parameter" });
25975
26395
  return;
@@ -25984,33 +26404,7 @@ var init_devAdmin = __esm({
25984
26404
  res.json({ error: `Table '${table2}' not found or has no columns` });
25985
26405
  return;
25986
26406
  }
25987
- const fieldMap = {};
25988
- for (const col of columns) {
25989
- const name = col.name.toLowerCase();
25990
- const type2 = col.type.toLowerCase();
25991
- if (name === "id" || col.primaryKey === true) continue;
25992
- if (name.includes("email")) {
25993
- fieldMap[col.name] = () => fake.email();
25994
- } else if (name.includes("name")) {
25995
- fieldMap[col.name] = () => fake.name();
25996
- } else if (name.includes("phone")) {
25997
- fieldMap[col.name] = () => fake.phone();
25998
- } else if (name.includes("address") || name.includes("city") || name.includes("country")) {
25999
- fieldMap[col.name] = () => fake.address();
26000
- } else if (name.includes("url") || name.includes("website")) {
26001
- fieldMap[col.name] = () => fake.url();
26002
- } else if (type2.includes("int")) {
26003
- fieldMap[col.name] = () => fake.integer(1, 1e3);
26004
- } else if (type2.includes("real") || type2.includes("float") || type2.includes("double") || type2.includes("numeric") || type2.includes("decimal")) {
26005
- fieldMap[col.name] = () => fake.numeric(0, 1e3, 2);
26006
- } else if (type2.includes("bool")) {
26007
- fieldMap[col.name] = () => fake.boolean();
26008
- } else if (type2.includes("date") || type2.includes("time")) {
26009
- fieldMap[col.name] = () => fake.date();
26010
- } else {
26011
- fieldMap[col.name] = () => fake.sentence(3);
26012
- }
26013
- }
26407
+ const fieldMap = buildSeedFieldMap(columns, fake);
26014
26408
  const summary = await seedTable2(db, table2, count, fieldMap, void 0, { clear, strict });
26015
26409
  res.json({ seeded: summary.seeded, failed: summary.failed, errors: summary.errors, table: table2 });
26016
26410
  } catch (e) {
@@ -26183,9 +26577,9 @@ var init_devAdmin = __esm({
26183
26577
  };
26184
26578
  handleTest = async (_req, res) => {
26185
26579
  try {
26186
- const { execFile } = await import("node:child_process");
26580
+ const { execFile: execFile2 } = await import("node:child_process");
26187
26581
  const { promisify } = await import("node:util");
26188
- const run = promisify(execFile);
26582
+ const run = promisify(execFile2);
26189
26583
  try {
26190
26584
  const { stdout, stderr } = await run("npm", ["test"], {
26191
26585
  cwd: resolve12(process.cwd()),
@@ -26303,9 +26697,9 @@ var init_devAdmin = __esm({
26303
26697
  }
26304
26698
  try {
26305
26699
  const envPath = join22(process.cwd(), ".env");
26306
- const fd = openSync4(envPath, constants3.O_RDWR | constants3.O_APPEND | constants3.O_CREAT | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0), 384);
26700
+ const fd = openSync5(envPath, constants4.O_RDWR | constants4.O_APPEND | constants4.O_CREAT | (constants4.O_NOFOLLOW ?? 0) | (constants4.O_NONBLOCK ?? 0), 384);
26307
26701
  try {
26308
- const info = fstatSync2(fd);
26702
+ const info = fstatSync3(fd);
26309
26703
  if (!info.isFile() || info.nlink !== 1) throw new Error("Refusing a non-regular or multiply-linked configuration file");
26310
26704
  fchmodSync2(fd, 384);
26311
26705
  const lines = readFileSync18(fd, "utf-8").split("\n");
@@ -26335,10 +26729,10 @@ var init_devAdmin = __esm({
26335
26729
  for (const [key, found] of Object.entries(keysFound)) {
26336
26730
  if (!found) newLines.push(`${key}=${values[key]}`);
26337
26731
  }
26338
- ftruncateSync(fd, 0);
26732
+ ftruncateSync2(fd, 0);
26339
26733
  writeFileSync13(fd, newLines.join("\n") + "\n");
26340
26734
  } finally {
26341
- closeSync4(fd);
26735
+ closeSync5(fd);
26342
26736
  }
26343
26737
  res.json({ success: true });
26344
26738
  } catch (e) {
@@ -26461,48 +26855,7 @@ var init_devAdmin = __esm({
26461
26855
  const full = join22(target, name);
26462
26856
  const entryRel = relative9(root, full).replace(/\\/g, "/");
26463
26857
  if (isSecretPath(entryRel)) continue;
26464
- let isDir = false;
26465
- let size = null;
26466
- try {
26467
- const st = statSync15(full);
26468
- isDir = st.isDirectory();
26469
- if (!isDir) size = st.size;
26470
- } catch {
26471
- }
26472
- const gitPath = cwdInGit + entryRel;
26473
- let gitLabel = "clean";
26474
- const code = gitStatus.get(gitPath);
26475
- if (code !== void 0) {
26476
- gitLabel = devGitStatusLabel(code);
26477
- } else if (isDir) {
26478
- const prefix = gitPath + "/";
26479
- for (const [gf, gc] of gitStatus) {
26480
- if (gf.startsWith(prefix)) {
26481
- gitLabel = gc === "??" ? "untracked" : "modified";
26482
- break;
26483
- }
26484
- }
26485
- }
26486
- let hasChildren = null;
26487
- if (isDir) {
26488
- hasChildren = false;
26489
- try {
26490
- for (const c of readdirSync13(full)) {
26491
- if (devFilesHidden(c)) continue;
26492
- hasChildren = true;
26493
- break;
26494
- }
26495
- } catch {
26496
- }
26497
- }
26498
- entries.push({
26499
- name,
26500
- path: entryRel,
26501
- is_dir: isDir,
26502
- has_children: hasChildren,
26503
- git_status: gitLabel,
26504
- size
26505
- });
26858
+ entries.push(buildFileEntry(name, full, entryRel, cwdInGit, gitStatus));
26506
26859
  }
26507
26860
  res.json({ path: relative9(root, target).replace(/\\/g, "/") || ".", branch, entries });
26508
26861
  };
@@ -27035,6 +27388,31 @@ var init_devAdmin = __esm({
27035
27388
  res.raw.writeHead(404, { "Content-Type": "text/plain" });
27036
27389
  res.raw.end("tina4-dev-admin.min.js not found");
27037
27390
  };
27391
+ handleDevAdminSplit = async (_req, res) => {
27392
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
27393
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
27394
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
27395
+ const dir = dirname17(fileURLToPath10(import.meta.url));
27396
+ const candidates = [
27397
+ join33(dir, "..", "public", "js", "tina4-dev-admin-split.html"),
27398
+ join33(dir, "..", "..", "public", "js", "tina4-dev-admin-split.html"),
27399
+ resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin-split.html"),
27400
+ resolve21(process.cwd(), "public", "js", "tina4-dev-admin-split.html")
27401
+ ];
27402
+ for (const path10 of candidates) {
27403
+ if (existsSync29(path10)) {
27404
+ try {
27405
+ const content = readFileSync27(path10, "utf-8");
27406
+ res.raw.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
27407
+ res.raw.end(content);
27408
+ return;
27409
+ } catch {
27410
+ }
27411
+ }
27412
+ }
27413
+ res.raw.writeHead(404, { "Content-Type": "text/plain" });
27414
+ res.raw.end("tina4-dev-admin-split.html not found");
27415
+ };
27038
27416
  handleToolbarCss = (_req, res) => {
27039
27417
  res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
27040
27418
  res.raw.end(toolbarCss());
@@ -27506,7 +27884,10 @@ function generate2(routes, models = []) {
27506
27884
  openapi: resolveOpenApiVersion(),
27507
27885
  info: buildInfo(),
27508
27886
  servers: resolveServers(),
27509
- paths: {},
27887
+ // Prototype-less: route paths are developer-defined, but a null prototype
27888
+ // makes `spec.paths[path][method] = op` structurally pollution-proof
27889
+ // regardless (js/prototype-polluting-assignment). Serialises to JSON the same.
27890
+ paths: /* @__PURE__ */ Object.create(null),
27510
27891
  components: {
27511
27892
  schemas: {},
27512
27893
  // Configurable security schemes (v3.13.42): bearerFormat via env, optional
@@ -27571,7 +27952,7 @@ function buildOperation(route, spec, ctx) {
27571
27952
  const openApiPath = patternToOpenAPI(route.pattern);
27572
27953
  const method = route.method.toLowerCase();
27573
27954
  if (!spec.paths[openApiPath]) {
27574
- spec.paths[openApiPath] = {};
27955
+ spec.paths[openApiPath] = /* @__PURE__ */ Object.create(null);
27575
27956
  }
27576
27957
  const tags = route.meta?.tags ?? inferTags(route.pattern);
27577
27958
  for (const t of tags) {
@@ -27877,7 +28258,7 @@ function inferModelFromPath(pattern) {
27877
28258
  return null;
27878
28259
  }
27879
28260
  function operationIdBase(method, openApiPath) {
27880
- const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
28261
+ const clean = trimSlashes(openApiPath).replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
27881
28262
  return clean ? `${method}_${clean}` : method;
27882
28263
  }
27883
28264
  function uniqueOperationId(method, openApiPath, seen) {
@@ -27895,6 +28276,7 @@ var WRITE_METHODS, INTERNAL_PREFIXES, registeredSchemes, registeredSchemas, PARA
27895
28276
  var init_generator = __esm({
27896
28277
  "../swagger/src/generator.ts"() {
27897
28278
  "use strict";
28279
+ init_slashTrim();
27898
28280
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
27899
28281
  INTERNAL_PREFIXES = ["/swagger", "/__dev", "/__feedback", "/ai", "/rag", "/vision", "/embed", "/image"];
27900
28282
  registeredSchemes = {};
@@ -27916,7 +28298,7 @@ var init_generator = __esm({
27916
28298
 
27917
28299
  // ../swagger/src/ui.ts
27918
28300
  function swaggerUiCdn() {
27919
- return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
28301
+ return rtrimSlashes(process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5");
27920
28302
  }
27921
28303
  function swaggerEnabled() {
27922
28304
  const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
@@ -27972,6 +28354,7 @@ var SWAGGER_UI_HTML;
27972
28354
  var init_ui = __esm({
27973
28355
  "../swagger/src/ui.ts"() {
27974
28356
  "use strict";
28357
+ init_slashTrim();
27975
28358
  SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
27976
28359
  <html lang="en">
27977
28360
  <head>
@@ -28389,7 +28772,7 @@ import { resolve as resolve15, dirname as dirname12, join as join25, relative as
28389
28772
  import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28390
28773
  import { isatty } from "node:tty";
28391
28774
  import { fileURLToPath as fileURLToPath7 } from "node:url";
28392
- import { execFileSync as execFileSync3, exec } from "node:child_process";
28775
+ import { execFileSync as execFileSync3, execFile } from "node:child_process";
28393
28776
  import cluster from "node:cluster";
28394
28777
  import os2 from "node:os";
28395
28778
  function isSwaggerAssetPath(pathname) {
@@ -28525,10 +28908,11 @@ function shouldOpenBrowser(isDevelopment, noBrowser = false) {
28525
28908
  });
28526
28909
  }
28527
28910
  function openBrowser(url) {
28911
+ if (!/^https?:\/\/([A-Za-z0-9.-]+|\[[0-9A-Fa-f:]+\])(:\d{1,5})?\/?$/.test(url)) return;
28528
28912
  setTimeout(() => {
28529
- if (process.platform === "darwin") exec(`open ${url}`);
28530
- else if (process.platform === "win32") exec(`start "" "${url}"`);
28531
- else exec(`xdg-open ${url}`);
28913
+ if (process.platform === "darwin") execFile("open", [url]);
28914
+ else if (process.platform === "win32") execFile("rundll32", ["url.dll,FileProtocolHandler", url]);
28915
+ else execFile("xdg-open", [url]);
28532
28916
  }, 2e3);
28533
28917
  }
28534
28918
  function resolvePortAndHost(config) {
@@ -28643,7 +29027,7 @@ function resetTemplateCache() {
28643
29027
  }
28644
29028
  function resolveTemplate(pathname, templatesDir) {
28645
29029
  if (!templateAutoRoutingEnabled()) return null;
28646
- const cleanPath = pathname.replace(/^\/+/, "").replace(/\/+$/, "") || "index";
29030
+ const cleanPath = trimSlashes(pathname) || "index";
28647
29031
  const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
28648
29032
  if (isDev2) {
28649
29033
  if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
@@ -28911,7 +29295,7 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
28911
29295
  }
28912
29296
  function resolveHandlerArgs(handler, req2, res, routeParams) {
28913
29297
  const fnStr = handler.toString();
28914
- const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
29298
+ const argMatch = fnStr.match(/^(?:async\s*)?(?:function(?:\s+\w+)?\s*)?\(([^)]*)\)/);
28915
29299
  const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
28916
29300
  const filteredArgs = argNames.filter((n) => n.length > 0);
28917
29301
  if (filteredArgs.length === 0) return [];
@@ -29147,8 +29531,9 @@ async function buildDispatchContext(router, base) {
29147
29531
  const templatesDir = resolve15(root, "src/templates");
29148
29532
  let frondEngine = null;
29149
29533
  try {
29150
- const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
29534
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2, setFormTokenSessionProvider: setFormTokenSessionProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
29151
29535
  setCspNonceProvider2(currentCspNonce);
29536
+ setFormTokenSessionProvider2(currentFormTokenSessionId);
29152
29537
  frondEngine = new Frond2(templatesDir);
29153
29538
  } catch {
29154
29539
  }
@@ -29263,7 +29648,10 @@ async function runDispatch(ctx, rawReq, rawRes) {
29263
29648
  const requestNonce = generateNonce();
29264
29649
  return Log.runWithRequestId(
29265
29650
  requestId,
29266
- () => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
29651
+ () => runWithCspNonce(
29652
+ requestNonce,
29653
+ () => runWithFormTokenSession(() => dispatchInner(ctx, rawReq, rawRes, requestId))
29654
+ )
29267
29655
  );
29268
29656
  }
29269
29657
  function loopbackBindHosts(host) {
@@ -29391,8 +29779,9 @@ async function startServer(config) {
29391
29779
  let frondEngine = null;
29392
29780
  setDefaultTemplatesDir(templatesDir);
29393
29781
  try {
29394
- const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
29782
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2, setFormTokenSessionProvider: setFormTokenSessionProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
29395
29783
  setCspNonceProvider2(currentCspNonce);
29784
+ setFormTokenSessionProvider2(currentFormTokenSessionId);
29396
29785
  frondEngine = new Frond2(templatesDir);
29397
29786
  router.addRoute({
29398
29787
  method: "GET",
@@ -29741,6 +30130,7 @@ var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swag
29741
30130
  var init_server = __esm({
29742
30131
  "../core/src/server.ts"() {
29743
30132
  "use strict";
30133
+ init_slashTrim();
29744
30134
  init_transport();
29745
30135
  init_router();
29746
30136
  init_authGate();
@@ -29748,6 +30138,7 @@ var init_server = __esm({
29748
30138
  init_portTakeover();
29749
30139
  init_request();
29750
30140
  init_dispatchPipeline();
30141
+ init_session();
29751
30142
  init_response();
29752
30143
  init_middleware();
29753
30144
  init_static();
@@ -31894,6 +32285,7 @@ var ApiStreamError, RETRY_STATUSES, DOWNLOAD_CHUNK_SIZE, MAX_REDIRECTS, STRIP_ON
31894
32285
  var init_api = __esm({
31895
32286
  "../core/src/api.ts"() {
31896
32287
  "use strict";
32288
+ init_slashTrim();
31897
32289
  init_version();
31898
32290
  init_ssrf();
31899
32291
  ApiStreamError = class extends Error {
@@ -31991,7 +32383,7 @@ var init_api = __esm({
31991
32383
  * turns on a per-client, in-memory cookie jar.
31992
32384
  */
31993
32385
  constructor(baseUrl = "", authHeaderOrOptions = "", timeout = 30) {
31994
- this.baseUrl = baseUrl.replace(/\/+$/, "");
32386
+ this.baseUrl = rtrimSlashes(baseUrl);
31995
32387
  this.headers = {};
31996
32388
  this.maxRetries = 0;
31997
32389
  this.retryBackoff = 0.5;
@@ -32757,6 +33149,23 @@ function sendCommand(socket, command) {
32757
33149
  });
32758
33150
  });
32759
33151
  }
33152
+ function pushAlternativeBody(lines, altBoundary, text, body) {
33153
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
33154
+ lines.push("");
33155
+ lines.push(`--${altBoundary}`);
33156
+ lines.push("Content-Type: text/plain; charset=UTF-8");
33157
+ lines.push("Content-Transfer-Encoding: 7bit");
33158
+ lines.push("");
33159
+ lines.push(text);
33160
+ lines.push("");
33161
+ lines.push(`--${altBoundary}`);
33162
+ lines.push("Content-Type: text/html; charset=UTF-8");
33163
+ lines.push("Content-Transfer-Encoding: 7bit");
33164
+ lines.push("");
33165
+ lines.push(body);
33166
+ lines.push("");
33167
+ lines.push(`--${altBoundary}--`);
33168
+ }
32760
33169
  function buildMimeMessage(options) {
32761
33170
  const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
32762
33171
  const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
@@ -32786,21 +33195,7 @@ function buildMimeMessage(options) {
32786
33195
  lines.push("");
32787
33196
  lines.push(`--${boundary}`);
32788
33197
  if (hasTextAlt) {
32789
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
32790
- lines.push("");
32791
- lines.push(`--${altBoundary}`);
32792
- lines.push("Content-Type: text/plain; charset=UTF-8");
32793
- lines.push("Content-Transfer-Encoding: 7bit");
32794
- lines.push("");
32795
- lines.push(options.text);
32796
- lines.push("");
32797
- lines.push(`--${altBoundary}`);
32798
- lines.push("Content-Type: text/html; charset=UTF-8");
32799
- lines.push("Content-Transfer-Encoding: 7bit");
32800
- lines.push("");
32801
- lines.push(options.body);
32802
- lines.push("");
32803
- lines.push(`--${altBoundary}--`);
33198
+ pushAlternativeBody(lines, altBoundary, options.text, options.body);
32804
33199
  } else {
32805
33200
  const contentType = options.html ? "text/html" : "text/plain";
32806
33201
  lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
@@ -32825,21 +33220,7 @@ function buildMimeMessage(options) {
32825
33220
  lines.push("");
32826
33221
  lines.push(`--${boundary}--`);
32827
33222
  } else if (hasTextAlt) {
32828
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
32829
- lines.push("");
32830
- lines.push(`--${altBoundary}`);
32831
- lines.push("Content-Type: text/plain; charset=UTF-8");
32832
- lines.push("Content-Transfer-Encoding: 7bit");
32833
- lines.push("");
32834
- lines.push(options.text);
32835
- lines.push("");
32836
- lines.push(`--${altBoundary}`);
32837
- lines.push("Content-Type: text/html; charset=UTF-8");
32838
- lines.push("Content-Transfer-Encoding: 7bit");
32839
- lines.push("");
32840
- lines.push(options.body);
32841
- lines.push("");
32842
- lines.push(`--${altBoundary}--`);
33223
+ pushAlternativeBody(lines, altBoundary, options.text, options.body);
32843
33224
  } else {
32844
33225
  const contentType = options.html ? "text/html" : "text/plain";
32845
33226
  lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
@@ -33604,61 +33985,72 @@ function decodeEntities(raw) {
33604
33985
  function parseXmlDocument(xml) {
33605
33986
  const stack = [];
33606
33987
  let root = null;
33607
- let position = 0;
33608
33988
  const addText = (text) => {
33609
33989
  const current = stack[stack.length - 1];
33610
33990
  if (current.children.length === 0) current.text += text;
33611
33991
  };
33612
- while (position < xml.length) {
33613
- const nextTag = xml.indexOf("<", position);
33992
+ const skipComment = (at) => {
33993
+ const close = xml.indexOf("-->", at + 4);
33994
+ if (close === -1) malformed();
33995
+ return close + 3;
33996
+ };
33997
+ const readCData = (at) => {
33998
+ const close = xml.indexOf("]]>", at + 9);
33999
+ if (close === -1 || stack.length === 0) malformed();
34000
+ addText(xml.slice(at + 9, close));
34001
+ return close + 3;
34002
+ };
34003
+ const skipProcessingInstruction = (at) => {
34004
+ const close = xml.indexOf("?>", at + 2);
34005
+ if (close === -1) malformed();
34006
+ const target = xml.slice(at + 2, close).split(/\s/, 1)[0];
34007
+ if (target.toLowerCase() === "xml" && at !== 0) malformed();
34008
+ return close + 2;
34009
+ };
34010
+ const closeElement = (at) => {
34011
+ END_TAG.lastIndex = at;
34012
+ const match = END_TAG.exec(xml);
34013
+ const open = stack.pop();
34014
+ if (!match || !open || open.qualifiedName !== match[1]) malformed();
34015
+ return END_TAG.lastIndex;
34016
+ };
34017
+ const openElement = (at) => {
34018
+ START_TAG.lastIndex = at;
34019
+ const match = START_TAG.exec(xml);
34020
+ if (!match || stack.length === 0 && root !== null) malformed();
34021
+ for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
34022
+ const qualifiedName = match[1];
34023
+ const element = {
34024
+ name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
34025
+ qualifiedName,
34026
+ children: [],
34027
+ text: ""
34028
+ };
34029
+ if (stack.length > 0) stack[stack.length - 1].children.push(element);
34030
+ else root = element;
34031
+ if (match[3] !== "/") stack.push(element);
34032
+ return START_TAG.lastIndex;
34033
+ };
34034
+ const readText = (at) => {
34035
+ const nextTag = xml.indexOf("<", at);
33614
34036
  const textEnd = nextTag === -1 ? xml.length : nextTag;
33615
- if (textEnd > position) {
33616
- const raw = xml.slice(position, textEnd);
34037
+ if (textEnd > at) {
34038
+ const raw = xml.slice(at, textEnd);
33617
34039
  if (stack.length > 0) addText(decodeEntities(raw));
33618
34040
  else if (raw.trim() !== "") malformed();
33619
- position = textEnd;
33620
- if (nextTag === -1) break;
33621
- }
33622
- if (xml.startsWith("<!--", position)) {
33623
- const close = xml.indexOf("-->", position + 4);
33624
- if (close === -1) malformed();
33625
- position = close + 3;
33626
- } else if (xml.startsWith("<![CDATA[", position)) {
33627
- const close = xml.indexOf("]]>", position + 9);
33628
- if (close === -1 || stack.length === 0) malformed();
33629
- addText(xml.slice(position + 9, close));
33630
- position = close + 3;
33631
- } else if (xml.startsWith("<?", position)) {
33632
- const close = xml.indexOf("?>", position + 2);
33633
- if (close === -1) malformed();
33634
- const target = xml.slice(position + 2, close).split(/\s/, 1)[0];
33635
- if (target.toLowerCase() === "xml" && position !== 0) malformed();
33636
- position = close + 2;
33637
- } else if (xml.startsWith("<!", position)) {
33638
- malformed();
33639
- } else if (xml.startsWith("</", position)) {
33640
- END_TAG.lastIndex = position;
33641
- const match = END_TAG.exec(xml);
33642
- const open = stack.pop();
33643
- if (!match || !open || open.qualifiedName !== match[1]) malformed();
33644
- position = END_TAG.lastIndex;
33645
- } else {
33646
- START_TAG.lastIndex = position;
33647
- const match = START_TAG.exec(xml);
33648
- if (!match || stack.length === 0 && root !== null) malformed();
33649
- for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
33650
- const qualifiedName = match[1];
33651
- const element = {
33652
- name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
33653
- qualifiedName,
33654
- children: [],
33655
- text: ""
33656
- };
33657
- if (stack.length > 0) stack[stack.length - 1].children.push(element);
33658
- else root = element;
33659
- if (match[3] !== "/") stack.push(element);
33660
- position = START_TAG.lastIndex;
33661
34041
  }
34042
+ return textEnd;
34043
+ };
34044
+ let position = 0;
34045
+ while (position < xml.length) {
34046
+ position = readText(position);
34047
+ if (position >= xml.length) break;
34048
+ if (xml.startsWith("<!--", position)) position = skipComment(position);
34049
+ else if (xml.startsWith("<![CDATA[", position)) position = readCData(position);
34050
+ else if (xml.startsWith("<?", position)) position = skipProcessingInstruction(position);
34051
+ else if (xml.startsWith("<!", position)) malformed();
34052
+ else if (xml.startsWith("</", position)) position = closeElement(position);
34053
+ else position = openElement(position);
33662
34054
  }
33663
34055
  if (root === null || stack.length > 0) malformed();
33664
34056
  return root;
@@ -35148,7 +35540,7 @@ import {
35148
35540
  randomBytes as randomBytes10
35149
35541
  } from "node:crypto";
35150
35542
  function encodeBase64Url(value) {
35151
- return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
35543
+ return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
35152
35544
  }
35153
35545
  function decodeBase64Url(value, name) {
35154
35546
  if (typeof value !== "string" || value.length === 0) {
@@ -35399,6 +35791,7 @@ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai, Aggre
35399
35791
  var init_aiClient = __esm({
35400
35792
  "../core/src/aiClient.ts"() {
35401
35793
  "use strict";
35794
+ init_slashTrim();
35402
35795
  init_api();
35403
35796
  AiError = class extends Error {
35404
35797
  };
@@ -35603,7 +35996,7 @@ var init_aiClient = __esm({
35603
35996
  throw new AiConfigError("AI URL must be an http or https URL");
35604
35997
  }
35605
35998
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
35606
- const path10 = url.pathname.replace(/\/+$/, "");
35999
+ const path10 = rtrimSlashes(url.pathname);
35607
36000
  if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
35608
36001
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
35609
36002
  url.pathname = (path10 || "/v1") + suffix;
@@ -37973,6 +38366,7 @@ __export(src_exports2, {
37973
38366
  backgroundTaskCount: () => backgroundTaskCount,
37974
38367
  buildEnvelope: () => buildEnvelope,
37975
38368
  buildFrame: () => buildFrame,
38369
+ buildSeedFieldMap: () => buildSeedFieldMap,
37976
38370
  buildSessionCookie: () => buildSessionCookie,
37977
38371
  cacheBackendStats: () => cacheBackendStats,
37978
38372
  cacheClear: () => cacheClear,
@@ -38078,6 +38472,7 @@ __export(src_exports2, {
38078
38472
  parseFrame: () => parseFrame,
38079
38473
  parseLineStream: () => parseLineStream,
38080
38474
  parseMultipart: () => parseMultipart,
38475
+ parseSeedRequest: () => parseSeedRequest,
38081
38476
  parseSseStream: () => parseSseStream,
38082
38477
  parseUpgradeHeaders: () => parseUpgradeHeaders,
38083
38478
  patch: () => patch,
@@ -42471,10 +42866,10 @@ __export(database_exports, {
42471
42866
  stripTrailingSemicolons: () => stripTrailingSemicolons,
42472
42867
  wrapWithCache: () => wrapWithCache
42473
42868
  });
42474
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
42869
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "node:async_hooks";
42475
42870
  function stripTrailingSemicolons(sql) {
42476
42871
  if (!sql) return sql;
42477
- let stripped = sql.replace(/\s+$/, "");
42872
+ let stripped = sql.trimEnd();
42478
42873
  while (stripped.endsWith(";")) {
42479
42874
  stripped = stripped.slice(0, -1).replace(/\s+$/, "");
42480
42875
  }
@@ -42641,6 +43036,13 @@ async function createAdapterFromUrl(url, username, password) {
42641
43036
  }
42642
43037
  async function buildAdapterFromUrl(url, username, password) {
42643
43038
  const parsed = parseDatabaseUrl(url, username, password);
43039
+ const serverConfig = {
43040
+ host: parsed.host ?? void 0,
43041
+ port: parsed.port ?? void 0,
43042
+ user: parsed.username ?? void 0,
43043
+ password: parsed.password ?? void 0,
43044
+ database: parsed.database || void 0
43045
+ };
42644
43046
  switch (parsed.engine) {
42645
43047
  case "sqlite": {
42646
43048
  const { SQLiteAdapter: SQLiteAdapter2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
@@ -42648,49 +43050,25 @@ async function buildAdapterFromUrl(url, username, password) {
42648
43050
  }
42649
43051
  case "postgres": {
42650
43052
  const { PostgresAdapter: PostgresAdapter2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
42651
- const adapter = new PostgresAdapter2({
42652
- host: parsed.host ?? void 0,
42653
- port: parsed.port ?? void 0,
42654
- user: parsed.username ?? void 0,
42655
- password: parsed.password ?? void 0,
42656
- database: parsed.database || void 0
42657
- });
43053
+ const adapter = new PostgresAdapter2(serverConfig);
42658
43054
  await adapter.connect();
42659
43055
  return adapter;
42660
43056
  }
42661
43057
  case "mysql": {
42662
43058
  const { MysqlAdapter: MysqlAdapter2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
42663
- const adapter = new MysqlAdapter2({
42664
- host: parsed.host ?? void 0,
42665
- port: parsed.port ?? void 0,
42666
- user: parsed.username ?? void 0,
42667
- password: parsed.password ?? void 0,
42668
- database: parsed.database || void 0
42669
- });
43059
+ const adapter = new MysqlAdapter2(serverConfig);
42670
43060
  await adapter.connect();
42671
43061
  return adapter;
42672
43062
  }
42673
43063
  case "mssql": {
42674
43064
  const { MssqlAdapter: MssqlAdapter2 } = await Promise.resolve().then(() => (init_mssql(), mssql_exports));
42675
- const adapter = new MssqlAdapter2({
42676
- host: parsed.host ?? void 0,
42677
- port: parsed.port ?? void 0,
42678
- user: parsed.username ?? void 0,
42679
- password: parsed.password ?? void 0,
42680
- database: parsed.database || void 0
42681
- });
43065
+ const adapter = new MssqlAdapter2(serverConfig);
42682
43066
  await adapter.connect();
42683
43067
  return adapter;
42684
43068
  }
42685
43069
  case "firebird": {
42686
43070
  const { FirebirdAdapter: FirebirdAdapter2 } = await Promise.resolve().then(() => (init_firebird(), firebird_exports));
42687
- const adapter = new FirebirdAdapter2({
42688
- host: parsed.host ?? void 0,
42689
- port: parsed.port ?? void 0,
42690
- user: parsed.username ?? void 0,
42691
- password: parsed.password ?? void 0,
42692
- database: parsed.database || void 0
42693
- });
43071
+ const adapter = new FirebirdAdapter2(serverConfig);
42694
43072
  await adapter.connect();
42695
43073
  return adapter;
42696
43074
  }
@@ -42885,9 +43263,9 @@ var init_database = __esm({
42885
43263
  leases = /* @__PURE__ */ new Map();
42886
43264
  discardedSlots = /* @__PURE__ */ new Set();
42887
43265
  repairs = /* @__PURE__ */ new Map();
42888
- leaseOwner = new AsyncLocalStorage3();
42889
- operationStore = new AsyncLocalStorage3();
42890
- txStore = new AsyncLocalStorage3();
43266
+ leaseOwner = new AsyncLocalStorage4();
43267
+ operationStore = new AsyncLocalStorage4();
43268
+ txStore = new AsyncLocalStorage4();
42891
43269
  /**
42892
43270
  * Create a Database wrapping an existing adapter.
42893
43271
  * For creating a Database from a URL, use the async static factories:
@@ -45627,7 +46005,7 @@ function tableData(opts) {
45627
46005
  readonly: !opts.editable,
45628
46006
  inline_script: opts.inlineScript,
45629
46007
  table_name: h(opts.tableName),
45630
- table_id_attr: opts.tableId ? ` id="${h(opts.tableId)}"` : ""
46008
+ table_id: opts.tableId ? String(opts.tableId) : ""
45631
46009
  };
45632
46010
  }
45633
46011
  var init_crudTable = __esm({
@@ -45668,13 +46046,13 @@ function renderModalForm(mode, columns, pk, edit) {
45668
46046
  value: "",
45669
46047
  placeholder: h(`Enter ${label.toLowerCase()}`),
45670
46048
  type: "text",
45671
- required_attr: "",
46049
+ required: false,
45672
46050
  input: true
45673
46051
  };
45674
46052
  });
45675
46053
  return renderCrudTemplate("form", {
45676
46054
  wrap: true,
45677
- form_id_attr: ` id="form-${mode}"`,
46055
+ form_id: `form-${mode}`,
45678
46056
  action: "",
45679
46057
  form_method: "POST",
45680
46058
  method_override: null,
@@ -45731,7 +46109,7 @@ function fieldInputShape(field, base) {
45731
46109
  case "text":
45732
46110
  return { ...base, textarea: true };
45733
46111
  case "boolean":
45734
- return { ...base, checkbox: true, checked_attr: value ? " checked" : "" };
46112
+ return { ...base, checkbox: true, checked: !!value };
45735
46113
  case "select":
45736
46114
  return { ...base, select: true, options_html: buildOptions(field.options, value) };
45737
46115
  case "date":
@@ -45754,7 +46132,7 @@ function buildCustomField(field) {
45754
46132
  label: h(String(label)),
45755
46133
  value: h(field.value ?? ""),
45756
46134
  placeholder: "",
45757
- required_attr: field.required ? " required" : ""
46135
+ required: !!field.required
45758
46136
  };
45759
46137
  return fieldInputShape(field, base);
45760
46138
  }
@@ -45762,7 +46140,7 @@ function generateForm2(fields, options = {}) {
45762
46140
  const verb = String(options.method ?? "POST").toUpperCase();
45763
46141
  return renderCrudTemplate("form", {
45764
46142
  wrap: true,
45765
- form_id_attr: "",
46143
+ form_id: "",
45766
46144
  action: h(options.action ?? "/"),
45767
46145
  form_method: h(verb),
45768
46146
  method_override: ["PUT", "PATCH", "DELETE"].includes(verb) ? verb : null,
@@ -49882,20 +50260,210 @@ var init_attachment = __esm({
49882
50260
  }
49883
50261
  });
49884
50262
 
50263
+ // src/realtime/s3Client.ts
50264
+ import { createHash as createHash11, createHmac as createHmac4 } from "node:crypto";
50265
+ function retryAfterSeconds(raw) {
50266
+ if (raw == null) return null;
50267
+ const n = Number(raw);
50268
+ return Number.isFinite(n) ? Math.max(0, n) : null;
50269
+ }
50270
+ function uriEncode(value, isPath) {
50271
+ let out = "";
50272
+ for (const byte of Buffer.from(value, "utf8")) {
50273
+ const ch = String.fromCharCode(byte);
50274
+ if (UNRESERVED.test(ch) || isPath && ch === "/") out += ch;
50275
+ else out += "%" + byte.toString(16).toUpperCase().padStart(2, "0");
50276
+ }
50277
+ return out;
50278
+ }
50279
+ function sha256Hex2(data) {
50280
+ return createHash11("sha256").update(data).digest("hex");
50281
+ }
50282
+ function hmac2(key, msg) {
50283
+ return createHmac4("sha256", key).update(msg, "utf8").digest();
50284
+ }
50285
+ var ALGORITHM, SERVICE, UNRESERVED, MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, S3Error, S3ConfigError, sleep, S3Client;
50286
+ var init_s3Client = __esm({
50287
+ "src/realtime/s3Client.ts"() {
50288
+ "use strict";
50289
+ ALGORITHM = "AWS4-HMAC-SHA256";
50290
+ SERVICE = "s3";
50291
+ UNRESERVED = /[A-Za-z0-9\-_.~]/;
50292
+ MAX_RETRIES = 2;
50293
+ BACKOFF_BASE = 0.5;
50294
+ BACKOFF_CAP = 20;
50295
+ S3Error = class extends Error {
50296
+ constructor(status2, message) {
50297
+ super(message);
50298
+ this.status = status2;
50299
+ this.name = "S3Error";
50300
+ }
50301
+ status;
50302
+ /** Seconds from a Retry-After header in its delta-seconds form, else null. */
50303
+ retryAfter = null;
50304
+ };
50305
+ S3ConfigError = class extends Error {
50306
+ constructor(message) {
50307
+ super(message);
50308
+ this.name = "S3ConfigError";
50309
+ }
50310
+ };
50311
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50312
+ S3Client = class {
50313
+ scheme;
50314
+ host;
50315
+ // authority (host[:port]) — matches the Host header
50316
+ prefix;
50317
+ // path an endpoint carries (https://host/s3 -> "/s3"), "" if none
50318
+ region;
50319
+ accessKey;
50320
+ secretKey;
50321
+ timeout;
50322
+ constructor(opts = {}) {
50323
+ this.region = opts.region || "us-east-1";
50324
+ this.accessKey = opts.accessKey || "";
50325
+ this.secretKey = opts.secretKey || "";
50326
+ this.timeout = opts.timeout ?? 3e4;
50327
+ let base = opts.endpoint || `https://s3.${this.region}.amazonaws.com`;
50328
+ while (base.endsWith("/")) base = base.slice(0, -1);
50329
+ const m = /^([a-z][a-z0-9+.-]*):\/\/([^/]+)(\/.*)?$/i.exec(base);
50330
+ this.scheme = m ? m[1] : "https";
50331
+ this.host = m ? m[2] : base;
50332
+ let p = m && m[3] ? m[3] : "";
50333
+ while (p.endsWith("/")) p = p.slice(0, -1);
50334
+ this.prefix = p;
50335
+ }
50336
+ // -- public API (the subset S3Storage uses) --------------------------------
50337
+ async putObject(bucket, key, data, contentType = "application/octet-stream") {
50338
+ const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
50339
+ await this.request("PUT", this.canonicalUri(bucket, key), body, { "content-type": contentType });
50340
+ }
50341
+ async getObject(bucket, key) {
50342
+ return (await this.request("GET", this.canonicalUri(bucket, key), Buffer.alloc(0))).body;
50343
+ }
50344
+ async deleteObject(bucket, key) {
50345
+ await this.request("DELETE", this.canonicalUri(bucket, key), Buffer.alloc(0));
50346
+ }
50347
+ async headObject(bucket, key) {
50348
+ await this.request("HEAD", this.canonicalUri(bucket, key), Buffer.alloc(0));
50349
+ }
50350
+ /** Create the bucket (path-style `PUT /<bucket>`); an already-owned bucket (409) is success. */
50351
+ async createBucket(bucket) {
50352
+ try {
50353
+ await this.request("PUT", `${this.prefix}/${uriEncode(bucket, true)}`, Buffer.alloc(0));
50354
+ } catch (e) {
50355
+ if (!(e instanceof S3Error) || e.status !== 409) throw e;
50356
+ }
50357
+ }
50358
+ /** A URL that fetches the object directly, signed via SigV4 query params. */
50359
+ presignedGetUrl(bucket, key, expires = 3600) {
50360
+ this.requireCredentials();
50361
+ const { amzDate, dateStamp } = this.now();
50362
+ const uri = this.canonicalUri(bucket, key);
50363
+ const scope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
50364
+ const query = {
50365
+ "X-Amz-Algorithm": ALGORITHM,
50366
+ "X-Amz-Credential": `${this.accessKey}/${scope}`,
50367
+ "X-Amz-Date": amzDate,
50368
+ "X-Amz-Expires": String(expires),
50369
+ "X-Amz-SignedHeaders": "host"
50370
+ };
50371
+ const canonicalQs = this.canonicalQuery(query);
50372
+ const canonicalRequest = ["GET", uri, canonicalQs, `host:${this.host}
50373
+ `, "host", "UNSIGNED-PAYLOAD"].join("\n");
50374
+ const signature = this.sign(canonicalRequest, amzDate, dateStamp, scope);
50375
+ return `${this.scheme}://${this.host}${uri}?${canonicalQs}&X-Amz-Signature=${signature}`;
50376
+ }
50377
+ // -- signing + transport ----------------------------------------------------
50378
+ requireCredentials() {
50379
+ if (!this.accessKey || !this.secretKey) {
50380
+ throw new S3ConfigError(
50381
+ "S3 credentials are not set: set TINA4_STORAGE_KEY and TINA4_STORAGE_SECRET"
50382
+ );
50383
+ }
50384
+ }
50385
+ now() {
50386
+ const iso2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
50387
+ return { amzDate: iso2, dateStamp: iso2.slice(0, 8) };
50388
+ }
50389
+ canonicalUri(bucket, key) {
50390
+ return `${this.prefix}/${uriEncode(bucket, true)}/${uriEncode(key, true)}`;
50391
+ }
50392
+ canonicalQuery(query) {
50393
+ return Object.keys(query).sort().map((k) => `${uriEncode(k, false)}=${uriEncode(query[k], false)}`).join("&");
50394
+ }
50395
+ signingKey(dateStamp) {
50396
+ const kDate = hmac2(`AWS4${this.secretKey}`, dateStamp);
50397
+ const kRegion = hmac2(kDate, this.region);
50398
+ const kService = hmac2(kRegion, SERVICE);
50399
+ return hmac2(kService, "aws4_request");
50400
+ }
50401
+ sign(canonicalRequest, amzDate, dateStamp, scope) {
50402
+ const stringToSign = [ALGORITHM, amzDate, scope, sha256Hex2(canonicalRequest)].join("\n");
50403
+ return createHmac4("sha256", this.signingKey(dateStamp)).update(stringToSign, "utf8").digest("hex");
50404
+ }
50405
+ async request(method, uri, body, extraHeaders = {}) {
50406
+ this.requireCredentials();
50407
+ for (let attempt = 0; ; attempt++) {
50408
+ const last = attempt === MAX_RETRIES;
50409
+ let delay2 = null;
50410
+ try {
50411
+ return await this.send(method, uri, body, extraHeaders);
50412
+ } catch (e) {
50413
+ if (e instanceof S3Error) {
50414
+ if (last || !(e.status === 429 || e.status >= 500)) throw e;
50415
+ delay2 = e.retryAfter;
50416
+ } else {
50417
+ if (last) throw e;
50418
+ delay2 = null;
50419
+ }
50420
+ }
50421
+ if (delay2 == null) delay2 = Math.random() * (BACKOFF_BASE * 2 ** attempt);
50422
+ await sleep(Math.min(delay2, BACKOFF_CAP) * 1e3);
50423
+ }
50424
+ }
50425
+ async send(method, uri, body, extraHeaders = {}) {
50426
+ const { amzDate, dateStamp } = this.now();
50427
+ const payloadHash = sha256Hex2(body);
50428
+ const signed = {
50429
+ host: this.host,
50430
+ "x-amz-content-sha256": payloadHash,
50431
+ "x-amz-date": amzDate
50432
+ };
50433
+ const signedKeys = Object.keys(signed).sort();
50434
+ const canonicalHeaders = signedKeys.map((k) => `${k}:${signed[k]}
50435
+ `).join("");
50436
+ const signedHeaders = signedKeys.join(";");
50437
+ const canonicalRequest = [method, uri, "", canonicalHeaders, signedHeaders, payloadHash].join("\n");
50438
+ const scope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
50439
+ const signature = this.sign(canonicalRequest, amzDate, dateStamp, scope);
50440
+ const authorization = `${ALGORITHM} Credential=${this.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
50441
+ const headers = {
50442
+ Authorization: authorization,
50443
+ "x-amz-content-sha256": payloadHash,
50444
+ "x-amz-date": amzDate,
50445
+ ...extraHeaders
50446
+ };
50447
+ const url = `${this.scheme}://${this.host}${uri}`;
50448
+ const init = { method, headers, redirect: "manual" };
50449
+ if (method === "PUT" || method === "POST") init.body = body;
50450
+ if (AbortSignal.timeout) init.signal = AbortSignal.timeout(this.timeout);
50451
+ const response = await fetch(url, init);
50452
+ if (response.status < 200 || response.status >= 300) {
50453
+ const err = new S3Error(response.status, `${method} ${uri}: HTTP ${response.status}`);
50454
+ err.retryAfter = retryAfterSeconds(response.headers.get("retry-after"));
50455
+ throw err;
50456
+ }
50457
+ return { status: response.status, body: Buffer.from(await response.arrayBuffer()) };
50458
+ }
50459
+ };
50460
+ }
50461
+ });
50462
+
49885
50463
  // src/realtime/storage.ts
49886
50464
  import { randomBytes as randomBytes12 } from "node:crypto";
49887
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync19 } from "node:fs";
50465
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync9, statSync as statSync19 } from "node:fs";
49888
50466
  import { resolve as resolve20, sep as sep8 } from "node:path";
49889
- import { createRequire as createRequire8 } from "node:module";
49890
- function loadAwsSdk(packageName) {
49891
- try {
49892
- return require2(packageName);
49893
- } catch (error2) {
49894
- const missing = error2?.code === "MODULE_NOT_FOUND" && String(error2?.message ?? "").includes(`'${packageName}'`);
49895
- if (!missing) throw error2;
49896
- throw optionalPackageMissing("S3Storage", S3_PACKAGES);
49897
- }
49898
- }
49899
50467
  function storageKey(filename = "") {
49900
50468
  let ext = "";
49901
50469
  if (filename && filename.includes(".")) {
@@ -49920,14 +50488,12 @@ function selectStorage(storage) {
49920
50488
  }
49921
50489
  return new LocalStorage();
49922
50490
  }
49923
- var require2, S3_PACKAGES, UNSAFE, LocalStorage, S3Storage;
50491
+ var UNSAFE, LocalStorage, S3Storage;
49924
50492
  var init_storage = __esm({
49925
50493
  "src/realtime/storage.ts"() {
49926
50494
  "use strict";
49927
50495
  init_src2();
49928
- init_optionalPackage();
49929
- require2 = createRequire8(import.meta.url);
49930
- S3_PACKAGES = ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"];
50496
+ init_s3Client();
49931
50497
  UNSAFE = /[^A-Za-z0-9]/g;
49932
50498
  LocalStorage = class {
49933
50499
  directory;
@@ -49959,7 +50525,7 @@ var init_storage = __esm({
49959
50525
  }
49960
50526
  delete(key) {
49961
50527
  try {
49962
- unlinkSync8(this.pathFor(key));
50528
+ unlinkSync9(this.pathFor(key));
49963
50529
  } catch {
49964
50530
  }
49965
50531
  }
@@ -49977,51 +50543,36 @@ var init_storage = __esm({
49977
50543
  constructor(opts = {}) {
49978
50544
  this.bucket = opts.bucket || process.env.TINA4_STORAGE_BUCKET || "";
49979
50545
  if (!this.bucket) throw new Error("S3Storage requires TINA4_STORAGE_BUCKET");
49980
- const { S3Client } = loadAwsSdk("@aws-sdk/client-s3");
49981
- loadAwsSdk("@aws-sdk/s3-request-presigner");
49982
- const endpoint = opts.endpoint || process.env.TINA4_STORAGE_URL;
49983
50546
  this.client = new S3Client({
49984
- endpoint: endpoint || void 0,
49985
- forcePathStyle: true,
50547
+ endpoint: opts.endpoint || process.env.TINA4_STORAGE_URL || void 0,
49986
50548
  region: opts.region || process.env.TINA4_STORAGE_REGION || "us-east-1",
49987
- credentials: {
49988
- accessKeyId: opts.key || process.env.TINA4_STORAGE_KEY || "",
49989
- secretAccessKey: opts.secret || process.env.TINA4_STORAGE_SECRET || ""
49990
- }
50549
+ accessKey: opts.key || process.env.TINA4_STORAGE_KEY || "",
50550
+ secretKey: opts.secret || process.env.TINA4_STORAGE_SECRET || ""
49991
50551
  });
49992
50552
  }
49993
50553
  async put(key, data, mime = "application/octet-stream") {
49994
- const { PutObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
49995
- await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data, ContentType: mime }));
50554
+ await this.client.putObject(this.bucket, key, data, mime);
49996
50555
  }
49997
50556
  async get(key) {
49998
50557
  try {
49999
- const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
50000
- const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
50001
- const chunks = [];
50002
- for await (const chunk of res.Body) chunks.push(Buffer.from(chunk));
50003
- return Buffer.concat(chunks);
50558
+ return await this.client.getObject(this.bucket, key);
50004
50559
  } catch {
50005
50560
  return null;
50006
50561
  }
50007
50562
  }
50008
50563
  async url(key, ttl = 3600) {
50009
- const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
50010
- const { getSignedUrl } = loadAwsSdk("@aws-sdk/s3-request-presigner");
50011
- return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn: ttl });
50564
+ return this.client.presignedGetUrl(this.bucket, key, ttl);
50012
50565
  }
50013
50566
  async delete(key) {
50014
50567
  try {
50015
- const { DeleteObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
50016
- await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
50568
+ await this.client.deleteObject(this.bucket, key);
50017
50569
  } catch (e) {
50018
50570
  Log.error(`S3Storage delete failed for ${key}: ${e.message}`);
50019
50571
  }
50020
50572
  }
50021
50573
  async exists(key) {
50022
50574
  try {
50023
- const { HeadObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
50024
- await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
50575
+ await this.client.headObject(this.bucket, key);
50025
50576
  return true;
50026
50577
  } catch {
50027
50578
  return false;
@@ -50032,7 +50583,7 @@ var init_storage = __esm({
50032
50583
  });
50033
50584
 
50034
50585
  // src/realtime/realtime.ts
50035
- import { createHmac as createHmac4 } from "node:crypto";
50586
+ import { createHmac as createHmac5 } from "node:crypto";
50036
50587
  function iceServers() {
50037
50588
  const stun = process.env.TINA4_RTC_STUN_URLS || DEFAULT_STUN;
50038
50589
  const servers = [{ urls: splitUrls(stun) }];
@@ -50041,14 +50592,14 @@ function iceServers() {
50041
50592
  if (turnUrl && secret) {
50042
50593
  const ttl = parseInt(process.env.TINA4_RTC_TURN_TTL || "3600", 10);
50043
50594
  const username = String(Math.floor(Date.now() / 1e3) + ttl);
50044
- const credential = createHmac4("sha1", secret).update(username).digest("base64");
50595
+ const credential = createHmac5("sha1", secret).update(username).digest("base64");
50045
50596
  servers.push({ urls: splitUrls(turnUrl), username, credential });
50046
50597
  }
50047
50598
  return servers;
50048
50599
  }
50049
50600
  async function realtime(options = {}) {
50050
50601
  const features = options.features ?? ["calls"];
50051
- const raw = (options.prefix ?? "").replace(/^\/+|\/+$/g, "");
50602
+ const raw = trimSlashes(options.prefix ?? "");
50052
50603
  const p = raw ? `/${raw}` : "";
50053
50604
  _authorize = options.authorize;
50054
50605
  if (features.includes("chat") || features.includes("files")) {
@@ -50320,6 +50871,7 @@ var DEFAULT_STUN, _authorize;
50320
50871
  var init_realtime = __esm({
50321
50872
  "src/realtime/realtime.ts"() {
50322
50873
  "use strict";
50874
+ init_slashTrim();
50323
50875
  init_src2();
50324
50876
  init_workspace();
50325
50877
  init_channel();
@@ -50337,6 +50889,7 @@ var init_realtime2 = __esm({
50337
50889
  "use strict";
50338
50890
  init_realtime();
50339
50891
  init_storage();
50892
+ init_s3Client();
50340
50893
  init_workspace();
50341
50894
  init_channel();
50342
50895
  init_channelMember();