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
@@ -13,6 +13,30 @@ var __export = (target, all) => {
13
13
  __defProp(target, name, { get: all[name], enumerable: true });
14
14
  };
15
15
 
16
+ // src/slashTrim.ts
17
+ function rtrimSlashes(s) {
18
+ let end = s.length;
19
+ while (end > 0 && s.charCodeAt(end - 1) === SLASH) end--;
20
+ return end === s.length ? s : s.slice(0, end);
21
+ }
22
+ function trimChar(s, code) {
23
+ let start2 = 0;
24
+ let end = s.length;
25
+ while (start2 < end && s.charCodeAt(start2) === code) start2++;
26
+ while (end > start2 && s.charCodeAt(end - 1) === code) end--;
27
+ return start2 === 0 && end === s.length ? s : s.slice(start2, end);
28
+ }
29
+ function trimSlashes(s) {
30
+ return trimChar(s, SLASH);
31
+ }
32
+ var SLASH;
33
+ var init_slashTrim = __esm({
34
+ "src/slashTrim.ts"() {
35
+ "use strict";
36
+ SLASH = 47;
37
+ }
38
+ });
39
+
16
40
  // src/constants.ts
17
41
  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;
18
42
  var init_constants = __esm({
@@ -1101,7 +1125,7 @@ function resolveAlgorithm(algorithm) {
1101
1125
  return chosen;
1102
1126
  }
1103
1127
  function base64urlEncode(data) {
1104
- return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1128
+ return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
1105
1129
  }
1106
1130
  function base64urlDecode(str) {
1107
1131
  let s = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -1380,6 +1404,12 @@ var init_types = __esm({
1380
1404
  });
1381
1405
 
1382
1406
  // ../orm/src/databaseResult.ts
1407
+ function stripTypeArgs(colType) {
1408
+ const open = colType.indexOf("(");
1409
+ if (open < 0) return colType;
1410
+ const close = colType.indexOf(")", open);
1411
+ return close < 0 ? colType : colType.slice(0, open) + colType.slice(close + 1);
1412
+ }
1383
1413
  var DatabaseResult;
1384
1414
  var init_databaseResult = __esm({
1385
1415
  "../orm/src/databaseResult.ts"() {
@@ -1562,7 +1592,7 @@ var init_databaseResult = __esm({
1562
1592
  const [size, decimals] = this._parseTypeSize(colType);
1563
1593
  return {
1564
1594
  name: col.name,
1565
- type: colType.replace(/\(.*\)/, ""),
1595
+ type: stripTypeArgs(colType),
1566
1596
  size,
1567
1597
  decimals,
1568
1598
  nullable: col.nullable ?? true,
@@ -2119,12 +2149,49 @@ var init_point = __esm({
2119
2149
  });
2120
2150
 
2121
2151
  // ../orm/src/sqlTranslator.ts
2122
- var SQLTranslator, QueryCache;
2152
+ function skipQuoted(sql, i, backslashEscapes) {
2153
+ const c = sql[i];
2154
+ const escapeString = c === "'" && (sql[i - 1] === "E" || sql[i - 1] === "e") && !isIdentChar(sql[i - 2]);
2155
+ const backslash = c !== "`" && (escapeString || backslashEscapes);
2156
+ const n = sql.length;
2157
+ i++;
2158
+ while (i < n) {
2159
+ if (backslash && sql[i] === "\\") {
2160
+ i += 2;
2161
+ continue;
2162
+ }
2163
+ if (sql[i] === c) {
2164
+ if (sql[i + 1] === c) {
2165
+ i += 2;
2166
+ continue;
2167
+ }
2168
+ return i + 1;
2169
+ }
2170
+ i++;
2171
+ }
2172
+ return i;
2173
+ }
2174
+ function skipLineComment(sql, i) {
2175
+ while (i < sql.length && sql[i] !== "\n") i++;
2176
+ return i;
2177
+ }
2178
+ function skipBlockComment(sql, i) {
2179
+ const end = sql.indexOf("*/", i + 2);
2180
+ return end === -1 ? sql.length : end + 2;
2181
+ }
2182
+ function skipDollarQuote(sql, i) {
2183
+ const tag = sql.slice(i).match(/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/);
2184
+ if (!tag) return i;
2185
+ const end = sql.indexOf(tag[0], i + tag[0].length);
2186
+ return end === -1 ? sql.length : end + tag[0].length;
2187
+ }
2188
+ var isIdentChar, SQLTranslator, QueryCache;
2123
2189
  var init_sqlTranslator = __esm({
2124
2190
  "../orm/src/sqlTranslator.ts"() {
2125
2191
  "use strict";
2126
2192
  init_databaseUrl();
2127
2193
  init_point();
2194
+ isIdentChar = (ch) => !!ch && /[A-Za-z0-9_]/.test(ch);
2128
2195
  SQLTranslator = class _SQLTranslator {
2129
2196
  static SPATIAL_ENGINES = /* @__PURE__ */ new Set(["postgres", "postgresql"]);
2130
2197
  static SPATIAL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -2405,47 +2472,26 @@ var init_sqlTranslator = __esm({
2405
2472
  static placeholderPositions(sql, options = {}) {
2406
2473
  const positions = [];
2407
2474
  const n = sql.length;
2408
- const isIdent = (ch) => !!ch && /[A-Za-z0-9_]/.test(ch);
2409
2475
  let i = 0;
2410
2476
  while (i < n) {
2411
2477
  const c = sql[i];
2412
2478
  const next = sql[i + 1];
2413
2479
  if (c === "'" || c === '"' || c === "`") {
2414
- const prev = sql[i - 1];
2415
- const escapeString = c === "'" && (prev === "E" || prev === "e") && !isIdent(sql[i - 2]);
2416
- const backslash = c !== "`" && (escapeString || options.backslashEscapes === true);
2417
- i++;
2418
- while (i < n) {
2419
- if (backslash && sql[i] === "\\") {
2420
- i += 2;
2421
- continue;
2422
- }
2423
- if (sql[i] === c) {
2424
- if (sql[i + 1] === c) {
2425
- i += 2;
2426
- continue;
2427
- }
2428
- i++;
2429
- break;
2430
- }
2431
- i++;
2432
- }
2480
+ i = skipQuoted(sql, i, options.backslashEscapes === true);
2433
2481
  continue;
2434
2482
  }
2435
2483
  if (c === "-" && next === "-") {
2436
- while (i < n && sql[i] !== "\n") i++;
2484
+ i = skipLineComment(sql, i);
2437
2485
  continue;
2438
2486
  }
2439
2487
  if (c === "/" && next === "*") {
2440
- const end = sql.indexOf("*/", i + 2);
2441
- i = end === -1 ? n : end + 2;
2488
+ i = skipBlockComment(sql, i);
2442
2489
  continue;
2443
2490
  }
2444
- if (c === "$" && !isIdent(sql[i - 1])) {
2445
- const tag = sql.slice(i).match(/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/);
2446
- if (tag) {
2447
- const end = sql.indexOf(tag[0], i + tag[0].length);
2448
- i = end === -1 ? n : end + tag[0].length;
2491
+ if (c === "$" && !isIdentChar(sql[i - 1])) {
2492
+ const past = skipDollarQuote(sql, i);
2493
+ if (past !== i) {
2494
+ i = past;
2449
2495
  continue;
2450
2496
  }
2451
2497
  }
@@ -2478,7 +2524,7 @@ var init_sqlTranslator = __esm({
2478
2524
  * → { sql: "INSERT INTO t (x) VALUES (1)", columns: ["id", "name"] }
2479
2525
  */
2480
2526
  static parseReturning(sql) {
2481
- const m = sql.match(/\bRETURNING\s+(.+)$/i);
2527
+ const m = sql.match(/\bRETURNING\s+(\S.*)$/i);
2482
2528
  if (!m) return { sql, columns: [] };
2483
2529
  const columns = m[1].split(",").map((c) => c.trim());
2484
2530
  return {
@@ -2532,7 +2578,12 @@ var init_sqlTranslator = __esm({
2532
2578
  // The `d` flag records group indices, so the head can be sliced at the exact
2533
2579
  // start of the VALUES group rather than by hunting for a parenthesis (the
2534
2580
  // column list has parentheses too).
2535
- static INSERT_VALUES = /^\s*INSERT\s+INTO\s+.+?\s+VALUES\s*\(([^()]*)\)\s*$/dis;
2581
+ static INSERT_VALUES = (
2582
+ // ReDoS-safe: the table ref matches `\S(?:[\s\S]*?\S)?` (bounded by non-space
2583
+ // at both ends) so the separator `\s+` before VALUES can't overlap it the way
2584
+ // the old `.+?\s+` did (polynomial on `INSERT INTO ` + a long space run).
2585
+ /^\s*INSERT\s+INTO\s+\S(?:[\s\S]*?\S)?\s+VALUES\s*\(([^()]*)\)\s*$/dis
2586
+ );
2536
2587
  /**
2537
2588
  * Engines whose lastInsertId reports the FIRST generated id of a multi-row
2538
2589
  * INSERT rather than the last. Verified live, not assumed: a 3-row insert
@@ -7272,7 +7323,7 @@ __export(database_exports, {
7272
7323
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
7273
7324
  function stripTrailingSemicolons(sql) {
7274
7325
  if (!sql) return sql;
7275
- let stripped = sql.replace(/\s+$/, "");
7326
+ let stripped = sql.trimEnd();
7276
7327
  while (stripped.endsWith(";")) {
7277
7328
  stripped = stripped.slice(0, -1).replace(/\s+$/, "");
7278
7329
  }
@@ -7439,6 +7490,13 @@ async function createAdapterFromUrl(url, username, password) {
7439
7490
  }
7440
7491
  async function buildAdapterFromUrl(url, username, password) {
7441
7492
  const parsed = parseDatabaseUrl(url, username, password);
7493
+ const serverConfig = {
7494
+ host: parsed.host ?? void 0,
7495
+ port: parsed.port ?? void 0,
7496
+ user: parsed.username ?? void 0,
7497
+ password: parsed.password ?? void 0,
7498
+ database: parsed.database || void 0
7499
+ };
7442
7500
  switch (parsed.engine) {
7443
7501
  case "sqlite": {
7444
7502
  const { SQLiteAdapter: SQLiteAdapter2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
@@ -7446,49 +7504,25 @@ async function buildAdapterFromUrl(url, username, password) {
7446
7504
  }
7447
7505
  case "postgres": {
7448
7506
  const { PostgresAdapter: PostgresAdapter2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
7449
- const adapter = new PostgresAdapter2({
7450
- host: parsed.host ?? void 0,
7451
- port: parsed.port ?? void 0,
7452
- user: parsed.username ?? void 0,
7453
- password: parsed.password ?? void 0,
7454
- database: parsed.database || void 0
7455
- });
7507
+ const adapter = new PostgresAdapter2(serverConfig);
7456
7508
  await adapter.connect();
7457
7509
  return adapter;
7458
7510
  }
7459
7511
  case "mysql": {
7460
7512
  const { MysqlAdapter: MysqlAdapter2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
7461
- const adapter = new MysqlAdapter2({
7462
- host: parsed.host ?? void 0,
7463
- port: parsed.port ?? void 0,
7464
- user: parsed.username ?? void 0,
7465
- password: parsed.password ?? void 0,
7466
- database: parsed.database || void 0
7467
- });
7513
+ const adapter = new MysqlAdapter2(serverConfig);
7468
7514
  await adapter.connect();
7469
7515
  return adapter;
7470
7516
  }
7471
7517
  case "mssql": {
7472
7518
  const { MssqlAdapter: MssqlAdapter2 } = await Promise.resolve().then(() => (init_mssql(), mssql_exports));
7473
- const adapter = new MssqlAdapter2({
7474
- host: parsed.host ?? void 0,
7475
- port: parsed.port ?? void 0,
7476
- user: parsed.username ?? void 0,
7477
- password: parsed.password ?? void 0,
7478
- database: parsed.database || void 0
7479
- });
7519
+ const adapter = new MssqlAdapter2(serverConfig);
7480
7520
  await adapter.connect();
7481
7521
  return adapter;
7482
7522
  }
7483
7523
  case "firebird": {
7484
7524
  const { FirebirdAdapter: FirebirdAdapter2 } = await Promise.resolve().then(() => (init_firebird(), firebird_exports));
7485
- const adapter = new FirebirdAdapter2({
7486
- host: parsed.host ?? void 0,
7487
- port: parsed.port ?? void 0,
7488
- user: parsed.username ?? void 0,
7489
- password: parsed.password ?? void 0,
7490
- database: parsed.database || void 0
7491
- });
7525
+ const adapter = new FirebirdAdapter2(serverConfig);
7492
7526
  await adapter.connect();
7493
7527
  return adapter;
7494
7528
  }
@@ -10224,9 +10258,11 @@ __export(engine_exports, {
10224
10258
  escapeHtml: () => escapeHtml,
10225
10259
  expressionFormCache: () => expressionFormCache,
10226
10260
  filterChainCache: () => filterChainCache,
10261
+ frondRange: () => frondRange,
10227
10262
  pathParseCache: () => pathParseCache,
10228
10263
  setCspNonceProvider: () => setCspNonceProvider,
10229
- setFormTokenSessionId: () => setFormTokenSessionId
10264
+ setFormTokenSessionId: () => setFormTokenSessionId,
10265
+ setFormTokenSessionProvider: () => setFormTokenSessionProvider
10230
10266
  });
10231
10267
  import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
10232
10268
  import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync4, realpathSync } from "node:fs";
@@ -10311,6 +10347,14 @@ function renderDump(value) {
10311
10347
  const escaped = dumped.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10312
10348
  return new SafeString(`<pre>${escaped}</pre>`);
10313
10349
  }
10350
+ function stripTags(input) {
10351
+ let s = input, prev;
10352
+ do {
10353
+ prev = s;
10354
+ s = s.replace(STRIP_TAGS_RE, "");
10355
+ } while (s !== prev);
10356
+ return s;
10357
+ }
10314
10358
  function liveAttr(value) {
10315
10359
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
10316
10360
  }
@@ -10328,35 +10372,58 @@ function sweepExpiredCache(cache) {
10328
10372
  if (expiresAt2 <= now) cache.delete(key);
10329
10373
  }
10330
10374
  }
10331
- function tokenize(source) {
10375
+ function extractRawBlocks(source) {
10332
10376
  const rawBlocks = [];
10333
- source = source.replace(RAW_BLOCK_RE, (_match, content) => {
10377
+ let out = "";
10378
+ let pos = 0;
10379
+ RAW_OPEN_RE.lastIndex = 0;
10380
+ let om;
10381
+ while ((om = RAW_OPEN_RE.exec(source)) !== null) {
10382
+ const openStart = om.index;
10383
+ const openEnd = RAW_OPEN_RE.lastIndex;
10384
+ RAW_CLOSE_RE.lastIndex = openEnd;
10385
+ const cm = RAW_CLOSE_RE.exec(source);
10386
+ if (cm === null) break;
10334
10387
  const idx = rawBlocks.length;
10335
- rawBlocks.push(content);
10336
- return `\0RAW_${idx}\0`;
10337
- });
10388
+ rawBlocks.push(source.slice(openEnd, cm.index));
10389
+ out += source.slice(pos, openStart) + `\0RAW_${idx}\0`;
10390
+ pos = RAW_CLOSE_RE.lastIndex;
10391
+ RAW_OPEN_RE.lastIndex = pos;
10392
+ }
10393
+ out += source.slice(pos);
10394
+ return { source: out, rawBlocks };
10395
+ }
10396
+ function tokenize(rawSource) {
10397
+ const { source, rawBlocks } = extractRawBlocks(rawSource);
10338
10398
  const tokens = [];
10399
+ const n = source.length;
10339
10400
  let pos = 0;
10340
- TOKEN_RE.lastIndex = 0;
10341
- let m;
10342
- while ((m = TOKEN_RE.exec(source)) !== null) {
10343
- const start2 = m.index;
10344
- if (start2 > pos) {
10345
- tokens.push(["TEXT", source.slice(pos, start2)]);
10401
+ let scan = 0;
10402
+ const exhausted = { "%}": false, "}}": false, "#}": false };
10403
+ while (scan < n) {
10404
+ const brace = source.indexOf("{", scan);
10405
+ if (brace === -1) break;
10406
+ const kind = TAG_KINDS[source[brace + 1]];
10407
+ if (!kind) {
10408
+ scan = brace + 1;
10409
+ continue;
10346
10410
  }
10347
- const raw = m[0];
10348
- if (raw.startsWith("{#")) {
10349
- tokens.push(["COMMENT", raw]);
10350
- } else if (raw.startsWith("{{")) {
10351
- tokens.push(["VAR", raw]);
10352
- } else if (raw.startsWith("{%")) {
10353
- tokens.push(["BLOCK", raw]);
10411
+ if (exhausted[kind.close]) {
10412
+ scan = brace + 1;
10413
+ continue;
10354
10414
  }
10355
- pos = m.index + raw.length;
10356
- }
10357
- if (pos < source.length) {
10358
- tokens.push(["TEXT", source.slice(pos)]);
10415
+ const close = source.indexOf(kind.close, brace + 2);
10416
+ if (close === -1) {
10417
+ exhausted[kind.close] = true;
10418
+ scan = brace + 1;
10419
+ continue;
10420
+ }
10421
+ if (brace > pos) tokens.push(["TEXT", source.slice(pos, brace)]);
10422
+ tokens.push([kind.type, source.slice(brace, close + 2)]);
10423
+ pos = close + 2;
10424
+ scan = close + 2;
10359
10425
  }
10426
+ if (pos < n) tokens.push(["TEXT", source.slice(pos)]);
10360
10427
  if (rawBlocks.length > 0) {
10361
10428
  for (let i = 0; i < tokens.length; i++) {
10362
10429
  if (tokens[i][0] === "TEXT" && tokens[i][1].includes("\0RAW_")) {
@@ -10682,26 +10749,123 @@ function evalConcatOrComparison(expr, context) {
10682
10749
  }
10683
10750
  return EXPR_NOT_MATCHED;
10684
10751
  }
10752
+ function topLevelPositions(expr) {
10753
+ const positions = [];
10754
+ let inQuote = null;
10755
+ let depth = 0;
10756
+ for (let pos = 0; pos < expr.length; pos++) {
10757
+ const ch = expr[pos];
10758
+ if (inQuote) {
10759
+ if (ch === inQuote) inQuote = null;
10760
+ } else if (ch === '"' || ch === "'") {
10761
+ inQuote = ch;
10762
+ } else if ("([{".includes(ch)) {
10763
+ depth++;
10764
+ } else if (")]}".includes(ch)) {
10765
+ depth--;
10766
+ } else if (depth === 0) {
10767
+ positions.push(pos);
10768
+ }
10769
+ }
10770
+ return positions;
10771
+ }
10772
+ function followsOperand(expr, pos) {
10773
+ return /[\w)\]'"]$/.test(expr.slice(0, pos).trimEnd());
10774
+ }
10775
+ function topLevelArithmeticOperators(expr) {
10776
+ const operators = [];
10777
+ let skipUntil = -1;
10778
+ for (const pos of topLevelPositions(expr)) {
10779
+ if (pos < skipUntil || !"+-*/%".includes(expr[pos])) continue;
10780
+ const pair = expr.slice(pos, pos + 2);
10781
+ const op = pair === "**" || pair === "//" ? pair : expr[pos];
10782
+ skipUntil = pos + op.length;
10783
+ if (followsOperand(expr, pos)) operators.push([pos, op]);
10784
+ }
10785
+ return operators;
10786
+ }
10787
+ function splitArithmetic(expr) {
10788
+ const operators = topLevelArithmeticOperators(expr);
10789
+ for (const level of ARITHMETIC_LEVELS) {
10790
+ const matches = operators.filter(([, op]) => level.includes(op));
10791
+ if (matches.length > 0) return level[0] === "**" ? matches[0] : matches[matches.length - 1];
10792
+ }
10793
+ return null;
10794
+ }
10685
10795
  function evalArithmeticExpression(expr, context) {
10686
- for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
10687
- const pos = findOutsideQuotes(expr, op);
10688
- if (pos >= 0) {
10689
- const left = expr.slice(0, pos).trim();
10690
- const right = expr.slice(pos + op.length).trim();
10691
- const lVal = evalExpr(left, context);
10692
- const rVal = evalExpr(right, context);
10693
- try {
10694
- let lNum = lVal != null ? Number(lVal) : 0;
10695
- let rNum = rVal != null ? Number(rVal) : 0;
10696
- if (isNaN(lNum)) lNum = 0;
10697
- if (isNaN(rNum)) rNum = 0;
10698
- return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
10699
- } catch {
10700
- return null;
10701
- }
10796
+ const split = splitArithmetic(expr);
10797
+ if (!split) return EXPR_NOT_MATCHED;
10798
+ const [pos, op] = split;
10799
+ const lVal = evalExpr(expr.slice(0, pos).trim(), context);
10800
+ const rVal = evalExpr(expr.slice(pos + op.length).trim(), context);
10801
+ let lNum = lVal != null ? Number(lVal) : 0;
10802
+ let rNum = rVal != null ? Number(rVal) : 0;
10803
+ if (isNaN(lNum)) lNum = 0;
10804
+ if (isNaN(rNum)) rNum = 0;
10805
+ return ARITHMETIC_OPERATIONS[op](lNum, rNum);
10806
+ }
10807
+ function frondRange(start2, end, step = 1) {
10808
+ const numeric = (value) => typeof value === "number" || typeof value === "string" && NUMERIC_RE.test(value);
10809
+ if (!numeric(start2) || !numeric(end) || !numeric(step)) return [];
10810
+ const first = Math.trunc(Number(start2));
10811
+ const last = Math.trunc(Number(end));
10812
+ const stride = Math.abs(Math.trunc(Number(step)));
10813
+ if (stride === 0) return [];
10814
+ const values = [];
10815
+ if (first <= last) for (let value = first; value <= last; value += stride) values.push(value);
10816
+ else for (let value = first; value >= last; value -= stride) values.push(value);
10817
+ return values;
10818
+ }
10819
+ function findRangeOperator(expr) {
10820
+ let pos = findOutsideQuotes(expr, "..");
10821
+ while (pos >= 0) {
10822
+ const partOfEllipsis = expr.startsWith("...", pos) || pos > 0 && expr[pos - 1] === ".";
10823
+ if (pos > 0 && !partOfEllipsis) return findOutsideQuotes(expr.slice(pos + 2), "|") >= 0 ? -1 : pos;
10824
+ const next = findOutsideQuotes(expr.slice(pos + 2), "..");
10825
+ pos = next >= 0 ? pos + 2 + next : -1;
10826
+ }
10827
+ return -1;
10828
+ }
10829
+ function evalRangeExpression(expr, context) {
10830
+ const pos = findRangeOperator(expr);
10831
+ if (pos < 0) return EXPR_NOT_MATCHED;
10832
+ return frondRange(evalExpr(expr.slice(0, pos).trim(), context), evalExpr(expr.slice(pos + 2).trim(), context));
10833
+ }
10834
+ function closingBracketIndex(expr) {
10835
+ let depth = 0;
10836
+ let inQuote = null;
10837
+ for (let i = 0; i < expr.length; i++) {
10838
+ const ch = expr[i];
10839
+ if (inQuote) {
10840
+ if (ch === inQuote) inQuote = null;
10841
+ } else if (ch === '"' || ch === "'") {
10842
+ inQuote = ch;
10843
+ } else if ("([{".includes(ch)) {
10844
+ depth++;
10845
+ } else if (")]}".includes(ch) && --depth === 0) {
10846
+ return i;
10702
10847
  }
10703
10848
  }
10704
- return EXPR_NOT_MATCHED;
10849
+ return -1;
10850
+ }
10851
+ function mapLiteralKey(rawKey, context) {
10852
+ const quoted = rawKey.length >= 2 && (rawKey[0] === '"' || rawKey[0] === "'") && rawKey.endsWith(rawKey[0]);
10853
+ if (quoted) return rawKey.slice(1, -1);
10854
+ return BARE_KEY_RE.test(rawKey) ? rawKey : String(evalExpr(rawKey, context));
10855
+ }
10856
+ function evalMapLiteral(inner, context) {
10857
+ const map = {};
10858
+ for (const pair of inner === "" ? [] : splitArgs(inner)) {
10859
+ const colon = findOutsideQuotes(pair, ":");
10860
+ if (colon > 0) map[mapLiteralKey(pair.slice(0, colon).trim(), context)] = evalExpr(pair.slice(colon + 1).trim(), context);
10861
+ }
10862
+ return map;
10863
+ }
10864
+ function evalCollectionLiteral(expr, context) {
10865
+ if (expr[0] !== "[" && expr[0] !== "{" || closingBracketIndex(expr) !== expr.length - 1) return EXPR_NOT_MATCHED;
10866
+ const inner = expr.slice(1, -1).trim();
10867
+ if (expr[0] === "{") return evalMapLiteral(inner, context);
10868
+ return inner === "" ? [] : splitArgs(inner).map((item) => evalExpr(item.trim(), context));
10705
10869
  }
10706
10870
  function evalFilterExpression(expr, context) {
10707
10871
  if (findOutsideQuotes(expr, "|") >= 0) {
@@ -11123,12 +11287,12 @@ function splitArgs(raw) {
11123
11287
  current += ch;
11124
11288
  continue;
11125
11289
  }
11126
- if (ch === "(" || ch === "[") {
11290
+ if (ch === "(" || ch === "[" || ch === "{") {
11127
11291
  depth++;
11128
11292
  current += ch;
11129
11293
  continue;
11130
11294
  }
11131
- if (ch === ")" || ch === "]") {
11295
+ if (ch === ")" || ch === "]" || ch === "}") {
11132
11296
  depth--;
11133
11297
  current += ch;
11134
11298
  continue;
@@ -11293,11 +11457,14 @@ function formatMatch(match, flags, width, prec, type2, arg) {
11293
11457
  return padFormattedValue(formatValue(type2, precision, arg), flags, width);
11294
11458
  }
11295
11459
  function _b64url(data) {
11296
- return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11460
+ return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
11297
11461
  }
11298
11462
  function setFormTokenSessionId(sessionId) {
11299
11463
  _formTokenSessionId = sessionId || "";
11300
11464
  }
11465
+ function setFormTokenSessionProvider(fn) {
11466
+ _formTokenSessionProvider = fn;
11467
+ }
11301
11468
  function setCspNonceProvider(fn) {
11302
11469
  _cspNonceProvider = fn;
11303
11470
  }
@@ -11322,8 +11489,9 @@ function _buildFormTokenJwt(descriptor = "") {
11322
11489
  payload.context = descriptor;
11323
11490
  }
11324
11491
  }
11325
- if (_formTokenSessionId) {
11326
- payload.session_id = _formTokenSessionId;
11492
+ const sessionId = _formTokenSessionProvider() || _formTokenSessionId;
11493
+ if (sessionId) {
11494
+ payload.session_id = sessionId;
11327
11495
  }
11328
11496
  const h2 = _b64url(Buffer.from(JSON.stringify(header)));
11329
11497
  const p = _b64url(Buffer.from(JSON.stringify(payload)));
@@ -11339,7 +11507,7 @@ function _generateFormToken(descriptor = "") {
11339
11507
  function _generateFormTokenValue(descriptor = "") {
11340
11508
  return new SafeString(_buildFormTokenJwt(descriptor));
11341
11509
  }
11342
- 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;
11510
+ 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;
11343
11511
  var init_engine = __esm({
11344
11512
  "../frond/src/engine.ts"() {
11345
11513
  "use strict";
@@ -11441,7 +11609,7 @@ var init_engine = __esm({
11441
11609
  int: (value) => value ? parseInt(String(value), 10) || 0 : 0,
11442
11610
  float: (value) => value ? parseFloat(String(value)) || 0 : 0,
11443
11611
  abs: (value) => typeof value === "number" ? Math.abs(value) : value,
11444
- striptags: (value) => String(value).replace(STRIP_TAGS_RE, ""),
11612
+ striptags: (value) => stripTags(String(value)),
11445
11613
  first: (value) => Array.isArray(value) ? value[0] ?? null : null,
11446
11614
  last: (value) => Array.isArray(value) ? value[value.length - 1] ?? null : null,
11447
11615
  keys: (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : [],
@@ -11455,20 +11623,23 @@ var init_engine = __esm({
11455
11623
  filter: (value) => Array.isArray(value) ? value.filter(Boolean) : value
11456
11624
  };
11457
11625
  FORMAT_RE = /%%|%([-+ 0]*)(\d+)?(?:\.(\d+))?([sdifFeEgGxXob])/g;
11458
- LEADING_WS_RE = /^\s+/;
11459
- TRAILING_WS_RE = /\s+$/;
11460
11626
  THOUSANDS_RE = /\B(?=(\d{3})+(?!\d))/g;
11461
11627
  LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
11462
11628
  LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
11463
11629
  LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
11464
- EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
11465
- EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
11630
+ EXTENDS_RE = /\{%[-\s]*extends\s+["']([^"']+)["']\s*[-]?%\}/;
11631
+ EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["']([^"']+)["']\s*[-]?%\}/g;
11466
11632
  filterChainCache = /* @__PURE__ */ new Map();
11467
11633
  pathParseCache = /* @__PURE__ */ new Map();
11468
11634
  TEMPLATE_CACHE_MAX = 256;
11469
11635
  MEMO_CACHE_MAX = 1024;
11470
- TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
11471
- RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
11636
+ RAW_OPEN_RE = /\{%-?\s*raw\s*-?%\}/g;
11637
+ RAW_CLOSE_RE = /\{%-?\s*endraw\s*-?%\}/g;
11638
+ TAG_KINDS = {
11639
+ "%": { close: "%}", type: "BLOCK" },
11640
+ "{": { close: "}}", type: "VAR" },
11641
+ "#": { close: "#}", type: "COMMENT" }
11642
+ };
11472
11643
  EXPR_NOT_MATCHED = /* @__PURE__ */ Symbol("frond-expression-not-matched");
11473
11644
  ARITHMETIC_OPERATIONS = {
11474
11645
  "+": (left, right) => left + right,
@@ -11479,10 +11650,14 @@ var init_engine = __esm({
11479
11650
  "%": (left, right) => right !== 0 ? left % right : 0,
11480
11651
  "**": (left, right) => left ** right
11481
11652
  };
11653
+ ARITHMETIC_LEVELS = [["+", "-"], ["*", "//", "/", "%"], ["**"]];
11654
+ BARE_KEY_RE = /^[A-Za-z_]\w*$/;
11482
11655
  EXPR_EVALUATORS = [
11656
+ evalCollectionLiteral,
11483
11657
  evalPrimary,
11484
11658
  evalConditional,
11485
11659
  evalConcatOrComparison,
11660
+ evalRangeExpression,
11486
11661
  evalArithmeticExpression,
11487
11662
  evalFilterExpression,
11488
11663
  evalFunctionExpression
@@ -11538,8 +11713,8 @@ var init_engine = __esm({
11538
11713
  },
11539
11714
  title: (v) => String(v).replace(TITLE_WORD_RE, (c) => c.toUpperCase()),
11540
11715
  trim: (v) => String(v).trim(),
11541
- ltrim: (v) => String(v).replace(LEADING_WS_RE, ""),
11542
- rtrim: (v) => String(v).replace(TRAILING_WS_RE, ""),
11716
+ ltrim: (v) => String(v).trimStart(),
11717
+ rtrim: (v) => String(v).trimEnd(),
11543
11718
  length: (v) => {
11544
11719
  if (Array.isArray(v)) return v.length;
11545
11720
  if (typeof v === "string") return v.length;
@@ -11580,7 +11755,7 @@ var init_engine = __esm({
11580
11755
  safe: (v) => v,
11581
11756
  escape: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
11582
11757
  e: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
11583
- striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
11758
+ striptags: (v) => stripTags(String(v)),
11584
11759
  nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
11585
11760
  abs: (v) => typeof v === "number" ? Math.abs(v) : v,
11586
11761
  round: (v, decimals) => {
@@ -11693,6 +11868,7 @@ var init_engine = __esm({
11693
11868
  js_escape: (v) => new SafeString(jsEscape(v))
11694
11869
  };
11695
11870
  _formTokenSessionId = "";
11871
+ _formTokenSessionProvider = () => "";
11696
11872
  _cspNonceProvider = () => "";
11697
11873
  Frond = class _Frond {
11698
11874
  // ── Class-level registries ──────────────────────────────────
@@ -11846,6 +12022,7 @@ var init_engine = __esm({
11846
12022
  this.globals.form_token = (descriptor) => _generateFormToken(descriptor || "");
11847
12023
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
11848
12024
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
12025
+ this.globals.range = frondRange;
11849
12026
  this.globals.dump = (value) => renderDump(value);
11850
12027
  this.globals.csp_nonce = () => _cspNonceProvider();
11851
12028
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
@@ -12182,20 +12359,20 @@ var init_engine = __esm({
12182
12359
  }
12183
12360
  renderVarToken(tokens, index, context, output) {
12184
12361
  const [content, stripB, stripA] = stripTag(tokens[index][1]);
12185
- if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
12362
+ if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].trimEnd();
12186
12363
  const result = this.evalVar(content, context);
12187
12364
  output.push(result !== null && result !== void 0 ? String(result) : "");
12188
12365
  if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
12189
- tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
12366
+ tokens[index + 1] = ["TEXT", tokens[index + 1][1].trimStart()];
12190
12367
  }
12191
12368
  return index + 1;
12192
12369
  }
12193
12370
  renderBlockToken(tokens, index, context, output) {
12194
12371
  const [content, stripB, stripA] = stripTag(tokens[index][1]);
12195
- if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
12372
+ if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].trimEnd();
12196
12373
  const tag = content.split(/\s+/)[0] || "";
12197
12374
  if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
12198
- tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
12375
+ tokens[index + 1] = ["TEXT", tokens[index + 1][1].trimStart()];
12199
12376
  }
12200
12377
  let next;
12201
12378
  if (tag === "if" && this.tagPermitted(tag)) {
@@ -12212,7 +12389,7 @@ var init_engine = __esm({
12212
12389
  next = block.next;
12213
12390
  }
12214
12391
  if (stripA && next < tokens.length && tokens[next][0] === "TEXT") {
12215
- tokens[next] = ["TEXT", tokens[next][1].replace(LEADING_WS_RE, "")];
12392
+ tokens[next] = ["TEXT", tokens[next][1].trimStart()];
12216
12393
  }
12217
12394
  return next;
12218
12395
  }
@@ -12471,7 +12648,7 @@ var init_engine = __esm({
12471
12648
  pushIfBranch(branches, condition, branchTokens, stripBefore) {
12472
12649
  if (stripBefore && branchTokens.length > 0 && branchTokens[branchTokens.length - 1][0] === "TEXT") {
12473
12650
  const last = branchTokens[branchTokens.length - 1];
12474
- branchTokens[branchTokens.length - 1] = ["TEXT", last[1].replace(TRAILING_WS_RE, "")];
12651
+ branchTokens[branchTokens.length - 1] = ["TEXT", last[1].trimEnd()];
12475
12652
  }
12476
12653
  branches.push([condition, branchTokens]);
12477
12654
  }
@@ -12499,7 +12676,7 @@ var init_engine = __esm({
12499
12676
  } else if (tag === "endif") {
12500
12677
  this.pushIfBranch(branches, currentCond, currentTokens, tagStripB);
12501
12678
  if (tagStripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
12502
- tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
12679
+ tokens[i + 1] = ["TEXT", tokens[i + 1][1].trimStart()];
12503
12680
  }
12504
12681
  return { branches, next: i + 1 };
12505
12682
  } else if ((tag === "elseif" || tag === "elif") && depth === 0) {
@@ -13285,7 +13462,7 @@ function tableData(opts) {
13285
13462
  readonly: !opts.editable,
13286
13463
  inline_script: opts.inlineScript,
13287
13464
  table_name: h(opts.tableName),
13288
- table_id_attr: opts.tableId ? ` id="${h(opts.tableId)}"` : ""
13465
+ table_id: opts.tableId ? String(opts.tableId) : ""
13289
13466
  };
13290
13467
  }
13291
13468
  var init_crudTable = __esm({
@@ -13393,13 +13570,13 @@ function renderModalForm(mode, columns, pk, edit) {
13393
13570
  value: "",
13394
13571
  placeholder: h(`Enter ${label.toLowerCase()}`),
13395
13572
  type: "text",
13396
- required_attr: "",
13573
+ required: false,
13397
13574
  input: true
13398
13575
  };
13399
13576
  });
13400
13577
  return renderCrudTemplate("form", {
13401
13578
  wrap: true,
13402
- form_id_attr: ` id="form-${mode}"`,
13579
+ form_id: `form-${mode}`,
13403
13580
  action: "",
13404
13581
  form_method: "POST",
13405
13582
  method_override: null,
@@ -13456,7 +13633,7 @@ function fieldInputShape(field, base) {
13456
13633
  case "text":
13457
13634
  return { ...base, textarea: true };
13458
13635
  case "boolean":
13459
- return { ...base, checkbox: true, checked_attr: value ? " checked" : "" };
13636
+ return { ...base, checkbox: true, checked: !!value };
13460
13637
  case "select":
13461
13638
  return { ...base, select: true, options_html: buildOptions(field.options, value) };
13462
13639
  case "date":
@@ -13479,7 +13656,7 @@ function buildCustomField(field) {
13479
13656
  label: h(String(label)),
13480
13657
  value: h(field.value ?? ""),
13481
13658
  placeholder: "",
13482
- required_attr: field.required ? " required" : ""
13659
+ required: !!field.required
13483
13660
  };
13484
13661
  return fieldInputShape(field, base);
13485
13662
  }
@@ -13487,7 +13664,7 @@ function generateForm(fields, options = {}) {
13487
13664
  const verb = String(options.method ?? "POST").toUpperCase();
13488
13665
  return renderCrudTemplate("form", {
13489
13666
  wrap: true,
13490
- form_id_attr: "",
13667
+ form_id: "",
13491
13668
  action: h(options.action ?? "/"),
13492
13669
  form_method: h(verb),
13493
13670
  method_override: ["PUT", "PATCH", "DELETE"].includes(verb) ? verb : null,
@@ -18138,20 +18315,210 @@ var init_attachment = __esm({
18138
18315
  }
18139
18316
  });
18140
18317
 
18318
+ // ../orm/src/realtime/s3Client.ts
18319
+ import { createHash as createHash4, createHmac as createHmac3 } from "node:crypto";
18320
+ function retryAfterSeconds(raw) {
18321
+ if (raw == null) return null;
18322
+ const n = Number(raw);
18323
+ return Number.isFinite(n) ? Math.max(0, n) : null;
18324
+ }
18325
+ function uriEncode(value, isPath) {
18326
+ let out = "";
18327
+ for (const byte of Buffer.from(value, "utf8")) {
18328
+ const ch = String.fromCharCode(byte);
18329
+ if (UNRESERVED.test(ch) || isPath && ch === "/") out += ch;
18330
+ else out += "%" + byte.toString(16).toUpperCase().padStart(2, "0");
18331
+ }
18332
+ return out;
18333
+ }
18334
+ function sha256Hex2(data) {
18335
+ return createHash4("sha256").update(data).digest("hex");
18336
+ }
18337
+ function hmac(key, msg) {
18338
+ return createHmac3("sha256", key).update(msg, "utf8").digest();
18339
+ }
18340
+ var ALGORITHM, SERVICE, UNRESERVED, MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, S3Error, S3ConfigError, sleep, S3Client;
18341
+ var init_s3Client = __esm({
18342
+ "../orm/src/realtime/s3Client.ts"() {
18343
+ "use strict";
18344
+ ALGORITHM = "AWS4-HMAC-SHA256";
18345
+ SERVICE = "s3";
18346
+ UNRESERVED = /[A-Za-z0-9\-_.~]/;
18347
+ MAX_RETRIES = 2;
18348
+ BACKOFF_BASE = 0.5;
18349
+ BACKOFF_CAP = 20;
18350
+ S3Error = class extends Error {
18351
+ constructor(status2, message) {
18352
+ super(message);
18353
+ this.status = status2;
18354
+ this.name = "S3Error";
18355
+ }
18356
+ status;
18357
+ /** Seconds from a Retry-After header in its delta-seconds form, else null. */
18358
+ retryAfter = null;
18359
+ };
18360
+ S3ConfigError = class extends Error {
18361
+ constructor(message) {
18362
+ super(message);
18363
+ this.name = "S3ConfigError";
18364
+ }
18365
+ };
18366
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18367
+ S3Client = class {
18368
+ scheme;
18369
+ host;
18370
+ // authority (host[:port]) — matches the Host header
18371
+ prefix;
18372
+ // path an endpoint carries (https://host/s3 -> "/s3"), "" if none
18373
+ region;
18374
+ accessKey;
18375
+ secretKey;
18376
+ timeout;
18377
+ constructor(opts = {}) {
18378
+ this.region = opts.region || "us-east-1";
18379
+ this.accessKey = opts.accessKey || "";
18380
+ this.secretKey = opts.secretKey || "";
18381
+ this.timeout = opts.timeout ?? 3e4;
18382
+ let base = opts.endpoint || `https://s3.${this.region}.amazonaws.com`;
18383
+ while (base.endsWith("/")) base = base.slice(0, -1);
18384
+ const m = /^([a-z][a-z0-9+.-]*):\/\/([^/]+)(\/.*)?$/i.exec(base);
18385
+ this.scheme = m ? m[1] : "https";
18386
+ this.host = m ? m[2] : base;
18387
+ let p = m && m[3] ? m[3] : "";
18388
+ while (p.endsWith("/")) p = p.slice(0, -1);
18389
+ this.prefix = p;
18390
+ }
18391
+ // -- public API (the subset S3Storage uses) --------------------------------
18392
+ async putObject(bucket, key, data, contentType = "application/octet-stream") {
18393
+ const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
18394
+ await this.request("PUT", this.canonicalUri(bucket, key), body, { "content-type": contentType });
18395
+ }
18396
+ async getObject(bucket, key) {
18397
+ return (await this.request("GET", this.canonicalUri(bucket, key), Buffer.alloc(0))).body;
18398
+ }
18399
+ async deleteObject(bucket, key) {
18400
+ await this.request("DELETE", this.canonicalUri(bucket, key), Buffer.alloc(0));
18401
+ }
18402
+ async headObject(bucket, key) {
18403
+ await this.request("HEAD", this.canonicalUri(bucket, key), Buffer.alloc(0));
18404
+ }
18405
+ /** Create the bucket (path-style `PUT /<bucket>`); an already-owned bucket (409) is success. */
18406
+ async createBucket(bucket) {
18407
+ try {
18408
+ await this.request("PUT", `${this.prefix}/${uriEncode(bucket, true)}`, Buffer.alloc(0));
18409
+ } catch (e) {
18410
+ if (!(e instanceof S3Error) || e.status !== 409) throw e;
18411
+ }
18412
+ }
18413
+ /** A URL that fetches the object directly, signed via SigV4 query params. */
18414
+ presignedGetUrl(bucket, key, expires = 3600) {
18415
+ this.requireCredentials();
18416
+ const { amzDate, dateStamp } = this.now();
18417
+ const uri = this.canonicalUri(bucket, key);
18418
+ const scope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
18419
+ const query = {
18420
+ "X-Amz-Algorithm": ALGORITHM,
18421
+ "X-Amz-Credential": `${this.accessKey}/${scope}`,
18422
+ "X-Amz-Date": amzDate,
18423
+ "X-Amz-Expires": String(expires),
18424
+ "X-Amz-SignedHeaders": "host"
18425
+ };
18426
+ const canonicalQs = this.canonicalQuery(query);
18427
+ const canonicalRequest = ["GET", uri, canonicalQs, `host:${this.host}
18428
+ `, "host", "UNSIGNED-PAYLOAD"].join("\n");
18429
+ const signature = this.sign(canonicalRequest, amzDate, dateStamp, scope);
18430
+ return `${this.scheme}://${this.host}${uri}?${canonicalQs}&X-Amz-Signature=${signature}`;
18431
+ }
18432
+ // -- signing + transport ----------------------------------------------------
18433
+ requireCredentials() {
18434
+ if (!this.accessKey || !this.secretKey) {
18435
+ throw new S3ConfigError(
18436
+ "S3 credentials are not set: set TINA4_STORAGE_KEY and TINA4_STORAGE_SECRET"
18437
+ );
18438
+ }
18439
+ }
18440
+ now() {
18441
+ const iso2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
18442
+ return { amzDate: iso2, dateStamp: iso2.slice(0, 8) };
18443
+ }
18444
+ canonicalUri(bucket, key) {
18445
+ return `${this.prefix}/${uriEncode(bucket, true)}/${uriEncode(key, true)}`;
18446
+ }
18447
+ canonicalQuery(query) {
18448
+ return Object.keys(query).sort().map((k) => `${uriEncode(k, false)}=${uriEncode(query[k], false)}`).join("&");
18449
+ }
18450
+ signingKey(dateStamp) {
18451
+ const kDate = hmac(`AWS4${this.secretKey}`, dateStamp);
18452
+ const kRegion = hmac(kDate, this.region);
18453
+ const kService = hmac(kRegion, SERVICE);
18454
+ return hmac(kService, "aws4_request");
18455
+ }
18456
+ sign(canonicalRequest, amzDate, dateStamp, scope) {
18457
+ const stringToSign = [ALGORITHM, amzDate, scope, sha256Hex2(canonicalRequest)].join("\n");
18458
+ return createHmac3("sha256", this.signingKey(dateStamp)).update(stringToSign, "utf8").digest("hex");
18459
+ }
18460
+ async request(method, uri, body, extraHeaders = {}) {
18461
+ this.requireCredentials();
18462
+ for (let attempt = 0; ; attempt++) {
18463
+ const last = attempt === MAX_RETRIES;
18464
+ let delay2 = null;
18465
+ try {
18466
+ return await this.send(method, uri, body, extraHeaders);
18467
+ } catch (e) {
18468
+ if (e instanceof S3Error) {
18469
+ if (last || !(e.status === 429 || e.status >= 500)) throw e;
18470
+ delay2 = e.retryAfter;
18471
+ } else {
18472
+ if (last) throw e;
18473
+ delay2 = null;
18474
+ }
18475
+ }
18476
+ if (delay2 == null) delay2 = Math.random() * (BACKOFF_BASE * 2 ** attempt);
18477
+ await sleep(Math.min(delay2, BACKOFF_CAP) * 1e3);
18478
+ }
18479
+ }
18480
+ async send(method, uri, body, extraHeaders = {}) {
18481
+ const { amzDate, dateStamp } = this.now();
18482
+ const payloadHash = sha256Hex2(body);
18483
+ const signed = {
18484
+ host: this.host,
18485
+ "x-amz-content-sha256": payloadHash,
18486
+ "x-amz-date": amzDate
18487
+ };
18488
+ const signedKeys = Object.keys(signed).sort();
18489
+ const canonicalHeaders = signedKeys.map((k) => `${k}:${signed[k]}
18490
+ `).join("");
18491
+ const signedHeaders = signedKeys.join(";");
18492
+ const canonicalRequest = [method, uri, "", canonicalHeaders, signedHeaders, payloadHash].join("\n");
18493
+ const scope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
18494
+ const signature = this.sign(canonicalRequest, amzDate, dateStamp, scope);
18495
+ const authorization = `${ALGORITHM} Credential=${this.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
18496
+ const headers = {
18497
+ Authorization: authorization,
18498
+ "x-amz-content-sha256": payloadHash,
18499
+ "x-amz-date": amzDate,
18500
+ ...extraHeaders
18501
+ };
18502
+ const url = `${this.scheme}://${this.host}${uri}`;
18503
+ const init = { method, headers, redirect: "manual" };
18504
+ if (method === "PUT" || method === "POST") init.body = body;
18505
+ if (AbortSignal.timeout) init.signal = AbortSignal.timeout(this.timeout);
18506
+ const response = await fetch(url, init);
18507
+ if (response.status < 200 || response.status >= 300) {
18508
+ const err = new S3Error(response.status, `${method} ${uri}: HTTP ${response.status}`);
18509
+ err.retryAfter = retryAfterSeconds(response.headers.get("retry-after"));
18510
+ throw err;
18511
+ }
18512
+ return { status: response.status, body: Buffer.from(await response.arrayBuffer()) };
18513
+ }
18514
+ };
18515
+ }
18516
+ });
18517
+
18141
18518
  // ../orm/src/realtime/storage.ts
18142
18519
  import { randomBytes as randomBytes5 } from "node:crypto";
18143
18520
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, statSync as statSync5 } from "node:fs";
18144
18521
  import { resolve as resolve6, sep as sep3 } from "node:path";
18145
- import { createRequire as createRequire6 } from "node:module";
18146
- function loadAwsSdk(packageName) {
18147
- try {
18148
- return require2(packageName);
18149
- } catch (error2) {
18150
- const missing = error2?.code === "MODULE_NOT_FOUND" && String(error2?.message ?? "").includes(`'${packageName}'`);
18151
- if (!missing) throw error2;
18152
- throw optionalPackageMissing("S3Storage", S3_PACKAGES);
18153
- }
18154
- }
18155
18522
  function storageKey(filename = "") {
18156
18523
  let ext = "";
18157
18524
  if (filename && filename.includes(".")) {
@@ -18176,14 +18543,12 @@ function selectStorage(storage) {
18176
18543
  }
18177
18544
  return new LocalStorage();
18178
18545
  }
18179
- var require2, S3_PACKAGES, UNSAFE, LocalStorage, S3Storage;
18546
+ var UNSAFE, LocalStorage, S3Storage;
18180
18547
  var init_storage = __esm({
18181
18548
  "../orm/src/realtime/storage.ts"() {
18182
18549
  "use strict";
18183
18550
  init_index();
18184
- init_optionalPackage();
18185
- require2 = createRequire6(import.meta.url);
18186
- S3_PACKAGES = ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"];
18551
+ init_s3Client();
18187
18552
  UNSAFE = /[^A-Za-z0-9]/g;
18188
18553
  LocalStorage = class {
18189
18554
  directory;
@@ -18233,51 +18598,36 @@ var init_storage = __esm({
18233
18598
  constructor(opts = {}) {
18234
18599
  this.bucket = opts.bucket || process.env.TINA4_STORAGE_BUCKET || "";
18235
18600
  if (!this.bucket) throw new Error("S3Storage requires TINA4_STORAGE_BUCKET");
18236
- const { S3Client } = loadAwsSdk("@aws-sdk/client-s3");
18237
- loadAwsSdk("@aws-sdk/s3-request-presigner");
18238
- const endpoint = opts.endpoint || process.env.TINA4_STORAGE_URL;
18239
18601
  this.client = new S3Client({
18240
- endpoint: endpoint || void 0,
18241
- forcePathStyle: true,
18602
+ endpoint: opts.endpoint || process.env.TINA4_STORAGE_URL || void 0,
18242
18603
  region: opts.region || process.env.TINA4_STORAGE_REGION || "us-east-1",
18243
- credentials: {
18244
- accessKeyId: opts.key || process.env.TINA4_STORAGE_KEY || "",
18245
- secretAccessKey: opts.secret || process.env.TINA4_STORAGE_SECRET || ""
18246
- }
18604
+ accessKey: opts.key || process.env.TINA4_STORAGE_KEY || "",
18605
+ secretKey: opts.secret || process.env.TINA4_STORAGE_SECRET || ""
18247
18606
  });
18248
18607
  }
18249
18608
  async put(key, data, mime = "application/octet-stream") {
18250
- const { PutObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18251
- await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data, ContentType: mime }));
18609
+ await this.client.putObject(this.bucket, key, data, mime);
18252
18610
  }
18253
18611
  async get(key) {
18254
18612
  try {
18255
- const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18256
- const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
18257
- const chunks = [];
18258
- for await (const chunk of res.Body) chunks.push(Buffer.from(chunk));
18259
- return Buffer.concat(chunks);
18613
+ return await this.client.getObject(this.bucket, key);
18260
18614
  } catch {
18261
18615
  return null;
18262
18616
  }
18263
18617
  }
18264
18618
  async url(key, ttl = 3600) {
18265
- const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18266
- const { getSignedUrl } = loadAwsSdk("@aws-sdk/s3-request-presigner");
18267
- return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn: ttl });
18619
+ return this.client.presignedGetUrl(this.bucket, key, ttl);
18268
18620
  }
18269
18621
  async delete(key) {
18270
18622
  try {
18271
- const { DeleteObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18272
- await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
18623
+ await this.client.deleteObject(this.bucket, key);
18273
18624
  } catch (e) {
18274
18625
  Log.error(`S3Storage delete failed for ${key}: ${e.message}`);
18275
18626
  }
18276
18627
  }
18277
18628
  async exists(key) {
18278
18629
  try {
18279
- const { HeadObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18280
- await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
18630
+ await this.client.headObject(this.bucket, key);
18281
18631
  return true;
18282
18632
  } catch {
18283
18633
  return false;
@@ -18288,7 +18638,7 @@ var init_storage = __esm({
18288
18638
  });
18289
18639
 
18290
18640
  // ../orm/src/realtime/realtime.ts
18291
- import { createHmac as createHmac3 } from "node:crypto";
18641
+ import { createHmac as createHmac4 } from "node:crypto";
18292
18642
  function iceServers() {
18293
18643
  const stun = process.env.TINA4_RTC_STUN_URLS || DEFAULT_STUN;
18294
18644
  const servers = [{ urls: splitUrls(stun) }];
@@ -18297,14 +18647,14 @@ function iceServers() {
18297
18647
  if (turnUrl && secret) {
18298
18648
  const ttl = parseInt(process.env.TINA4_RTC_TURN_TTL || "3600", 10);
18299
18649
  const username = String(Math.floor(Date.now() / 1e3) + ttl);
18300
- const credential = createHmac3("sha1", secret).update(username).digest("base64");
18650
+ const credential = createHmac4("sha1", secret).update(username).digest("base64");
18301
18651
  servers.push({ urls: splitUrls(turnUrl), username, credential });
18302
18652
  }
18303
18653
  return servers;
18304
18654
  }
18305
18655
  async function realtime(options = {}) {
18306
18656
  const features = options.features ?? ["calls"];
18307
- const raw = (options.prefix ?? "").replace(/^\/+|\/+$/g, "");
18657
+ const raw = trimSlashes(options.prefix ?? "");
18308
18658
  const p = raw ? `/${raw}` : "";
18309
18659
  _authorize = options.authorize;
18310
18660
  if (features.includes("chat") || features.includes("files")) {
@@ -18576,6 +18926,7 @@ var DEFAULT_STUN, _authorize;
18576
18926
  var init_realtime = __esm({
18577
18927
  "../orm/src/realtime/realtime.ts"() {
18578
18928
  "use strict";
18929
+ init_slashTrim();
18579
18930
  init_index();
18580
18931
  init_workspace();
18581
18932
  init_channel();
@@ -18593,6 +18944,7 @@ var init_realtime2 = __esm({
18593
18944
  "use strict";
18594
18945
  init_realtime();
18595
18946
  init_storage();
18947
+ init_s3Client();
18596
18948
  init_workspace();
18597
18949
  init_channel();
18598
18950
  init_channelMember();
@@ -18958,6 +19310,7 @@ function mayStore(req2, vary, cacheControl, setCookie) {
18958
19310
  if (requestHeader(req2, "authorization") !== void 0) return sharedCacheAllowed(cacheControl);
18959
19311
  if (requestHeader(req2, "cookie") !== void 0) return sharedCacheAllowed(cacheControl);
18960
19312
  if (hasSetCookie(setCookie)) return sharedCacheAllowed(cacheControl);
19313
+ if (req2.session?.isFresh && !req2.session.isFresh()) return sharedCacheAllowed(cacheControl);
18961
19314
  return true;
18962
19315
  }
18963
19316
  function varyMatches(entry, req2) {
@@ -20310,7 +20663,7 @@ function coerceParam(raw, type2) {
20310
20663
  }
20311
20664
  function joinGroupPath(prefix, path10) {
20312
20665
  const full = `${prefix}/${path10.replace(/^\/+/, "")}`;
20313
- const trimmed = `/${full.replace(/^\/+|\/+$/g, "")}`;
20666
+ const trimmed = `/${trimSlashes(full)}`;
20314
20667
  return trimmed.replace(/\/+/g, "/");
20315
20668
  }
20316
20669
  async function resolveStringMiddleware(spec) {
@@ -20375,6 +20728,7 @@ var WsRouteRef, RouteRef, Router, RouteGroup, TINA4_DEFAULT_ROUTER, globalRouter
20375
20728
  var init_router = __esm({
20376
20729
  "src/router.ts"() {
20377
20730
  "use strict";
20731
+ init_slashTrim();
20378
20732
  init_dotenv();
20379
20733
  init_middleware();
20380
20734
  WsRouteRef = class {
@@ -20584,7 +20938,7 @@ var init_router = __esm({
20584
20938
  const direct = this.matchRoute(routes, path10);
20585
20939
  if (direct) return direct;
20586
20940
  if (isTrailingSlashRedirectEnabled() && path10.length > 1 && path10.endsWith("/")) {
20587
- const stripped = path10.replace(/\/+$/, "");
20941
+ const stripped = rtrimSlashes(path10);
20588
20942
  if (stripped.length > 0) {
20589
20943
  const retry = this.matchRoute(routes, stripped);
20590
20944
  if (retry) return retry;
@@ -22260,7 +22614,8 @@ var init_middleware = __esm({
22260
22614
  "Content-Security-Policy": resolveCspHeader(currentCspNonce()),
22261
22615
  "Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
22262
22616
  "X-XSS-Protection": "0",
22263
- "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
22617
+ "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()",
22618
+ "Cross-Origin-Opener-Policy": process.env.TINA4_COOP ?? "same-origin-allow-popups"
22264
22619
  };
22265
22620
  }
22266
22621
  static beforeSecurity(req2, res) {
@@ -22277,7 +22632,7 @@ var init_middleware = __esm({
22277
22632
  if (process.env.TINA4_CSP === void 0) {
22278
22633
  _SecurityHeadersMiddleware.warnCspDefaultOnce();
22279
22634
  }
22280
- for (const name of ["Content-Security-Policy", "Referrer-Policy", "X-XSS-Protection", "Permissions-Policy"]) {
22635
+ for (const name of ["Content-Security-Policy", "Referrer-Policy", "X-XSS-Protection", "Permissions-Policy", "Cross-Origin-Opener-Policy"]) {
22281
22636
  res.header(name, canonical[name]);
22282
22637
  }
22283
22638
  return [req2, res];
@@ -22361,7 +22716,7 @@ var init_middleware = __esm({
22361
22716
  });
22362
22717
 
22363
22718
  // src/request.ts
22364
- import { mkdirSync as mkdirSync7, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
22719
+ import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, ftruncateSync, lstatSync, mkdirSync as mkdirSync7, openSync as openSync3, realpathSync as realpathSync2, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
22365
22720
  import { dirname as dirname6, join as join11 } from "node:path";
22366
22721
  function makeCaseInsensitiveHeaders(raw) {
22367
22722
  return new Proxy(raw, {
@@ -22397,7 +22752,8 @@ function createRequest(req2) {
22397
22752
  try {
22398
22753
  const url = new URL(req2.url ?? "/", `${proto}://${host}`);
22399
22754
  for (const [key, value] of url.searchParams) {
22400
- query[key] = value;
22755
+ if (isUnsafeFieldName(key)) continue;
22756
+ Reflect.set(query, key, value);
22401
22757
  }
22402
22758
  path10 = url.pathname;
22403
22759
  queryString = url.search.replace(/^\?/, "");
@@ -22409,7 +22765,8 @@ function createRequest(req2) {
22409
22765
  queryString = questionIdx >= 0 ? rawTarget.slice(questionIdx + 1) : "";
22410
22766
  try {
22411
22767
  for (const [key, value] of new URLSearchParams(queryString)) {
22412
- query[key] = value;
22768
+ if (isUnsafeFieldName(key)) continue;
22769
+ Reflect.set(query, key, value);
22413
22770
  }
22414
22771
  } catch {
22415
22772
  }
@@ -22428,7 +22785,8 @@ function createRequest(req2) {
22428
22785
  if (cookieHeader) {
22429
22786
  for (const pair of cookieHeader.split(";")) {
22430
22787
  const [k, ...v] = pair.trim().split("=");
22431
- if (k) cookies[k.trim()] = v.join("=").trim();
22788
+ const name = k ? k.trim() : "";
22789
+ if (name && !isUnsafeFieldName(name)) Reflect.set(cookies, name, v.join("=").trim());
22432
22790
  }
22433
22791
  }
22434
22792
  w.cookies = cookies;
@@ -22519,13 +22877,7 @@ async function parseBody(req2) {
22519
22877
  req2.body = str;
22520
22878
  }
22521
22879
  } else if (contentType.includes("application/x-www-form-urlencoded")) {
22522
- const str = raw.toString("utf-8");
22523
- const params = new URLSearchParams(str);
22524
- const obj = {};
22525
- for (const [key, value] of params) {
22526
- obj[key] = value;
22527
- }
22528
- req2.body = obj;
22880
+ req2.body = nestFormFields([...new URLSearchParams(raw.toString("utf-8"))]);
22529
22881
  } else {
22530
22882
  req2.body = raw.toString("utf-8");
22531
22883
  }
@@ -22535,7 +22887,7 @@ function extractBoundary(contentType) {
22535
22887
  return match ? match[1] ?? match[2] : null;
22536
22888
  }
22537
22889
  function parseMultipart(body, boundary) {
22538
- const fields = {};
22890
+ const pairs = [];
22539
22891
  const files = {};
22540
22892
  const delimiter2 = Buffer.from(`--${boundary}`);
22541
22893
  const closeDelimiter = Buffer.from(`--${boundary}--`);
@@ -22543,7 +22895,7 @@ function parseMultipart(body, boundary) {
22543
22895
  const doubleCrlf = Buffer.from("\r\n\r\n");
22544
22896
  let offset = 0;
22545
22897
  const firstIdx = bufferIndexOf(body, delimiter2, offset);
22546
- if (firstIdx === -1) return { fields, files };
22898
+ if (firstIdx === -1) return { fields: {}, files };
22547
22899
  offset = firstIdx + delimiter2.length;
22548
22900
  if (body[offset] === 13 && body[offset + 1] === 10) {
22549
22901
  offset += 2;
@@ -22567,14 +22919,15 @@ function parseMultipart(body, boundary) {
22567
22919
  content: Buffer.from(content),
22568
22920
  size: content.length
22569
22921
  };
22570
- if (files[disposition.name]) {
22922
+ if (isUnsafeFieldName(disposition.name)) {
22923
+ } else if (files[disposition.name]) {
22571
22924
  const existing = files[disposition.name];
22572
- files[disposition.name] = Array.isArray(existing) ? [...existing, file] : [existing, file];
22925
+ Reflect.set(files, disposition.name, Array.isArray(existing) ? [...existing, file] : [existing, file]);
22573
22926
  } else {
22574
- files[disposition.name] = file;
22927
+ Reflect.set(files, disposition.name, file);
22575
22928
  }
22576
22929
  } else if (disposition.name) {
22577
- fields[disposition.name] = content.toString("utf-8");
22930
+ pairs.push([disposition.name, content.toString("utf-8")]);
22578
22931
  }
22579
22932
  offset = nextDelimIdx + delimiter2.length;
22580
22933
  if (body[offset] === 45 && body[offset + 1] === 45) {
@@ -22584,7 +22937,7 @@ function parseMultipart(body, boundary) {
22584
22937
  offset += 2;
22585
22938
  }
22586
22939
  }
22587
- return { fields, files };
22940
+ return { fields: nestFormFields(pairs), files };
22588
22941
  }
22589
22942
  function saveUpload(file, targetDir, filename) {
22590
22943
  const raw = String(filename ?? file.filename ?? "");
@@ -22600,7 +22953,21 @@ function saveUpload(file, targetDir, filename) {
22600
22953
  if (realParent !== realDir) {
22601
22954
  throw new Error(`refusing to write outside ${targetDir}: ${JSON.stringify(raw)}`);
22602
22955
  }
22603
- writeFileSync4(dest, file.content);
22956
+ if (lstatSync(dest, { throwIfNoEntry: false })?.isSymbolicLink()) unlinkSync4(dest);
22957
+ const flags = constants2.O_WRONLY | constants2.O_CREAT | (constants2.O_NOFOLLOW ?? 0);
22958
+ let fd = openSync3(dest, flags, 384);
22959
+ try {
22960
+ if (fstatSync2(fd).nlink > 1) {
22961
+ closeSync3(fd);
22962
+ fd = -1;
22963
+ unlinkSync4(dest);
22964
+ fd = openSync3(dest, flags | constants2.O_EXCL, 384);
22965
+ }
22966
+ ftruncateSync(fd, 0);
22967
+ writeFileSync4(fd, file.content);
22968
+ } finally {
22969
+ if (fd >= 0) closeSync3(fd);
22970
+ }
22604
22971
  return dest;
22605
22972
  }
22606
22973
  function bufferIndexOf(haystack, needle, offset) {
@@ -22625,10 +22992,98 @@ function parseDisposition(headers) {
22625
22992
  };
22626
22993
  }
22627
22994
  function parsePartContentType(headers) {
22628
- const match = headers.match(/Content-Type:\s*(.+?)(?:\r?\n|$)/i);
22629
- return match?.[1]?.trim() ?? null;
22995
+ const match = headers.match(/Content-Type:([^\r\n]*)/i);
22996
+ const value = match?.[1]?.trim();
22997
+ return value ? value : null;
22998
+ }
22999
+ function nestFormFields(pairs) {
23000
+ const kept = pairs.filter(([name]) => !/(^|[[\]])__proto__([[\]]|$)/.test(name));
23001
+ if (kept.length > FORM_FIELD_LIMIT) return Object.fromEntries(kept);
23002
+ const nested = /* @__PURE__ */ new WeakSet();
23003
+ const node = () => {
23004
+ const created = /* @__PURE__ */ Object.create(null);
23005
+ nested.add(created);
23006
+ return created;
23007
+ };
23008
+ const isNode = (value) => typeof value === "object" && value !== null && nested.has(value);
23009
+ const own = (params, key) => Object.prototype.hasOwnProperty.call(params, key) ? params[key] : void 0;
23010
+ const list = (params, key) => {
23011
+ let current = own(params, key);
23012
+ if (current === void 0) {
23013
+ current = [];
23014
+ Reflect.set(params, key, current);
23015
+ }
23016
+ if (!Array.isArray(current)) throw new RangeError(`expected an array for '${key}'`);
23017
+ return current;
23018
+ };
23019
+ const object = (params, key) => {
23020
+ let current = own(params, key);
23021
+ if (current === void 0) {
23022
+ current = node();
23023
+ Reflect.set(params, key, current);
23024
+ }
23025
+ if (!isNode(current)) throw new RangeError(`expected an object for '${key}'`);
23026
+ return current;
23027
+ };
23028
+ const hasKey = (params, key) => {
23029
+ if (key.includes("[]")) return false;
23030
+ let level = params;
23031
+ for (const part of key.split(/[[\]]+/).filter(Boolean)) {
23032
+ if (!isNode(level) || own(level, part) === void 0) return false;
23033
+ level = level[part];
23034
+ }
23035
+ return true;
23036
+ };
23037
+ const splitName = (name, depth) => {
23038
+ if (depth === 0) {
23039
+ const start2 = name.indexOf("[", 1);
23040
+ return start2 === -1 ? [name, ""] : [name.slice(0, start2), name.slice(start2)];
23041
+ }
23042
+ if (name.startsWith("[]")) return ["[]", name.slice(2)];
23043
+ const close = name.indexOf("]", 1);
23044
+ return name.startsWith("[") && close !== -1 ? [name.slice(1, close), name.slice(close + 1)] : [name, ""];
23045
+ };
23046
+ const putListItem = (items, after, value, depth) => {
23047
+ let childKey = after[2] === "[" && after.endsWith("]") ? after.slice(3, -1) : "";
23048
+ if (childKey === "" || childKey.includes("[") || childKey.includes("]")) childKey = after.slice(2);
23049
+ const last = items[items.length - 1];
23050
+ if (isNode(last) && !hasKey(last, childKey)) put2(last, childKey, value, depth + 1);
23051
+ else items.push(put2(node(), childKey, value, depth + 1));
23052
+ };
23053
+ const put2 = (params, name, value, depth) => {
23054
+ const [key, after] = splitName(name, depth);
23055
+ if (key === "") return void 0;
23056
+ if (after === "" && key === "[]" && depth !== 0) return [value];
23057
+ if (after === "") Reflect.set(params, key, value);
23058
+ else if (after === "[") Reflect.set(params, name, value);
23059
+ else if (after === "[]") list(params, key).push(value);
23060
+ else if (after.startsWith("[]")) putListItem(list(params, key), after, value, depth);
23061
+ else put2(object(params, key), after, value, depth + 1);
23062
+ return params;
23063
+ };
23064
+ const tooDeep = (name) => {
23065
+ let count = 0;
23066
+ for (let at = name.indexOf("["); at !== -1; at = name.indexOf("[", at + 1)) {
23067
+ if (++count >= FORM_DEPTH_LIMIT) return true;
23068
+ }
23069
+ return false;
23070
+ };
23071
+ const fields = node();
23072
+ for (const [name, value] of kept) {
23073
+ if (name.length > FORM_NAME_LIMIT || tooDeep(name)) {
23074
+ Reflect.set(fields, name, value);
23075
+ continue;
23076
+ }
23077
+ try {
23078
+ put2(fields, name, value, 0);
23079
+ } catch (error2) {
23080
+ if (!(error2 instanceof RangeError)) throw error2;
23081
+ Reflect.set(fields, name, value);
23082
+ }
23083
+ }
23084
+ return fields;
22630
23085
  }
22631
- var DEFAULT_MAX_UPLOAD_SIZE, warnedLimits, PayloadTooLargeError;
23086
+ var DEFAULT_MAX_UPLOAD_SIZE, warnedLimits, PayloadTooLargeError, FORM_DEPTH_LIMIT, FORM_FIELD_LIMIT, FORM_NAME_LIMIT, isUnsafeFieldName;
22632
23087
  var init_request = __esm({
22633
23088
  "src/request.ts"() {
22634
23089
  "use strict";
@@ -22643,6 +23098,10 @@ var init_request = __esm({
22643
23098
  this.name = "PayloadTooLargeError";
22644
23099
  }
22645
23100
  };
23101
+ FORM_DEPTH_LIMIT = 32;
23102
+ FORM_FIELD_LIMIT = 4096;
23103
+ FORM_NAME_LIMIT = 1024;
23104
+ isUnsafeFieldName = (key) => key === "__proto__" || key === "constructor" || key === "prototype";
22646
23105
  }
22647
23106
  });
22648
23107
 
@@ -23149,7 +23608,7 @@ var init_routeDiscovery = __esm({
23149
23608
 
23150
23609
  // src/portTakeover.ts
23151
23610
  import { execFileSync } from "node:child_process";
23152
- import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync9, unlinkSync as unlinkSync4 } from "node:fs";
23611
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync9, unlinkSync as unlinkSync5 } from "node:fs";
23153
23612
  import { join as join13 } from "node:path";
23154
23613
  function isTruthy3(value) {
23155
23614
  return ["true", "1", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
@@ -23203,7 +23662,7 @@ function readPidfile(port, baseDir) {
23203
23662
  }
23204
23663
  function removePidfile(port, baseDir) {
23205
23664
  try {
23206
- unlinkSync4(pidfilePath(port, baseDir));
23665
+ unlinkSync5(pidfilePath(port, baseDir));
23207
23666
  } catch {
23208
23667
  }
23209
23668
  }
@@ -23304,21 +23763,6 @@ var init_portTakeover = __esm({
23304
23763
  }
23305
23764
  });
23306
23765
 
23307
- // src/etag.ts
23308
- function etagMatches(ifNoneMatch, etag) {
23309
- const strip = (tag) => tag.trim().replace(/^W\//, "");
23310
- const target = strip(etag);
23311
- return ifNoneMatch.split(",").some((candidate) => {
23312
- const trimmed = candidate.trim();
23313
- return trimmed === "*" || strip(trimmed) === target;
23314
- });
23315
- }
23316
- var init_etag = __esm({
23317
- "src/etag.ts"() {
23318
- "use strict";
23319
- }
23320
- });
23321
-
23322
23766
  // src/sessionHandlers/syncBridge.ts
23323
23767
  import { Worker } from "node:worker_threads";
23324
23768
  function getBridge(key, workerSource, workerData) {
@@ -23678,7 +24122,7 @@ var init_valkeyHandler = __esm({
23678
24122
  });
23679
24123
 
23680
24124
  // src/sessionHandlers/memcachedHandler.ts
23681
- import { createHash as createHash5 } from "node:crypto";
24125
+ import { createHash as createHash6 } from "node:crypto";
23682
24126
  var MAX_KEY_BYTES, MAX_RELATIVE_EXPTIME, MemcachedSessionHandler;
23683
24127
  var init_memcachedHandler = __esm({
23684
24128
  "src/sessionHandlers/memcachedHandler.ts"() {
@@ -23700,7 +24144,7 @@ var init_memcachedHandler = __esm({
23700
24144
  key(sessionId) {
23701
24145
  const candidate = `${this.prefix}${sessionId}`;
23702
24146
  if (Buffer.byteLength(candidate) > MAX_KEY_BYTES || /[\x00-\x20\x7f]/.test(candidate)) {
23703
- return `${this.prefix}${createHash5("sha256").update(sessionId).digest("hex")}`;
24147
+ return `${this.prefix}${createHash6("sha256").update(sessionId).digest("hex")}`;
23704
24148
  }
23705
24149
  return candidate;
23706
24150
  }
@@ -24166,7 +24610,7 @@ var init_mongoHandler = __esm({
24166
24610
  });
24167
24611
 
24168
24612
  // src/sessionHandlers/sqlClient.ts
24169
- import { createRequire as createRequire7 } from "node:module";
24613
+ import { createRequire as createRequire6 } from "node:module";
24170
24614
  function driverPath(engine) {
24171
24615
  const packageName = DRIVER_PACKAGE4[engine];
24172
24616
  try {
@@ -24205,7 +24649,7 @@ var init_sqlClient = __esm({
24205
24649
  mssql: "tedious",
24206
24650
  firebird: "node-firebird"
24207
24651
  };
24208
- requireFromHere = createRequire7(import.meta.url);
24652
+ requireFromHere = createRequire6(import.meta.url);
24209
24653
  SQL_WORKER = `
24210
24654
  const target = workerData.target;
24211
24655
  const engine = target.engine;
@@ -24687,14 +25131,18 @@ __export(session_exports, {
24687
25131
  Session: () => Session,
24688
25132
  VALID_SESSION_BACKENDS: () => VALID_SESSION_BACKENDS,
24689
25133
  buildSessionCookie: () => buildSessionCookie,
25134
+ currentFormTokenSessionId: () => currentFormTokenSessionId,
24690
25135
  isSecureScheme: () => isSecureScheme,
24691
25136
  isValidSessionId: () => isValidSessionId,
25137
+ runWithFormTokenSession: () => runWithFormTokenSession,
24692
25138
  sessionCookieName: () => sessionCookieName,
24693
25139
  sessionCookieToSend: () => sessionCookieToSend,
24694
- sessionStrictMode: () => sessionStrictMode
25140
+ sessionStrictMode: () => sessionStrictMode,
25141
+ setFormTokenSession: () => setFormTokenSession
24695
25142
  });
24696
- import { createHash as createHash6, randomBytes as randomBytes6 } from "node:crypto";
24697
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync5, readdirSync as readdirSync6 } from "node:fs";
25143
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "node:async_hooks";
25144
+ import { createHash as createHash7, randomBytes as randomBytes6 } from "node:crypto";
25145
+ import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync6, readdirSync as readdirSync6 } from "node:fs";
24698
25146
  import { join as join14 } from "node:path";
24699
25147
  function isValidSessionId(sessionId) {
24700
25148
  return typeof sessionId === "string" && SESSION_ID_PATTERN.test(sessionId);
@@ -24760,7 +25208,21 @@ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
24760
25208
  const socketEncrypted = rawReq.socket?.encrypted === true;
24761
25209
  return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
24762
25210
  }
24763
- var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
25211
+ function runWithFormTokenSession(fn) {
25212
+ return sessionStore.run({ session: null }, fn);
25213
+ }
25214
+ function setFormTokenSession(session) {
25215
+ const store = sessionStore.getStore();
25216
+ if (store) store.session = session;
25217
+ }
25218
+ function currentFormTokenSessionId() {
25219
+ const session = sessionStore.getStore()?.session;
25220
+ const sid = session?.getSessionId();
25221
+ if (!session || !sid) return "";
25222
+ if (session.isFresh()) session.set("_tina4_csrf", true);
25223
+ return sid;
25224
+ }
25225
+ var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session, sessionStore;
24764
25226
  var init_session = __esm({
24765
25227
  "src/session.ts"() {
24766
25228
  "use strict";
@@ -24834,7 +25296,7 @@ var init_session = __esm({
24834
25296
  `Invalid session id ${JSON.stringify(id)} \u2014 a session id is opaque and must match ${SESSION_ID_PATTERN.source}. It is never a path, so it is refused rather than resolved to a file.`
24835
25297
  );
24836
25298
  }
24837
- return join14(this.storagePath, `${createHash6("sha256").update(id).digest("hex")}.json`);
25299
+ return join14(this.storagePath, `${createHash7("sha256").update(id).digest("hex")}.json`);
24838
25300
  }
24839
25301
  read(sessionId) {
24840
25302
  const filePath = this.filePath(sessionId);
@@ -24844,7 +25306,7 @@ var init_session = __esm({
24844
25306
  const wrapper = JSON.parse(raw);
24845
25307
  if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
24846
25308
  try {
24847
- unlinkSync5(filePath);
25309
+ unlinkSync6(filePath);
24848
25310
  } catch {
24849
25311
  }
24850
25312
  return null;
@@ -24863,7 +25325,7 @@ var init_session = __esm({
24863
25325
  destroy(sessionId) {
24864
25326
  const filePath = this.filePath(sessionId);
24865
25327
  try {
24866
- if (existsSync11(filePath)) unlinkSync5(filePath);
25328
+ if (existsSync11(filePath)) unlinkSync6(filePath);
24867
25329
  } catch {
24868
25330
  }
24869
25331
  }
@@ -24879,11 +25341,11 @@ var init_session = __esm({
24879
25341
  const raw = readFileSync8(fullPath, "utf-8");
24880
25342
  const wrapper = JSON.parse(raw);
24881
25343
  if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
24882
- unlinkSync5(fullPath);
25344
+ unlinkSync6(fullPath);
24883
25345
  }
24884
25346
  } catch {
24885
25347
  try {
24886
- unlinkSync5(fullPath);
25348
+ unlinkSync6(fullPath);
24887
25349
  } catch {
24888
25350
  }
24889
25351
  }
@@ -25403,12 +25865,28 @@ var init_session = __esm({
25403
25865
  this.loaded = /* @__PURE__ */ new Map();
25404
25866
  }
25405
25867
  };
25868
+ sessionStore = new AsyncLocalStorage4();
25869
+ }
25870
+ });
25871
+
25872
+ // src/etag.ts
25873
+ function etagMatches(ifNoneMatch, etag) {
25874
+ const strip = (tag) => tag.trim().replace(/^W\//, "");
25875
+ const target = strip(etag);
25876
+ return ifNoneMatch.split(",").some((candidate) => {
25877
+ const trimmed = candidate.trim();
25878
+ return trimmed === "*" || strip(trimmed) === target;
25879
+ });
25880
+ }
25881
+ var init_etag = __esm({
25882
+ "src/etag.ts"() {
25883
+ "use strict";
25406
25884
  }
25407
25885
  });
25408
25886
 
25409
25887
  // src/dispatchPipeline.ts
25410
25888
  import { gzipSync } from "node:zlib";
25411
- import { createHash as createHash7 } from "node:crypto";
25889
+ import { createHash as createHash8 } from "node:crypto";
25412
25890
  async function resetRequestCaches2() {
25413
25891
  if (_resetRequestCaches === void 0) {
25414
25892
  _resetRequestCaches = Promise.resolve().then(() => (init_src(), src_exports)).then((orm) => orm.resetRequestCaches).catch(() => null);
@@ -25481,7 +25959,7 @@ function applyResponseValidator(rawReq, rawRes, body) {
25481
25959
  if ((rawRes.statusCode || 200) !== 200 || body.length === 0) return false;
25482
25960
  let etag = rawRes.getHeader("etag");
25483
25961
  if (!etag) {
25484
- etag = `"${createHash7("md5").update(body).digest("hex").slice(0, 16)}"`;
25962
+ etag = `"${createHash8("md5").update(body).digest("hex").slice(0, 16)}"`;
25485
25963
  rawRes.setHeader("ETag", etag);
25486
25964
  }
25487
25965
  if (!responseIsNotModified(rawReq, rawRes, String(etag))) return false;
@@ -25556,6 +26034,7 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
25556
26034
  return;
25557
26035
  }
25558
26036
  req2.session = sess;
26037
+ setFormTokenSession(sess);
25559
26038
  const origEnd = rawRes.end.bind(rawRes);
25560
26039
  let finalised = false;
25561
26040
  rawRes.end = function(...args) {
@@ -25584,6 +26063,7 @@ var _resetRequestCaches, COMPRESSIBLE_PREFIXES;
25584
26063
  var init_dispatchPipeline = __esm({
25585
26064
  "src/dispatchPipeline.ts"() {
25586
26065
  "use strict";
26066
+ init_session();
25587
26067
  init_logger();
25588
26068
  init_etag();
25589
26069
  COMPRESSIBLE_PREFIXES = [
@@ -25733,14 +26213,21 @@ function parseStack(stack) {
25733
26213
  const frames = [];
25734
26214
  const lines = stack.split("\n");
25735
26215
  for (const line of lines) {
25736
- let match = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
25737
- if (match) {
25738
- frames.push({ func: match[1], file: match[2], line: parseInt(match[3], 10), column: parseInt(match[4], 10) });
25739
- continue;
26216
+ const t = line.trim();
26217
+ if (!t.startsWith("at ")) continue;
26218
+ const rest = t.slice(3);
26219
+ let func = "{anonymous}";
26220
+ let loc = rest;
26221
+ if (rest.endsWith(")")) {
26222
+ const open = rest.lastIndexOf(" (");
26223
+ if (open >= 0) {
26224
+ func = rest.slice(0, open);
26225
+ loc = rest.slice(open + 2, -1);
26226
+ }
25740
26227
  }
25741
- match = line.match(/^\s*at\s+(.+?):(\d+):(\d+)/);
25742
- if (match) {
25743
- frames.push({ func: "{anonymous}", file: match[1], line: parseInt(match[2], 10), column: parseInt(match[3], 10) });
26228
+ const lc = loc.match(/:(\d+):(\d+)$/);
26229
+ if (lc && lc.index !== void 0) {
26230
+ frames.push({ func, file: loc.slice(0, lc.index), line: parseInt(lc[1], 10), column: parseInt(lc[2], 10) });
25744
26231
  }
25745
26232
  }
25746
26233
  return frames;
@@ -26065,7 +26552,7 @@ var init_rateLimiter = __esm({
26065
26552
  });
26066
26553
 
26067
26554
  // src/devMailbox.ts
26068
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, existsSync as existsSync12 } from "node:fs";
26555
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync7, existsSync as existsSync12 } from "node:fs";
26069
26556
  import { join as join16 } from "node:path";
26070
26557
  import { randomUUID as randomUUID2 } from "node:crypto";
26071
26558
  var DevMailbox;
@@ -26197,7 +26684,7 @@ var init_devMailbox = __esm({
26197
26684
  const filePath = join16(this.mailboxDir, folder, `${msgId}.json`);
26198
26685
  if (existsSync12(filePath)) {
26199
26686
  try {
26200
- unlinkSync6(filePath);
26687
+ unlinkSync7(filePath);
26201
26688
  deleted = true;
26202
26689
  } catch {
26203
26690
  }
@@ -26216,7 +26703,7 @@ var init_devMailbox = __esm({
26216
26703
  if (existsSync12(dir)) {
26217
26704
  const files = readdirSync7(dir).filter((file) => file.endsWith(".json"));
26218
26705
  for (const file of files) {
26219
- unlinkSync6(join16(dir, file));
26706
+ unlinkSync7(join16(dir, file));
26220
26707
  }
26221
26708
  }
26222
26709
  } catch {
@@ -26360,7 +26847,7 @@ function enginePath() {
26360
26847
  function runEngine(target) {
26361
26848
  const binary = enginePath();
26362
26849
  if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
26363
- const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
26850
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json", "--no-history"], {
26364
26851
  encoding: "utf8",
26365
26852
  timeout: 6e4,
26366
26853
  maxBuffer: 64 * 1024 * 1024
@@ -28862,7 +29349,7 @@ import * as fs4 from "node:fs";
28862
29349
  import * as path3 from "node:path";
28863
29350
  import * as os from "node:os";
28864
29351
  import { spawnSync as spawnSync2 } from "node:child_process";
28865
- import { createRequire as createRequire8 } from "node:module";
29352
+ import { createRequire as createRequire7 } from "node:module";
28866
29353
  import { randomBytes as randomBytes7 } from "node:crypto";
28867
29354
  function reqSibling(pkg) {
28868
29355
  return req(`../../${pkg}/src/index.ts`);
@@ -29021,21 +29508,45 @@ function safePath(projectRoot3, relPath) {
29021
29508
  if (resolved !== projectRoot3 && !resolved.startsWith(rootPrefix)) {
29022
29509
  throw new Error(`Path escapes project directory: ${relPath}`);
29023
29510
  }
29024
- let real = null;
29511
+ let realRoot;
29025
29512
  try {
29026
- real = fs4.realpathSync(resolved);
29513
+ realRoot = fs4.realpathSync(projectRoot3);
29027
29514
  } catch {
29028
- real = null;
29515
+ return resolved;
29029
29516
  }
29030
- if (real !== null) {
29031
- const realRoot = fs4.realpathSync(projectRoot3);
29032
- const realPrefix = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
29033
- if (real !== realRoot && !real.startsWith(realPrefix)) {
29034
- throw new Error(`Path escapes project directory: ${relPath}`);
29035
- }
29517
+ const real = realPathOf(resolved);
29518
+ const realPrefix = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
29519
+ if (real === null || real !== realRoot && !real.startsWith(realPrefix)) {
29520
+ throw new Error(`Path escapes project directory: ${relPath}`);
29036
29521
  }
29037
29522
  return resolved;
29038
29523
  }
29524
+ function realPathOf(p, hops = 0) {
29525
+ let probe = p;
29526
+ while (!fs4.existsSync(probe) && !isSymlink(probe) && path3.dirname(probe) !== probe) {
29527
+ probe = path3.dirname(probe);
29528
+ }
29529
+ try {
29530
+ return fs4.realpathSync.native(probe);
29531
+ } catch {
29532
+ if (hops >= 40) return null;
29533
+ let target;
29534
+ try {
29535
+ target = fs4.readlinkSync(probe);
29536
+ } catch {
29537
+ return null;
29538
+ }
29539
+ const base = path3.isAbsolute(target) ? target : path3.dirname(probe) + path3.sep + target;
29540
+ return realPathOf(base + p.slice(probe.length), hops + 1);
29541
+ }
29542
+ }
29543
+ function isSymlink(p) {
29544
+ try {
29545
+ return fs4.lstatSync(p).isSymbolicLink();
29546
+ } catch {
29547
+ return false;
29548
+ }
29549
+ }
29039
29550
  function redactEnv(key, value) {
29040
29551
  const sensitive = ["secret", "password", "token", "key", "credential", "api_key"];
29041
29552
  if (sensitive.some((s) => key.toLowerCase().includes(s))) {
@@ -29880,8 +30391,10 @@ function registerDevTools(server) {
29880
30391
  return { error: `Invalid path ${JSON.stringify(rel)}: ${proseErr}` };
29881
30392
  }
29882
30393
  rel = normalizeCoderPath(projectRoot4, rel);
29883
- const resolved = path3.resolve(projectRoot4, rel);
29884
- if (!resolved.startsWith(projectRoot4)) {
30394
+ let resolved;
30395
+ try {
30396
+ resolved = safePath(projectRoot4, rel);
30397
+ } catch {
29885
30398
  return { error: `Path escapes project directory: ${rel}` };
29886
30399
  }
29887
30400
  if (!fs4.existsSync(resolved) || !fs4.statSync(resolved).isFile()) {
@@ -30178,8 +30691,9 @@ var req, PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNA
30178
30691
  var init_mcp = __esm({
30179
30692
  "src/mcp.ts"() {
30180
30693
  "use strict";
30694
+ init_slashTrim();
30181
30695
  init_version();
30182
- req = createRequire8(import.meta.url);
30696
+ req = createRequire7(import.meta.url);
30183
30697
  PARSE_ERROR = -32700;
30184
30698
  INVALID_REQUEST = -32600;
30185
30699
  METHOD_NOT_FOUND = -32601;
@@ -30204,7 +30718,7 @@ var init_mcp = __esm({
30204
30718
  // threaded, so this in-process map is the whole coordination mechanism.
30205
30719
  _sseChannels = /* @__PURE__ */ new Map();
30206
30720
  constructor(mcpPath, name = "Tina4 MCP", version = "1.0.0") {
30207
- this.path = mcpPath.replace(/\/+$/, "");
30721
+ this.path = rtrimSlashes(mcpPath);
30208
30722
  this.name = name;
30209
30723
  this.version = version;
30210
30724
  _McpServer._instances.push(this);
@@ -32287,7 +32801,7 @@ __export(websocket_exports, {
32287
32801
  wsToken: () => wsToken
32288
32802
  });
32289
32803
  import { createServer as createServer2 } from "node:http";
32290
- import { createHash as createHash8 } from "node:crypto";
32804
+ import { createHash as createHash9 } from "node:crypto";
32291
32805
  import { randomUUID as randomUUID4 } from "node:crypto";
32292
32806
  function buildCloseFrame(code, reason = "") {
32293
32807
  const reasonBytes = Buffer.from(reason, "utf-8");
@@ -32297,7 +32811,7 @@ function buildCloseFrame(code, reason = "") {
32297
32811
  return buildFrame(OP_CLOSE, payload);
32298
32812
  }
32299
32813
  function computeAcceptKey(key) {
32300
- return createHash8("sha1").update(key + MAGIC_STRING).digest("base64");
32814
+ return createHash9("sha1").update(key + MAGIC_STRING).digest("base64");
32301
32815
  }
32302
32816
  function originAllowed(headers) {
32303
32817
  const raw = (process.env.TINA4_WS_ALLOWED_ORIGINS ?? "").trim();
@@ -33988,7 +34502,7 @@ var init_job = __esm({
33988
34502
  });
33989
34503
 
33990
34504
  // src/queueBackends/liteBackend.ts
33991
- import { mkdirSync as mkdirSync15, readdirSync as readdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, existsSync as existsSync19 } from "node:fs";
34505
+ import { mkdirSync as mkdirSync15, readdirSync as readdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync8, existsSync as existsSync19 } from "node:fs";
33992
34506
  import { join as join23 } from "node:path";
33993
34507
  import { randomUUID as randomUUID6 } from "node:crypto";
33994
34508
  var LiteBackend;
@@ -34158,7 +34672,7 @@ var init_liteBackend = __esm({
34158
34672
  }
34159
34673
  if (record.availableAt && record.availableAt > now) continue;
34160
34674
  try {
34161
- unlinkSync7(filePath);
34675
+ unlinkSync8(filePath);
34162
34676
  } catch {
34163
34677
  continue;
34164
34678
  }
@@ -34192,10 +34706,10 @@ var init_liteBackend = __esm({
34192
34706
  job.priority = job.priority ?? 0;
34193
34707
  this.writeReserved(queue, job);
34194
34708
  try {
34195
- unlinkSync7(filePath);
34709
+ unlinkSync8(filePath);
34196
34710
  } catch {
34197
34711
  try {
34198
- unlinkSync7(this.reservedPath(queue, job.id));
34712
+ unlinkSync8(this.reservedPath(queue, job.id));
34199
34713
  } catch {
34200
34714
  }
34201
34715
  continue;
@@ -34217,10 +34731,10 @@ var init_liteBackend = __esm({
34217
34731
  job.priority = job.priority ?? 0;
34218
34732
  this.writeReserved(queue, job);
34219
34733
  try {
34220
- unlinkSync7(filePath);
34734
+ unlinkSync8(filePath);
34221
34735
  } catch {
34222
34736
  try {
34223
- unlinkSync7(this.reservedPath(queue, job.id));
34737
+ unlinkSync8(this.reservedPath(queue, job.id));
34224
34738
  } catch {
34225
34739
  }
34226
34740
  continue;
@@ -34235,7 +34749,7 @@ var init_liteBackend = __esm({
34235
34749
  */
34236
34750
  clearReservation(queue, jobId) {
34237
34751
  try {
34238
- unlinkSync7(this.reservedPath(queue, jobId));
34752
+ unlinkSync8(this.reservedPath(queue, jobId));
34239
34753
  } catch {
34240
34754
  }
34241
34755
  }
@@ -34278,7 +34792,7 @@ var init_liteBackend = __esm({
34278
34792
  try {
34279
34793
  const files = readdirSync12(dir).filter((f) => f.endsWith(".queue-data"));
34280
34794
  for (const file of files) {
34281
- unlinkSync7(join23(dir, file));
34795
+ unlinkSync8(join23(dir, file));
34282
34796
  count++;
34283
34797
  }
34284
34798
  } catch {
@@ -34288,7 +34802,7 @@ var init_liteBackend = __esm({
34288
34802
  if (existsSync19(failedDir)) {
34289
34803
  const files = readdirSync12(failedDir).filter((f) => f.endsWith(".queue-data"));
34290
34804
  for (const file of files) {
34291
- unlinkSync7(join23(failedDir, file));
34805
+ unlinkSync8(join23(failedDir, file));
34292
34806
  count++;
34293
34807
  }
34294
34808
  }
@@ -34299,7 +34813,7 @@ var init_liteBackend = __esm({
34299
34813
  if (existsSync19(reservedDir)) {
34300
34814
  const files = readdirSync12(reservedDir).filter((f) => f.endsWith(".queue-data"));
34301
34815
  for (const file of files) {
34302
- unlinkSync7(join23(reservedDir, file));
34816
+ unlinkSync8(join23(reservedDir, file));
34303
34817
  count++;
34304
34818
  }
34305
34819
  }
@@ -34357,7 +34871,7 @@ var init_liteBackend = __esm({
34357
34871
  const prefix = this.nextPrefix();
34358
34872
  const queueDir = join23(this.basePath, q);
34359
34873
  writeFileSync11(join23(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
34360
- unlinkSync7(filePath);
34874
+ unlinkSync8(filePath);
34361
34875
  return true;
34362
34876
  }
34363
34877
  }
@@ -34393,7 +34907,7 @@ var init_liteBackend = __esm({
34393
34907
  const files = readdirSync12(failedDir).filter((f) => f.endsWith(".queue-data"));
34394
34908
  for (const file of files) {
34395
34909
  try {
34396
- unlinkSync7(join23(failedDir, file));
34910
+ unlinkSync8(join23(failedDir, file));
34397
34911
  count++;
34398
34912
  } catch {
34399
34913
  }
@@ -34408,7 +34922,7 @@ var init_liteBackend = __esm({
34408
34922
  try {
34409
34923
  const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
34410
34924
  if (job.status === status2) {
34411
- unlinkSync7(join23(dir, file));
34925
+ unlinkSync8(join23(dir, file));
34412
34926
  count++;
34413
34927
  }
34414
34928
  } catch {
@@ -34443,7 +34957,7 @@ var init_liteBackend = __esm({
34443
34957
  job.delayUntil = null;
34444
34958
  const prefix = this.nextPrefix();
34445
34959
  writeFileSync11(join23(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
34446
- unlinkSync7(filePath);
34960
+ unlinkSync8(filePath);
34447
34961
  count++;
34448
34962
  } catch {
34449
34963
  }
@@ -34475,10 +34989,10 @@ var init_liteBackend = __esm({
34475
34989
  job.priority = job.priority ?? 0;
34476
34990
  this.writeReserved(queue, job);
34477
34991
  try {
34478
- unlinkSync7(filePath);
34992
+ unlinkSync8(filePath);
34479
34993
  } catch {
34480
34994
  try {
34481
- unlinkSync7(this.reservedPath(queue, job.id));
34995
+ unlinkSync8(this.reservedPath(queue, job.id));
34482
34996
  } catch {
34483
34997
  }
34484
34998
  continue;
@@ -34580,7 +35094,7 @@ var init_liteBackend = __esm({
34580
35094
  retryJob(queue, job, delaySeconds) {
34581
35095
  this.clearReservation(queue, job.id);
34582
35096
  try {
34583
- unlinkSync7(join23(this.ensureFailedDir(queue), `${job.id}.queue-data`));
35097
+ unlinkSync8(join23(this.ensureFailedDir(queue), `${job.id}.queue-data`));
34584
35098
  } catch {
34585
35099
  }
34586
35100
  job.attempts = (job.attempts || 0) + 1;
@@ -35130,7 +35644,8 @@ function archiveDir() {
35130
35644
  return p;
35131
35645
  }
35132
35646
  function slugify(title) {
35133
- const slug = (title || "").trim().toLowerCase().replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
35647
+ const dashed = (title || "").trim().toLowerCase().replace(/[^A-Za-z0-9_-]+/g, "-");
35648
+ const slug = trimChar(dashed, 45);
35134
35649
  return slug.slice(0, 80) || `plan-${Math.floor(Date.now() / 1e3)}`;
35135
35650
  }
35136
35651
  function parse(text) {
@@ -35267,6 +35782,7 @@ var PLAN_DIR, CURRENT_FILE, ARCHIVE_SUBDIR, STEP_RE, Plan;
35267
35782
  var init_plan = __esm({
35268
35783
  "src/plan.ts"() {
35269
35784
  "use strict";
35785
+ init_slashTrim();
35270
35786
  PLAN_DIR = "plan";
35271
35787
  CURRENT_FILE = ".current";
35272
35788
  ARCHIVE_SUBDIR = "done";
@@ -35538,7 +36054,7 @@ var init_plan = __esm({
35538
36054
  const existing = (currentPlan.steps || []).map((s) => s.text || "");
35539
36055
  const title = currentPlan.title || target;
35540
36056
  const goal = currentPlan.goal || "";
35541
- const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11437/api/chat";
36057
+ const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11435/api/chat";
35542
36058
  const aiModel = process.env.TINA4_AI_MODEL || "qwen2.5-coder:14b";
35543
36059
  const prompts = fleshPrompt(title, goal, existing, prompt);
35544
36060
  const fetched = await fetchFleshReply(aiUrl, aiModel, prompts);
@@ -36506,7 +37022,7 @@ function relativePath(absPath, projectRoot3, frameworkRoots) {
36506
37022
  }
36507
37023
  function tokenise(text) {
36508
37024
  if (!text) return [];
36509
- const expanded = text.replace(CAMEL_SPLIT, (_m, a, b, c, d) => a ? `${a} ${b}` : `${c} ${d}`).toLowerCase();
37025
+ const expanded = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ").toLowerCase();
36510
37026
  return expanded.split(/[\s_\-./:,;()\[\]{}\\]+/).filter((p) => p.length > 0);
36511
37027
  }
36512
37028
  function summariseDoc(doc) {
@@ -36737,7 +37253,7 @@ function returnFromSignature(signature) {
36737
37253
  const rest = signature.slice(close + 1).trim();
36738
37254
  return rest.startsWith(":") ? rest.slice(1).trim() : "";
36739
37255
  }
36740
- var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
37256
+ var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
36741
37257
  var init_docs = __esm({
36742
37258
  "src/docs.ts"() {
36743
37259
  "use strict";
@@ -36876,7 +37392,6 @@ var init_docs = __esm({
36876
37392
  __FILENAME = fileURLToPath6(import.meta.url);
36877
37393
  __DIRNAME = path8.dirname(__FILENAME);
36878
37394
  USER_DIRS = ["src/orm", "src/routes", "src/app", "src/services", "src/models"];
36879
- CAMEL_SPLIT = /([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
36880
37395
  FENCE_RE = new RegExp(
36881
37396
  String.fromCharCode(96, 96, 96) + "[a-zA-Z0-9_+-]*\\n([\\s\\S]*?)\\n" + String.fromCharCode(96, 96, 96),
36882
37397
  "g"
@@ -37250,7 +37765,7 @@ ${end}
37250
37765
 
37251
37766
  // src/devAdmin.ts
37252
37767
  import { cpus as osCpus } from "node:os";
37253
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync23, readdirSync as readdirSync16, mkdirSync as mkdirSync18, copyFileSync as copyFileSync2, statSync as statSync17, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
37768
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync23, readdirSync as readdirSync16, mkdirSync as mkdirSync18, copyFileSync as copyFileSync2, statSync as statSync17, 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";
37254
37769
  import { join as join27, dirname as dirname14, resolve as resolve16, relative as relative9, sep as sep8, basename as basename5 } from "node:path";
37255
37770
  import { fileURLToPath as fileURLToPath7 } from "node:url";
37256
37771
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
@@ -37449,8 +37964,41 @@ function quoteTableRef(name) {
37449
37964
  }
37450
37965
  return parts.map((p) => `"${p}"`).join(".");
37451
37966
  }
37967
+ function parseSeedRequest(url, body) {
37968
+ const b = body ?? {};
37969
+ const table2 = String(url.searchParams.get("table") ?? b.table ?? "");
37970
+ const count = parseInt(String(url.searchParams.get("count") ?? b.count ?? "10"), 10) || 10;
37971
+ const seedRaw = url.searchParams.get("seed") ?? b.seed;
37972
+ const seed = seedRaw !== void 0 && seedRaw !== null && String(seedRaw) !== "" ? Number.isNaN(parseInt(String(seedRaw), 10)) ? void 0 : parseInt(String(seedRaw), 10) : void 0;
37973
+ const clear = String(url.searchParams.get("clear") ?? b.clear ?? "") === "true" || b.clear === true;
37974
+ const strict = String(url.searchParams.get("strict") ?? b.strict ?? "") === "true" || b.strict === true;
37975
+ return { table: table2, count, seed, clear, strict };
37976
+ }
37977
+ function buildSeedFieldMap(columns, fake) {
37978
+ const rules = [
37979
+ [(n) => n.includes("email"), () => fake.email()],
37980
+ [(n) => n.includes("name"), () => fake.name()],
37981
+ [(n) => n.includes("phone"), () => fake.phone()],
37982
+ [(n) => n.includes("address") || n.includes("city") || n.includes("country"), () => fake.address()],
37983
+ [(n) => n.includes("url") || n.includes("website"), () => fake.url()],
37984
+ [(_n, t) => t.includes("int"), () => fake.integer(1, 1e3)],
37985
+ [(_n, t) => /real|float|double|numeric|decimal/.test(t), () => fake.numeric(0, 1e3, 2)],
37986
+ [(_n, t) => t.includes("bool"), () => fake.boolean()],
37987
+ [(_n, t) => t.includes("date") || t.includes("time"), () => fake.date()]
37988
+ ];
37989
+ const sentence = () => fake.sentence(3);
37990
+ const fieldMap = {};
37991
+ for (const col of columns) {
37992
+ if (col.name.toLowerCase() === "id" || col.primaryKey === true) continue;
37993
+ const name = col.name.toLowerCase();
37994
+ const type2 = col.type.toLowerCase();
37995
+ const rule = rules.find(([matches]) => matches(name, type2));
37996
+ fieldMap[col.name] = rule ? rule[1] : sentence;
37997
+ }
37998
+ return fieldMap;
37999
+ }
37452
38000
  function supervisorBaseUrl() {
37453
- const explicit = (process.env.TINA4_SUPERVISOR_URL ?? "").replace(/\/+$/, "");
38001
+ const explicit = rtrimSlashes(process.env.TINA4_SUPERVISOR_URL ?? "");
37454
38002
  if (explicit) return explicit;
37455
38003
  const agentPort = (process.env.TINA4_AGENT_PORT ?? "").trim();
37456
38004
  if (/^\d+$/.test(agentPort)) return `http://127.0.0.1:${parseInt(agentPort, 10)}`;
@@ -37458,6 +38006,42 @@ function supervisorBaseUrl() {
37458
38006
  if (/^\d+$/.test(fwPort)) return `http://127.0.0.1:${parseInt(fwPort, 10) + 2e3}`;
37459
38007
  return "http://127.0.0.1:9145";
37460
38008
  }
38009
+ function buildSupervisorBody(method, body) {
38010
+ if (method !== "POST" && method !== "PUT" && method !== "PATCH" && method !== "DELETE") return void 0;
38011
+ if (body === void 0 || body === null) return void 0;
38012
+ if (typeof body === "string") return body;
38013
+ if (typeof body !== "object") return void 0;
38014
+ let outBody = body;
38015
+ if (!Array.isArray(body)) {
38016
+ const pf = body.plan_file;
38017
+ if (typeof pf === "string" && pf && !pf.includes("/")) outBody = { ...body, plan_file: `plan/${pf}` };
38018
+ }
38019
+ return JSON.stringify(outBody);
38020
+ }
38021
+ async function streamEventStream(res, upstream, timer) {
38022
+ res.raw.writeHead(upstream.status || 200, {
38023
+ "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream",
38024
+ "Cache-Control": "no-cache",
38025
+ Connection: "keep-alive"
38026
+ });
38027
+ if (typeof res.raw.flushHeaders === "function") res.raw.flushHeaders();
38028
+ if (!upstream.body) {
38029
+ res.raw.end();
38030
+ clearTimeout(timer);
38031
+ return;
38032
+ }
38033
+ const reader = upstream.body.getReader();
38034
+ try {
38035
+ while (true) {
38036
+ const { done, value } = await reader.read();
38037
+ if (done) break;
38038
+ if (value) res.raw.write(Buffer.from(value));
38039
+ }
38040
+ } finally {
38041
+ clearTimeout(timer);
38042
+ res.raw.end();
38043
+ }
38044
+ }
37461
38045
  async function proxyToSupervisor(req2, res, downstreamPath) {
37462
38046
  const base = supervisorBaseUrl();
37463
38047
  let qs = "";
@@ -37468,24 +38052,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
37468
38052
  }
37469
38053
  const target = `${base}${downstreamPath}${qs}`;
37470
38054
  const method = (req2.method ?? "GET").toUpperCase();
37471
- let bodyText;
37472
- if (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") {
37473
- const body = req2.body;
37474
- if (body !== void 0 && body !== null) {
37475
- if (typeof body === "string") {
37476
- bodyText = body;
37477
- } else if (typeof body === "object") {
37478
- let outBody = body;
37479
- if (!Array.isArray(body)) {
37480
- const pf = body.plan_file;
37481
- if (typeof pf === "string" && pf && !pf.includes("/")) {
37482
- outBody = { ...body, plan_file: `plan/${pf}` };
37483
- }
37484
- }
37485
- bodyText = JSON.stringify(outBody);
37486
- }
37487
- }
37488
- }
38055
+ const bodyText = buildSupervisorBody(method, req2.body);
37489
38056
  const timeoutMs = downstreamPath === "/execute" || downstreamPath === "/chat" ? 6e5 : 3e4;
37490
38057
  const ctrl = new AbortController();
37491
38058
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -37511,30 +38078,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
37511
38078
  }
37512
38079
  const ct = (upstream.headers.get("content-type") ?? "").toLowerCase();
37513
38080
  if (ct.includes("text/event-stream")) {
37514
- res.raw.writeHead(upstream.status || 200, {
37515
- "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream",
37516
- "Cache-Control": "no-cache",
37517
- Connection: "keep-alive"
37518
- });
37519
- if (typeof res.raw.flushHeaders === "function") {
37520
- res.raw.flushHeaders();
37521
- }
37522
- if (!upstream.body) {
37523
- res.raw.end();
37524
- clearTimeout(timer);
37525
- return;
37526
- }
37527
- const reader = upstream.body.getReader();
37528
- try {
37529
- while (true) {
37530
- const { done, value } = await reader.read();
37531
- if (done) break;
37532
- if (value) res.raw.write(Buffer.from(value));
37533
- }
37534
- } finally {
37535
- clearTimeout(timer);
37536
- res.raw.end();
37537
- }
38081
+ await streamEventStream(res, upstream, timer);
37538
38082
  return;
37539
38083
  }
37540
38084
  clearTimeout(timer);
@@ -37651,13 +38195,13 @@ function handleGalleryDeploy(router) {
37651
38195
  res.json({ deployed: name, files: copied });
37652
38196
  };
37653
38197
  }
37654
- function realPathOf(target) {
38198
+ function realPathOf2(target) {
37655
38199
  let existing = target;
37656
38200
  const tail = [];
37657
38201
  for (; ; ) {
37658
38202
  let present = false;
37659
38203
  try {
37660
- lstatSync(existing);
38204
+ lstatSync3(existing);
37661
38205
  present = true;
37662
38206
  } catch {
37663
38207
  }
@@ -37669,9 +38213,9 @@ function realPathOf(target) {
37669
38213
  }
37670
38214
  function safeJoin(projectRoot3, rel, extraRoots = []) {
37671
38215
  try {
37672
- const root = realPathOf(resolve16(projectRoot3));
37673
- const resolved = realPathOf(resolve16(root, rel));
37674
- const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf(resolve16(r)))];
38216
+ const root = realPathOf2(resolve16(projectRoot3));
38217
+ const resolved = realPathOf2(resolve16(root, rel));
38218
+ const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf2(resolve16(r)))];
37675
38219
  for (const r of roots) {
37676
38220
  if (resolved === r || resolved.startsWith(r.endsWith(sep8) ? r : r + sep8)) return resolved;
37677
38221
  }
@@ -37680,7 +38224,7 @@ function safeJoin(projectRoot3, rel, extraRoots = []) {
37680
38224
  return null;
37681
38225
  }
37682
38226
  function resolvedRelative(projectRoot3, resolvedPath) {
37683
- return relative9(realPathOf(resolve16(projectRoot3)), resolvedPath);
38227
+ return relative9(realPathOf2(resolve16(projectRoot3)), resolvedPath);
37684
38228
  }
37685
38229
  function devFilesHidden(name) {
37686
38230
  if (DEV_FILES_IGNORED.has(name)) return true;
@@ -37708,7 +38252,7 @@ async function devGitInfo(root) {
37708
38252
  if (!inside || inside.trim() !== "true") return empty;
37709
38253
  const branch = (git(["rev-parse", "--abbrev-ref", "HEAD"]) ?? "").trim();
37710
38254
  const topRaw = git(["rev-parse", "--show-toplevel"]);
37711
- const gitRoot = topRaw && topRaw.trim() !== "" ? topRaw.trim().replace(/\\/g, "/").replace(/\/+$/, "") : null;
38255
+ const gitRoot = topRaw && topRaw.trim() !== "" ? rtrimSlashes(topRaw.trim().replace(/\\/g, "/")) : null;
37712
38256
  const status2 = /* @__PURE__ */ new Map();
37713
38257
  const porcelain = git(["status", "--porcelain", "-uall"]);
37714
38258
  if (porcelain) {
@@ -37727,6 +38271,44 @@ async function devGitInfo(root) {
37727
38271
  return empty;
37728
38272
  }
37729
38273
  }
38274
+ function devGitLabel(gitPath, isDir, gitStatus) {
38275
+ const code = gitStatus.get(gitPath);
38276
+ if (code !== void 0) return devGitStatusLabel(code);
38277
+ if (isDir) {
38278
+ const prefix = gitPath + "/";
38279
+ for (const [gf, gc] of gitStatus) {
38280
+ if (gf.startsWith(prefix)) return gc === "??" ? "untracked" : "modified";
38281
+ }
38282
+ }
38283
+ return "clean";
38284
+ }
38285
+ function devDirHasVisibleChild(full) {
38286
+ try {
38287
+ for (const c of readdirSync16(full)) {
38288
+ if (!devFilesHidden(c)) return true;
38289
+ }
38290
+ } catch {
38291
+ }
38292
+ return false;
38293
+ }
38294
+ function buildFileEntry(name, full, entryRel, cwdInGit, gitStatus) {
38295
+ let isDir = false;
38296
+ let size = null;
38297
+ try {
38298
+ const st = statSync17(full);
38299
+ isDir = st.isDirectory();
38300
+ if (!isDir) size = st.size;
38301
+ } catch {
38302
+ }
38303
+ return {
38304
+ name,
38305
+ path: entryRel,
38306
+ is_dir: isDir,
38307
+ has_children: isDir ? devDirHasVisibleChild(full) : null,
38308
+ git_status: devGitLabel(cwdInGit + entryRel, isDir, gitStatus),
38309
+ size
38310
+ };
38311
+ }
37730
38312
  function devAdminLanguage(rel) {
37731
38313
  const base = (rel.split(/[\\/]/).pop() ?? "").toLowerCase();
37732
38314
  if (base === "dockerfile" || base === "dockerfile.dev" || base === "dockerfile.prod") {
@@ -37741,14 +38323,14 @@ function devAdminLanguage(rel) {
37741
38323
  function readDevFile(target) {
37742
38324
  let fd;
37743
38325
  try {
37744
- fd = openSync4(target, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
37745
- if (!fstatSync2(fd).isFile()) return null;
38326
+ fd = openSync5(target, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
38327
+ if (!fstatSync3(fd).isFile()) return null;
37746
38328
  return readFileSync20(fd);
37747
38329
  } catch (error2) {
37748
38330
  if (["ENOENT", "ENOTDIR", "ELOOP"].includes(error2.code ?? "")) return null;
37749
38331
  throw error2;
37750
38332
  } finally {
37751
- if (fd !== void 0) closeSync4(fd);
38333
+ if (fd !== void 0) closeSync5(fd);
37752
38334
  }
37753
38335
  }
37754
38336
  function mcpSecureEqual(expected, provided) {
@@ -37975,10 +38557,11 @@ function toolbarJs() {
37975
38557
  connect();
37976
38558
  })();`;
37977
38559
  }
37978
- 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;
38560
+ 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;
37979
38561
  var init_devAdmin = __esm({
37980
38562
  "src/devAdmin.ts"() {
37981
38563
  "use strict";
38564
+ init_slashTrim();
37982
38565
  init_devMailbox();
37983
38566
  init_upstreamResponse();
37984
38567
  init_dotenv();
@@ -38445,6 +39028,7 @@ var init_devAdmin = __esm({
38445
39028
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
38446
39029
  // JS asset
38447
39030
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
39031
+ { method: "GET", pattern: "/__dev/split", handler: handleDevAdminSplit },
38448
39032
  // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
38449
39033
  // CSP-clean (no inline style=, onclick=, or <script>) under a strict
38450
39034
  // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
@@ -38811,13 +39395,7 @@ var init_devAdmin = __esm({
38811
39395
  };
38812
39396
  handleSeed = async (req2, res) => {
38813
39397
  const url = new URL(req2.url ?? "/", "http://localhost");
38814
- const body = req2.body ?? {};
38815
- const table2 = url.searchParams.get("table") ?? body?.table ?? "";
38816
- const count = parseInt(String(url.searchParams.get("count") ?? body?.count ?? "10"), 10) || 10;
38817
- const seedRaw = url.searchParams.get("seed") ?? body?.seed;
38818
- const seed = seedRaw !== void 0 && seedRaw !== null && String(seedRaw) !== "" ? Number.isNaN(parseInt(String(seedRaw), 10)) ? void 0 : parseInt(String(seedRaw), 10) : void 0;
38819
- const clear = String(url.searchParams.get("clear") ?? body?.clear ?? "") === "true" || body?.clear === true;
38820
- const strict = String(url.searchParams.get("strict") ?? body?.strict ?? "") === "true" || body?.strict === true;
39398
+ const { table: table2, count, seed, clear, strict } = parseSeedRequest(url, req2.body);
38821
39399
  if (!table2) {
38822
39400
  res.json({ error: "Missing table parameter" });
38823
39401
  return;
@@ -38832,33 +39410,7 @@ var init_devAdmin = __esm({
38832
39410
  res.json({ error: `Table '${table2}' not found or has no columns` });
38833
39411
  return;
38834
39412
  }
38835
- const fieldMap = {};
38836
- for (const col of columns) {
38837
- const name = col.name.toLowerCase();
38838
- const type2 = col.type.toLowerCase();
38839
- if (name === "id" || col.primaryKey === true) continue;
38840
- if (name.includes("email")) {
38841
- fieldMap[col.name] = () => fake.email();
38842
- } else if (name.includes("name")) {
38843
- fieldMap[col.name] = () => fake.name();
38844
- } else if (name.includes("phone")) {
38845
- fieldMap[col.name] = () => fake.phone();
38846
- } else if (name.includes("address") || name.includes("city") || name.includes("country")) {
38847
- fieldMap[col.name] = () => fake.address();
38848
- } else if (name.includes("url") || name.includes("website")) {
38849
- fieldMap[col.name] = () => fake.url();
38850
- } else if (type2.includes("int")) {
38851
- fieldMap[col.name] = () => fake.integer(1, 1e3);
38852
- } else if (type2.includes("real") || type2.includes("float") || type2.includes("double") || type2.includes("numeric") || type2.includes("decimal")) {
38853
- fieldMap[col.name] = () => fake.numeric(0, 1e3, 2);
38854
- } else if (type2.includes("bool")) {
38855
- fieldMap[col.name] = () => fake.boolean();
38856
- } else if (type2.includes("date") || type2.includes("time")) {
38857
- fieldMap[col.name] = () => fake.date();
38858
- } else {
38859
- fieldMap[col.name] = () => fake.sentence(3);
38860
- }
38861
- }
39413
+ const fieldMap = buildSeedFieldMap(columns, fake);
38862
39414
  const summary = await seedTable2(db, table2, count, fieldMap, void 0, { clear, strict });
38863
39415
  res.json({ seeded: summary.seeded, failed: summary.failed, errors: summary.errors, table: table2 });
38864
39416
  } catch (e) {
@@ -39031,9 +39583,9 @@ var init_devAdmin = __esm({
39031
39583
  };
39032
39584
  handleTest = async (_req, res) => {
39033
39585
  try {
39034
- const { execFile } = await import("node:child_process");
39586
+ const { execFile: execFile2 } = await import("node:child_process");
39035
39587
  const { promisify } = await import("node:util");
39036
- const run = promisify(execFile);
39588
+ const run = promisify(execFile2);
39037
39589
  try {
39038
39590
  const { stdout, stderr } = await run("npm", ["test"], {
39039
39591
  cwd: resolve16(process.cwd()),
@@ -39151,9 +39703,9 @@ var init_devAdmin = __esm({
39151
39703
  }
39152
39704
  try {
39153
39705
  const envPath = join27(process.cwd(), ".env");
39154
- const fd = openSync4(envPath, constants3.O_RDWR | constants3.O_APPEND | constants3.O_CREAT | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0), 384);
39706
+ const fd = openSync5(envPath, constants4.O_RDWR | constants4.O_APPEND | constants4.O_CREAT | (constants4.O_NOFOLLOW ?? 0) | (constants4.O_NONBLOCK ?? 0), 384);
39155
39707
  try {
39156
- const info = fstatSync2(fd);
39708
+ const info = fstatSync3(fd);
39157
39709
  if (!info.isFile() || info.nlink !== 1) throw new Error("Refusing a non-regular or multiply-linked configuration file");
39158
39710
  fchmodSync2(fd, 384);
39159
39711
  const lines = readFileSync20(fd, "utf-8").split("\n");
@@ -39183,10 +39735,10 @@ var init_devAdmin = __esm({
39183
39735
  for (const [key, found] of Object.entries(keysFound)) {
39184
39736
  if (!found) newLines.push(`${key}=${values[key]}`);
39185
39737
  }
39186
- ftruncateSync(fd, 0);
39738
+ ftruncateSync2(fd, 0);
39187
39739
  writeFileSync15(fd, newLines.join("\n") + "\n");
39188
39740
  } finally {
39189
- closeSync4(fd);
39741
+ closeSync5(fd);
39190
39742
  }
39191
39743
  res.json({ success: true });
39192
39744
  } catch (e) {
@@ -39309,48 +39861,7 @@ var init_devAdmin = __esm({
39309
39861
  const full = join27(target, name);
39310
39862
  const entryRel = relative9(root, full).replace(/\\/g, "/");
39311
39863
  if (isSecretPath(entryRel)) continue;
39312
- let isDir = false;
39313
- let size = null;
39314
- try {
39315
- const st = statSync17(full);
39316
- isDir = st.isDirectory();
39317
- if (!isDir) size = st.size;
39318
- } catch {
39319
- }
39320
- const gitPath = cwdInGit + entryRel;
39321
- let gitLabel = "clean";
39322
- const code = gitStatus.get(gitPath);
39323
- if (code !== void 0) {
39324
- gitLabel = devGitStatusLabel(code);
39325
- } else if (isDir) {
39326
- const prefix = gitPath + "/";
39327
- for (const [gf, gc] of gitStatus) {
39328
- if (gf.startsWith(prefix)) {
39329
- gitLabel = gc === "??" ? "untracked" : "modified";
39330
- break;
39331
- }
39332
- }
39333
- }
39334
- let hasChildren = null;
39335
- if (isDir) {
39336
- hasChildren = false;
39337
- try {
39338
- for (const c of readdirSync16(full)) {
39339
- if (devFilesHidden(c)) continue;
39340
- hasChildren = true;
39341
- break;
39342
- }
39343
- } catch {
39344
- }
39345
- }
39346
- entries.push({
39347
- name,
39348
- path: entryRel,
39349
- is_dir: isDir,
39350
- has_children: hasChildren,
39351
- git_status: gitLabel,
39352
- size
39353
- });
39864
+ entries.push(buildFileEntry(name, full, entryRel, cwdInGit, gitStatus));
39354
39865
  }
39355
39866
  res.json({ path: relative9(root, target).replace(/\\/g, "/") || ".", branch, entries });
39356
39867
  };
@@ -39883,6 +40394,31 @@ var init_devAdmin = __esm({
39883
40394
  res.raw.writeHead(404, { "Content-Type": "text/plain" });
39884
40395
  res.raw.end("tina4-dev-admin.min.js not found");
39885
40396
  };
40397
+ handleDevAdminSplit = async (_req, res) => {
40398
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
40399
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
40400
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
40401
+ const dir = dirname17(fileURLToPath10(import.meta.url));
40402
+ const candidates = [
40403
+ join33(dir, "..", "public", "js", "tina4-dev-admin-split.html"),
40404
+ join33(dir, "..", "..", "public", "js", "tina4-dev-admin-split.html"),
40405
+ resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin-split.html"),
40406
+ resolve21(process.cwd(), "public", "js", "tina4-dev-admin-split.html")
40407
+ ];
40408
+ for (const path10 of candidates) {
40409
+ if (existsSync29(path10)) {
40410
+ try {
40411
+ const content = readFileSync27(path10, "utf-8");
40412
+ res.raw.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
40413
+ res.raw.end(content);
40414
+ return;
40415
+ } catch {
40416
+ }
40417
+ }
40418
+ }
40419
+ res.raw.writeHead(404, { "Content-Type": "text/plain" });
40420
+ res.raw.end("tina4-dev-admin-split.html not found");
40421
+ };
39886
40422
  handleToolbarCss = (_req, res) => {
39887
40423
  res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
39888
40424
  res.raw.end(toolbarCss());
@@ -40354,7 +40890,10 @@ function generate2(routes, models = []) {
40354
40890
  openapi: resolveOpenApiVersion(),
40355
40891
  info: buildInfo(),
40356
40892
  servers: resolveServers(),
40357
- paths: {},
40893
+ // Prototype-less: route paths are developer-defined, but a null prototype
40894
+ // makes `spec.paths[path][method] = op` structurally pollution-proof
40895
+ // regardless (js/prototype-polluting-assignment). Serialises to JSON the same.
40896
+ paths: /* @__PURE__ */ Object.create(null),
40358
40897
  components: {
40359
40898
  schemas: {},
40360
40899
  // Configurable security schemes (v3.13.42): bearerFormat via env, optional
@@ -40419,7 +40958,7 @@ function buildOperation(route, spec, ctx) {
40419
40958
  const openApiPath = patternToOpenAPI(route.pattern);
40420
40959
  const method = route.method.toLowerCase();
40421
40960
  if (!spec.paths[openApiPath]) {
40422
- spec.paths[openApiPath] = {};
40961
+ spec.paths[openApiPath] = /* @__PURE__ */ Object.create(null);
40423
40962
  }
40424
40963
  const tags = route.meta?.tags ?? inferTags(route.pattern);
40425
40964
  for (const t of tags) {
@@ -40725,7 +41264,7 @@ function inferModelFromPath(pattern) {
40725
41264
  return null;
40726
41265
  }
40727
41266
  function operationIdBase(method, openApiPath) {
40728
- const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
41267
+ const clean = trimSlashes(openApiPath).replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
40729
41268
  return clean ? `${method}_${clean}` : method;
40730
41269
  }
40731
41270
  function uniqueOperationId(method, openApiPath, seen) {
@@ -40743,6 +41282,7 @@ var WRITE_METHODS, INTERNAL_PREFIXES, registeredSchemes, registeredSchemas, PARA
40743
41282
  var init_generator = __esm({
40744
41283
  "../swagger/src/generator.ts"() {
40745
41284
  "use strict";
41285
+ init_slashTrim();
40746
41286
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
40747
41287
  INTERNAL_PREFIXES = ["/swagger", "/__dev", "/__feedback", "/ai", "/rag", "/vision", "/embed", "/image"];
40748
41288
  registeredSchemes = {};
@@ -40764,7 +41304,7 @@ var init_generator = __esm({
40764
41304
 
40765
41305
  // ../swagger/src/ui.ts
40766
41306
  function swaggerUiCdn() {
40767
- return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
41307
+ return rtrimSlashes(process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5");
40768
41308
  }
40769
41309
  function swaggerEnabled() {
40770
41310
  const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
@@ -40820,6 +41360,7 @@ var SWAGGER_UI_HTML;
40820
41360
  var init_ui = __esm({
40821
41361
  "../swagger/src/ui.ts"() {
40822
41362
  "use strict";
41363
+ init_slashTrim();
40823
41364
  SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
40824
41365
  <html lang="en">
40825
41366
  <head>
@@ -40874,7 +41415,7 @@ __export(sso_exports, {
40874
41415
  SsoError: () => SsoError,
40875
41416
  liveSessionIdentity: () => liveSessionIdentity
40876
41417
  });
40877
- import { createHash as createHash10, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
41418
+ import { createHash as createHash11, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
40878
41419
  function expiresAt(tokens) {
40879
41420
  const lifetime = Number(tokens.expires_in ?? 0);
40880
41421
  return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
@@ -41005,7 +41546,7 @@ var init_sso = __esm({
41005
41546
  const state = randomBytes8(32).toString("base64url");
41006
41547
  const nonce = randomBytes8(32).toString("base64url");
41007
41548
  const verifier = randomBytes8(64).toString("base64url");
41008
- const challenge = createHash10("sha256").update(verifier).digest("base64url");
41549
+ const challenge = createHash11("sha256").update(verifier).digest("base64url");
41009
41550
  session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
41010
41551
  const metadata = await this.discover();
41011
41552
  const query = new URLSearchParams({
@@ -41237,7 +41778,7 @@ import { resolve as resolve19, dirname as dirname15, join as join30, relative as
41237
41778
  import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as readFileSync23, statSync as statSync18 } from "node:fs";
41238
41779
  import { isatty } from "node:tty";
41239
41780
  import { fileURLToPath as fileURLToPath8 } from "node:url";
41240
- import { execFileSync as execFileSync3, exec } from "node:child_process";
41781
+ import { execFileSync as execFileSync3, execFile } from "node:child_process";
41241
41782
  import cluster from "node:cluster";
41242
41783
  import os2 from "node:os";
41243
41784
  function isSwaggerAssetPath(pathname) {
@@ -41373,10 +41914,11 @@ function shouldOpenBrowser(isDevelopment, noBrowser = false) {
41373
41914
  });
41374
41915
  }
41375
41916
  function openBrowser(url) {
41917
+ if (!/^https?:\/\/([A-Za-z0-9.-]+|\[[0-9A-Fa-f:]+\])(:\d{1,5})?\/?$/.test(url)) return;
41376
41918
  setTimeout(() => {
41377
- if (process.platform === "darwin") exec(`open ${url}`);
41378
- else if (process.platform === "win32") exec(`start "" "${url}"`);
41379
- else exec(`xdg-open ${url}`);
41919
+ if (process.platform === "darwin") execFile("open", [url]);
41920
+ else if (process.platform === "win32") execFile("rundll32", ["url.dll,FileProtocolHandler", url]);
41921
+ else execFile("xdg-open", [url]);
41380
41922
  }, 2e3);
41381
41923
  }
41382
41924
  function resolvePortAndHost(config) {
@@ -41491,7 +42033,7 @@ function resetTemplateCache() {
41491
42033
  }
41492
42034
  function resolveTemplate(pathname, templatesDir) {
41493
42035
  if (!templateAutoRoutingEnabled()) return null;
41494
- const cleanPath = pathname.replace(/^\/+/, "").replace(/\/+$/, "") || "index";
42036
+ const cleanPath = trimSlashes(pathname) || "index";
41495
42037
  const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
41496
42038
  if (isDev2) {
41497
42039
  if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
@@ -41759,7 +42301,7 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
41759
42301
  }
41760
42302
  function resolveHandlerArgs(handler, req2, res, routeParams) {
41761
42303
  const fnStr = handler.toString();
41762
- const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
42304
+ const argMatch = fnStr.match(/^(?:async\s*)?(?:function(?:\s+\w+)?\s*)?\(([^)]*)\)/);
41763
42305
  const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
41764
42306
  const filteredArgs = argNames.filter((n) => n.length > 0);
41765
42307
  if (filteredArgs.length === 0) return [];
@@ -41995,8 +42537,9 @@ async function buildDispatchContext(router, base) {
41995
42537
  const templatesDir = resolve19(root, "src/templates");
41996
42538
  let frondEngine = null;
41997
42539
  try {
41998
- const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42540
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2, setFormTokenSessionProvider: setFormTokenSessionProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
41999
42541
  setCspNonceProvider2(currentCspNonce);
42542
+ setFormTokenSessionProvider2(currentFormTokenSessionId);
42000
42543
  frondEngine = new Frond2(templatesDir);
42001
42544
  } catch {
42002
42545
  }
@@ -42111,7 +42654,10 @@ async function runDispatch(ctx, rawReq, rawRes) {
42111
42654
  const requestNonce = generateNonce();
42112
42655
  return Log.runWithRequestId(
42113
42656
  requestId,
42114
- () => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
42657
+ () => runWithCspNonce(
42658
+ requestNonce,
42659
+ () => runWithFormTokenSession(() => dispatchInner(ctx, rawReq, rawRes, requestId))
42660
+ )
42115
42661
  );
42116
42662
  }
42117
42663
  function loopbackBindHosts(host) {
@@ -42239,8 +42785,9 @@ async function startServer(config) {
42239
42785
  let frondEngine = null;
42240
42786
  setDefaultTemplatesDir(templatesDir);
42241
42787
  try {
42242
- const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42788
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2, setFormTokenSessionProvider: setFormTokenSessionProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42243
42789
  setCspNonceProvider2(currentCspNonce);
42790
+ setFormTokenSessionProvider2(currentFormTokenSessionId);
42244
42791
  frondEngine = new Frond2(templatesDir);
42245
42792
  router.addRoute({
42246
42793
  method: "GET",
@@ -42589,6 +43136,7 @@ var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swag
42589
43136
  var init_server = __esm({
42590
43137
  "src/server.ts"() {
42591
43138
  "use strict";
43139
+ init_slashTrim();
42592
43140
  init_transport();
42593
43141
  init_router();
42594
43142
  init_authGate();
@@ -42596,6 +43144,7 @@ var init_server = __esm({
42596
43144
  init_portTakeover();
42597
43145
  init_request();
42598
43146
  init_dispatchPipeline();
43147
+ init_session();
42599
43148
  init_response();
42600
43149
  init_middleware();
42601
43150
  init_static();
@@ -44211,6 +44760,7 @@ var ApiStreamError, RETRY_STATUSES, DOWNLOAD_CHUNK_SIZE, MAX_REDIRECTS, STRIP_ON
44211
44760
  var init_api = __esm({
44212
44761
  "src/api.ts"() {
44213
44762
  "use strict";
44763
+ init_slashTrim();
44214
44764
  init_version();
44215
44765
  init_ssrf();
44216
44766
  ApiStreamError = class extends Error {
@@ -44308,7 +44858,7 @@ var init_api = __esm({
44308
44858
  * turns on a per-client, in-memory cookie jar.
44309
44859
  */
44310
44860
  constructor(baseUrl = "", authHeaderOrOptions = "", timeout = 30) {
44311
- this.baseUrl = baseUrl.replace(/\/+$/, "");
44861
+ this.baseUrl = rtrimSlashes(baseUrl);
44312
44862
  this.headers = {};
44313
44863
  this.maxRetries = 0;
44314
44864
  this.retryBackoff = 0.5;
@@ -45074,6 +45624,23 @@ function sendCommand(socket, command) {
45074
45624
  });
45075
45625
  });
45076
45626
  }
45627
+ function pushAlternativeBody(lines, altBoundary, text, body) {
45628
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
45629
+ lines.push("");
45630
+ lines.push(`--${altBoundary}`);
45631
+ lines.push("Content-Type: text/plain; charset=UTF-8");
45632
+ lines.push("Content-Transfer-Encoding: 7bit");
45633
+ lines.push("");
45634
+ lines.push(text);
45635
+ lines.push("");
45636
+ lines.push(`--${altBoundary}`);
45637
+ lines.push("Content-Type: text/html; charset=UTF-8");
45638
+ lines.push("Content-Transfer-Encoding: 7bit");
45639
+ lines.push("");
45640
+ lines.push(body);
45641
+ lines.push("");
45642
+ lines.push(`--${altBoundary}--`);
45643
+ }
45077
45644
  function buildMimeMessage(options) {
45078
45645
  const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
45079
45646
  const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
@@ -45103,21 +45670,7 @@ function buildMimeMessage(options) {
45103
45670
  lines.push("");
45104
45671
  lines.push(`--${boundary}`);
45105
45672
  if (hasTextAlt) {
45106
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
45107
- lines.push("");
45108
- lines.push(`--${altBoundary}`);
45109
- lines.push("Content-Type: text/plain; charset=UTF-8");
45110
- lines.push("Content-Transfer-Encoding: 7bit");
45111
- lines.push("");
45112
- lines.push(options.text);
45113
- lines.push("");
45114
- lines.push(`--${altBoundary}`);
45115
- lines.push("Content-Type: text/html; charset=UTF-8");
45116
- lines.push("Content-Transfer-Encoding: 7bit");
45117
- lines.push("");
45118
- lines.push(options.body);
45119
- lines.push("");
45120
- lines.push(`--${altBoundary}--`);
45673
+ pushAlternativeBody(lines, altBoundary, options.text, options.body);
45121
45674
  } else {
45122
45675
  const contentType = options.html ? "text/html" : "text/plain";
45123
45676
  lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
@@ -45142,21 +45695,7 @@ function buildMimeMessage(options) {
45142
45695
  lines.push("");
45143
45696
  lines.push(`--${boundary}--`);
45144
45697
  } else if (hasTextAlt) {
45145
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
45146
- lines.push("");
45147
- lines.push(`--${altBoundary}`);
45148
- lines.push("Content-Type: text/plain; charset=UTF-8");
45149
- lines.push("Content-Transfer-Encoding: 7bit");
45150
- lines.push("");
45151
- lines.push(options.text);
45152
- lines.push("");
45153
- lines.push(`--${altBoundary}`);
45154
- lines.push("Content-Type: text/html; charset=UTF-8");
45155
- lines.push("Content-Transfer-Encoding: 7bit");
45156
- lines.push("");
45157
- lines.push(options.body);
45158
- lines.push("");
45159
- lines.push(`--${altBoundary}--`);
45698
+ pushAlternativeBody(lines, altBoundary, options.text, options.body);
45160
45699
  } else {
45161
45700
  const contentType = options.html ? "text/html" : "text/plain";
45162
45701
  lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
@@ -45921,61 +46460,72 @@ function decodeEntities(raw) {
45921
46460
  function parseXmlDocument(xml) {
45922
46461
  const stack = [];
45923
46462
  let root = null;
45924
- let position = 0;
45925
46463
  const addText = (text) => {
45926
46464
  const current = stack[stack.length - 1];
45927
46465
  if (current.children.length === 0) current.text += text;
45928
46466
  };
45929
- while (position < xml.length) {
45930
- const nextTag = xml.indexOf("<", position);
46467
+ const skipComment = (at) => {
46468
+ const close = xml.indexOf("-->", at + 4);
46469
+ if (close === -1) malformed();
46470
+ return close + 3;
46471
+ };
46472
+ const readCData = (at) => {
46473
+ const close = xml.indexOf("]]>", at + 9);
46474
+ if (close === -1 || stack.length === 0) malformed();
46475
+ addText(xml.slice(at + 9, close));
46476
+ return close + 3;
46477
+ };
46478
+ const skipProcessingInstruction = (at) => {
46479
+ const close = xml.indexOf("?>", at + 2);
46480
+ if (close === -1) malformed();
46481
+ const target = xml.slice(at + 2, close).split(/\s/, 1)[0];
46482
+ if (target.toLowerCase() === "xml" && at !== 0) malformed();
46483
+ return close + 2;
46484
+ };
46485
+ const closeElement = (at) => {
46486
+ END_TAG.lastIndex = at;
46487
+ const match = END_TAG.exec(xml);
46488
+ const open = stack.pop();
46489
+ if (!match || !open || open.qualifiedName !== match[1]) malformed();
46490
+ return END_TAG.lastIndex;
46491
+ };
46492
+ const openElement = (at) => {
46493
+ START_TAG.lastIndex = at;
46494
+ const match = START_TAG.exec(xml);
46495
+ if (!match || stack.length === 0 && root !== null) malformed();
46496
+ for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
46497
+ const qualifiedName = match[1];
46498
+ const element = {
46499
+ name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
46500
+ qualifiedName,
46501
+ children: [],
46502
+ text: ""
46503
+ };
46504
+ if (stack.length > 0) stack[stack.length - 1].children.push(element);
46505
+ else root = element;
46506
+ if (match[3] !== "/") stack.push(element);
46507
+ return START_TAG.lastIndex;
46508
+ };
46509
+ const readText = (at) => {
46510
+ const nextTag = xml.indexOf("<", at);
45931
46511
  const textEnd = nextTag === -1 ? xml.length : nextTag;
45932
- if (textEnd > position) {
45933
- const raw = xml.slice(position, textEnd);
46512
+ if (textEnd > at) {
46513
+ const raw = xml.slice(at, textEnd);
45934
46514
  if (stack.length > 0) addText(decodeEntities(raw));
45935
46515
  else if (raw.trim() !== "") malformed();
45936
- position = textEnd;
45937
- if (nextTag === -1) break;
45938
- }
45939
- if (xml.startsWith("<!--", position)) {
45940
- const close = xml.indexOf("-->", position + 4);
45941
- if (close === -1) malformed();
45942
- position = close + 3;
45943
- } else if (xml.startsWith("<![CDATA[", position)) {
45944
- const close = xml.indexOf("]]>", position + 9);
45945
- if (close === -1 || stack.length === 0) malformed();
45946
- addText(xml.slice(position + 9, close));
45947
- position = close + 3;
45948
- } else if (xml.startsWith("<?", position)) {
45949
- const close = xml.indexOf("?>", position + 2);
45950
- if (close === -1) malformed();
45951
- const target = xml.slice(position + 2, close).split(/\s/, 1)[0];
45952
- if (target.toLowerCase() === "xml" && position !== 0) malformed();
45953
- position = close + 2;
45954
- } else if (xml.startsWith("<!", position)) {
45955
- malformed();
45956
- } else if (xml.startsWith("</", position)) {
45957
- END_TAG.lastIndex = position;
45958
- const match = END_TAG.exec(xml);
45959
- const open = stack.pop();
45960
- if (!match || !open || open.qualifiedName !== match[1]) malformed();
45961
- position = END_TAG.lastIndex;
45962
- } else {
45963
- START_TAG.lastIndex = position;
45964
- const match = START_TAG.exec(xml);
45965
- if (!match || stack.length === 0 && root !== null) malformed();
45966
- for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
45967
- const qualifiedName = match[1];
45968
- const element = {
45969
- name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
45970
- qualifiedName,
45971
- children: [],
45972
- text: ""
45973
- };
45974
- if (stack.length > 0) stack[stack.length - 1].children.push(element);
45975
- else root = element;
45976
- if (match[3] !== "/") stack.push(element);
45977
- position = START_TAG.lastIndex;
45978
46516
  }
46517
+ return textEnd;
46518
+ };
46519
+ let position = 0;
46520
+ while (position < xml.length) {
46521
+ position = readText(position);
46522
+ if (position >= xml.length) break;
46523
+ if (xml.startsWith("<!--", position)) position = skipComment(position);
46524
+ else if (xml.startsWith("<![CDATA[", position)) position = readCData(position);
46525
+ else if (xml.startsWith("<?", position)) position = skipProcessingInstruction(position);
46526
+ else if (xml.startsWith("<!", position)) malformed();
46527
+ else if (xml.startsWith("</", position)) position = closeElement(position);
46528
+ else position = openElement(position);
45979
46529
  }
45980
46530
  if (root === null || stack.length > 0) malformed();
45981
46531
  return root;
@@ -47459,13 +48009,13 @@ export default class User {
47459
48009
  import {
47460
48010
  createCipheriv,
47461
48011
  createECDH,
47462
- createHmac as createHmac4,
48012
+ createHmac as createHmac5,
47463
48013
  createPrivateKey,
47464
48014
  createSign as createSign2,
47465
48015
  randomBytes as randomBytes12
47466
48016
  } from "node:crypto";
47467
48017
  function encodeBase64Url(value) {
47468
- return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
48018
+ return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
47469
48019
  }
47470
48020
  function decodeBase64Url(value, name) {
47471
48021
  if (typeof value !== "string" || value.length === 0) {
@@ -47495,15 +48045,15 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
47495
48045
  format: "jwk"
47496
48046
  });
47497
48047
  }
47498
- function hmac(salt, value) {
47499
- return createHmac4("sha256", salt).update(value).digest();
48048
+ function hmac2(salt, value) {
48049
+ return createHmac5("sha256", salt).update(value).digest();
47500
48050
  }
47501
48051
  function hkdfExpand(prk, info, length) {
47502
48052
  const chunks = [];
47503
48053
  let previous = Buffer.alloc(0);
47504
48054
  for (let counter = 1; Buffer.concat(chunks).length < length; counter++) {
47505
48055
  if (counter > 255) throw new PushError("HKDF output is too large");
47506
- previous = hmac(prk, Buffer.concat([previous, info, Buffer.from([counter])]));
48056
+ previous = hmac2(prk, Buffer.concat([previous, info, Buffer.from([counter])]));
47507
48057
  chunks.push(previous);
47508
48058
  }
47509
48059
  return Buffer.concat(chunks).subarray(0, length);
@@ -47540,10 +48090,10 @@ function encryptPayload(payload, subscription) {
47540
48090
  clientPublic,
47541
48091
  serverPublic
47542
48092
  ]);
47543
- const prkKey = hmac(authSecret, sharedSecret);
48093
+ const prkKey = hmac2(authSecret, sharedSecret);
47544
48094
  const ikm = hkdfExpand(prkKey, keyInfo, 32);
47545
48095
  const salt = randomBytes12(16);
47546
- const contentPrk = hmac(salt, ikm);
48096
+ const contentPrk = hmac2(salt, ikm);
47547
48097
  const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
47548
48098
  const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
47549
48099
  const cipher = createCipheriv("aes-128-gcm", cek, nonce);
@@ -47716,6 +48266,7 @@ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai, Aggre
47716
48266
  var init_aiClient = __esm({
47717
48267
  "src/aiClient.ts"() {
47718
48268
  "use strict";
48269
+ init_slashTrim();
47719
48270
  init_api();
47720
48271
  AiError = class extends Error {
47721
48272
  };
@@ -47920,7 +48471,7 @@ var init_aiClient = __esm({
47920
48471
  throw new AiConfigError("AI URL must be an http or https URL");
47921
48472
  }
47922
48473
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
47923
- const path10 = url.pathname.replace(/\/+$/, "");
48474
+ const path10 = rtrimSlashes(url.pathname);
47924
48475
  if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
47925
48476
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
47926
48477
  url.pathname = (path10 || "/v1") + suffix;
@@ -50290,6 +50841,7 @@ __export(index_exports, {
50290
50841
  backgroundTaskCount: () => backgroundTaskCount,
50291
50842
  buildEnvelope: () => buildEnvelope,
50292
50843
  buildFrame: () => buildFrame,
50844
+ buildSeedFieldMap: () => buildSeedFieldMap,
50293
50845
  buildSessionCookie: () => buildSessionCookie,
50294
50846
  cacheBackendStats: () => cacheBackendStats,
50295
50847
  cacheClear: () => cacheClear,
@@ -50395,6 +50947,7 @@ __export(index_exports, {
50395
50947
  parseFrame: () => parseFrame,
50396
50948
  parseLineStream: () => parseLineStream,
50397
50949
  parseMultipart: () => parseMultipart,
50950
+ parseSeedRequest: () => parseSeedRequest,
50398
50951
  parseSseStream: () => parseSseStream,
50399
50952
  parseUpgradeHeaders: () => parseUpgradeHeaders,
50400
50953
  patch: () => patch,
@@ -50679,6 +51232,7 @@ export {
50679
51232
  backgroundTaskCount,
50680
51233
  buildEnvelope,
50681
51234
  buildFrame,
51235
+ buildSeedFieldMap,
50682
51236
  buildSessionCookie,
50683
51237
  cacheBackendStats,
50684
51238
  cacheClear,
@@ -50784,6 +51338,7 @@ export {
50784
51338
  parseFrame,
50785
51339
  parseLineStream,
50786
51340
  parseMultipart,
51341
+ parseSeedRequest,
50787
51342
  parseSseStream,
50788
51343
  parseUpgradeHeaders,
50789
51344
  patch,