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
@@ -14,6 +14,30 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
+ // ../core/src/slashTrim.ts
18
+ function rtrimSlashes(s) {
19
+ let end = s.length;
20
+ while (end > 0 && s.charCodeAt(end - 1) === SLASH) end--;
21
+ return end === s.length ? s : s.slice(0, end);
22
+ }
23
+ function trimChar(s, code) {
24
+ let start2 = 0;
25
+ let end = s.length;
26
+ while (start2 < end && s.charCodeAt(start2) === code) start2++;
27
+ while (end > start2 && s.charCodeAt(end - 1) === code) end--;
28
+ return start2 === 0 && end === s.length ? s : s.slice(start2, end);
29
+ }
30
+ function trimSlashes(s) {
31
+ return trimChar(s, SLASH);
32
+ }
33
+ var SLASH;
34
+ var init_slashTrim = __esm({
35
+ "../core/src/slashTrim.ts"() {
36
+ "use strict";
37
+ SLASH = 47;
38
+ }
39
+ });
40
+
17
41
  // ../core/src/constants.ts
18
42
  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;
19
43
  var init_constants = __esm({
@@ -1102,7 +1126,7 @@ function resolveAlgorithm(algorithm) {
1102
1126
  return chosen;
1103
1127
  }
1104
1128
  function base64urlEncode(data) {
1105
- return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1129
+ return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
1106
1130
  }
1107
1131
  function base64urlDecode(str) {
1108
1132
  let s = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -1381,6 +1405,12 @@ var init_types = __esm({
1381
1405
  });
1382
1406
 
1383
1407
  // ../orm/src/databaseResult.ts
1408
+ function stripTypeArgs(colType) {
1409
+ const open = colType.indexOf("(");
1410
+ if (open < 0) return colType;
1411
+ const close = colType.indexOf(")", open);
1412
+ return close < 0 ? colType : colType.slice(0, open) + colType.slice(close + 1);
1413
+ }
1384
1414
  var DatabaseResult;
1385
1415
  var init_databaseResult = __esm({
1386
1416
  "../orm/src/databaseResult.ts"() {
@@ -1563,7 +1593,7 @@ var init_databaseResult = __esm({
1563
1593
  const [size, decimals] = this._parseTypeSize(colType);
1564
1594
  return {
1565
1595
  name: col.name,
1566
- type: colType.replace(/\(.*\)/, ""),
1596
+ type: stripTypeArgs(colType),
1567
1597
  size,
1568
1598
  decimals,
1569
1599
  nullable: col.nullable ?? true,
@@ -2120,12 +2150,49 @@ var init_point = __esm({
2120
2150
  });
2121
2151
 
2122
2152
  // ../orm/src/sqlTranslator.ts
2123
- var SQLTranslator, QueryCache;
2153
+ function skipQuoted(sql, i, backslashEscapes) {
2154
+ const c = sql[i];
2155
+ const escapeString = c === "'" && (sql[i - 1] === "E" || sql[i - 1] === "e") && !isIdentChar(sql[i - 2]);
2156
+ const backslash = c !== "`" && (escapeString || backslashEscapes);
2157
+ const n = sql.length;
2158
+ i++;
2159
+ while (i < n) {
2160
+ if (backslash && sql[i] === "\\") {
2161
+ i += 2;
2162
+ continue;
2163
+ }
2164
+ if (sql[i] === c) {
2165
+ if (sql[i + 1] === c) {
2166
+ i += 2;
2167
+ continue;
2168
+ }
2169
+ return i + 1;
2170
+ }
2171
+ i++;
2172
+ }
2173
+ return i;
2174
+ }
2175
+ function skipLineComment(sql, i) {
2176
+ while (i < sql.length && sql[i] !== "\n") i++;
2177
+ return i;
2178
+ }
2179
+ function skipBlockComment(sql, i) {
2180
+ const end = sql.indexOf("*/", i + 2);
2181
+ return end === -1 ? sql.length : end + 2;
2182
+ }
2183
+ function skipDollarQuote(sql, i) {
2184
+ const tag = sql.slice(i).match(/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/);
2185
+ if (!tag) return i;
2186
+ const end = sql.indexOf(tag[0], i + tag[0].length);
2187
+ return end === -1 ? sql.length : end + tag[0].length;
2188
+ }
2189
+ var isIdentChar, SQLTranslator, QueryCache;
2124
2190
  var init_sqlTranslator = __esm({
2125
2191
  "../orm/src/sqlTranslator.ts"() {
2126
2192
  "use strict";
2127
2193
  init_databaseUrl();
2128
2194
  init_point();
2195
+ isIdentChar = (ch) => !!ch && /[A-Za-z0-9_]/.test(ch);
2129
2196
  SQLTranslator = class _SQLTranslator {
2130
2197
  static SPATIAL_ENGINES = /* @__PURE__ */ new Set(["postgres", "postgresql"]);
2131
2198
  static SPATIAL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -2406,47 +2473,26 @@ var init_sqlTranslator = __esm({
2406
2473
  static placeholderPositions(sql, options = {}) {
2407
2474
  const positions = [];
2408
2475
  const n = sql.length;
2409
- const isIdent = (ch) => !!ch && /[A-Za-z0-9_]/.test(ch);
2410
2476
  let i = 0;
2411
2477
  while (i < n) {
2412
2478
  const c = sql[i];
2413
2479
  const next = sql[i + 1];
2414
2480
  if (c === "'" || c === '"' || c === "`") {
2415
- const prev = sql[i - 1];
2416
- const escapeString = c === "'" && (prev === "E" || prev === "e") && !isIdent(sql[i - 2]);
2417
- const backslash = c !== "`" && (escapeString || options.backslashEscapes === true);
2418
- i++;
2419
- while (i < n) {
2420
- if (backslash && sql[i] === "\\") {
2421
- i += 2;
2422
- continue;
2423
- }
2424
- if (sql[i] === c) {
2425
- if (sql[i + 1] === c) {
2426
- i += 2;
2427
- continue;
2428
- }
2429
- i++;
2430
- break;
2431
- }
2432
- i++;
2433
- }
2481
+ i = skipQuoted(sql, i, options.backslashEscapes === true);
2434
2482
  continue;
2435
2483
  }
2436
2484
  if (c === "-" && next === "-") {
2437
- while (i < n && sql[i] !== "\n") i++;
2485
+ i = skipLineComment(sql, i);
2438
2486
  continue;
2439
2487
  }
2440
2488
  if (c === "/" && next === "*") {
2441
- const end = sql.indexOf("*/", i + 2);
2442
- i = end === -1 ? n : end + 2;
2489
+ i = skipBlockComment(sql, i);
2443
2490
  continue;
2444
2491
  }
2445
- if (c === "$" && !isIdent(sql[i - 1])) {
2446
- const tag = sql.slice(i).match(/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/);
2447
- if (tag) {
2448
- const end = sql.indexOf(tag[0], i + tag[0].length);
2449
- i = end === -1 ? n : end + tag[0].length;
2492
+ if (c === "$" && !isIdentChar(sql[i - 1])) {
2493
+ const past = skipDollarQuote(sql, i);
2494
+ if (past !== i) {
2495
+ i = past;
2450
2496
  continue;
2451
2497
  }
2452
2498
  }
@@ -2479,7 +2525,7 @@ var init_sqlTranslator = __esm({
2479
2525
  * → { sql: "INSERT INTO t (x) VALUES (1)", columns: ["id", "name"] }
2480
2526
  */
2481
2527
  static parseReturning(sql) {
2482
- const m = sql.match(/\bRETURNING\s+(.+)$/i);
2528
+ const m = sql.match(/\bRETURNING\s+(\S.*)$/i);
2483
2529
  if (!m) return { sql, columns: [] };
2484
2530
  const columns = m[1].split(",").map((c) => c.trim());
2485
2531
  return {
@@ -2533,7 +2579,12 @@ var init_sqlTranslator = __esm({
2533
2579
  // The `d` flag records group indices, so the head can be sliced at the exact
2534
2580
  // start of the VALUES group rather than by hunting for a parenthesis (the
2535
2581
  // column list has parentheses too).
2536
- static INSERT_VALUES = /^\s*INSERT\s+INTO\s+.+?\s+VALUES\s*\(([^()]*)\)\s*$/dis;
2582
+ static INSERT_VALUES = (
2583
+ // ReDoS-safe: the table ref matches `\S(?:[\s\S]*?\S)?` (bounded by non-space
2584
+ // at both ends) so the separator `\s+` before VALUES can't overlap it the way
2585
+ // the old `.+?\s+` did (polynomial on `INSERT INTO ` + a long space run).
2586
+ /^\s*INSERT\s+INTO\s+\S(?:[\s\S]*?\S)?\s+VALUES\s*\(([^()]*)\)\s*$/dis
2587
+ );
2537
2588
  /**
2538
2589
  * Engines whose lastInsertId reports the FIRST generated id of a multi-row
2539
2590
  * INSERT rather than the last. Verified live, not assumed: a 3-row insert
@@ -7273,7 +7324,7 @@ __export(database_exports, {
7273
7324
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
7274
7325
  function stripTrailingSemicolons(sql) {
7275
7326
  if (!sql) return sql;
7276
- let stripped = sql.replace(/\s+$/, "");
7327
+ let stripped = sql.trimEnd();
7277
7328
  while (stripped.endsWith(";")) {
7278
7329
  stripped = stripped.slice(0, -1).replace(/\s+$/, "");
7279
7330
  }
@@ -7440,6 +7491,13 @@ async function createAdapterFromUrl(url, username, password) {
7440
7491
  }
7441
7492
  async function buildAdapterFromUrl(url, username, password) {
7442
7493
  const parsed = parseDatabaseUrl(url, username, password);
7494
+ const serverConfig = {
7495
+ host: parsed.host ?? void 0,
7496
+ port: parsed.port ?? void 0,
7497
+ user: parsed.username ?? void 0,
7498
+ password: parsed.password ?? void 0,
7499
+ database: parsed.database || void 0
7500
+ };
7443
7501
  switch (parsed.engine) {
7444
7502
  case "sqlite": {
7445
7503
  const { SQLiteAdapter: SQLiteAdapter2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
@@ -7447,49 +7505,25 @@ async function buildAdapterFromUrl(url, username, password) {
7447
7505
  }
7448
7506
  case "postgres": {
7449
7507
  const { PostgresAdapter: PostgresAdapter2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
7450
- const adapter = new PostgresAdapter2({
7451
- host: parsed.host ?? void 0,
7452
- port: parsed.port ?? void 0,
7453
- user: parsed.username ?? void 0,
7454
- password: parsed.password ?? void 0,
7455
- database: parsed.database || void 0
7456
- });
7508
+ const adapter = new PostgresAdapter2(serverConfig);
7457
7509
  await adapter.connect();
7458
7510
  return adapter;
7459
7511
  }
7460
7512
  case "mysql": {
7461
7513
  const { MysqlAdapter: MysqlAdapter2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
7462
- const adapter = new MysqlAdapter2({
7463
- host: parsed.host ?? void 0,
7464
- port: parsed.port ?? void 0,
7465
- user: parsed.username ?? void 0,
7466
- password: parsed.password ?? void 0,
7467
- database: parsed.database || void 0
7468
- });
7514
+ const adapter = new MysqlAdapter2(serverConfig);
7469
7515
  await adapter.connect();
7470
7516
  return adapter;
7471
7517
  }
7472
7518
  case "mssql": {
7473
7519
  const { MssqlAdapter: MssqlAdapter2 } = await Promise.resolve().then(() => (init_mssql(), mssql_exports));
7474
- const adapter = new MssqlAdapter2({
7475
- host: parsed.host ?? void 0,
7476
- port: parsed.port ?? void 0,
7477
- user: parsed.username ?? void 0,
7478
- password: parsed.password ?? void 0,
7479
- database: parsed.database || void 0
7480
- });
7520
+ const adapter = new MssqlAdapter2(serverConfig);
7481
7521
  await adapter.connect();
7482
7522
  return adapter;
7483
7523
  }
7484
7524
  case "firebird": {
7485
7525
  const { FirebirdAdapter: FirebirdAdapter2 } = await Promise.resolve().then(() => (init_firebird(), firebird_exports));
7486
- const adapter = new FirebirdAdapter2({
7487
- host: parsed.host ?? void 0,
7488
- port: parsed.port ?? void 0,
7489
- user: parsed.username ?? void 0,
7490
- password: parsed.password ?? void 0,
7491
- database: parsed.database || void 0
7492
- });
7526
+ const adapter = new FirebirdAdapter2(serverConfig);
7493
7527
  await adapter.connect();
7494
7528
  return adapter;
7495
7529
  }
@@ -10225,9 +10259,11 @@ __export(engine_exports, {
10225
10259
  escapeHtml: () => escapeHtml,
10226
10260
  expressionFormCache: () => expressionFormCache,
10227
10261
  filterChainCache: () => filterChainCache,
10262
+ frondRange: () => frondRange,
10228
10263
  pathParseCache: () => pathParseCache,
10229
10264
  setCspNonceProvider: () => setCspNonceProvider,
10230
- setFormTokenSessionId: () => setFormTokenSessionId
10265
+ setFormTokenSessionId: () => setFormTokenSessionId,
10266
+ setFormTokenSessionProvider: () => setFormTokenSessionProvider
10231
10267
  });
10232
10268
  import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
10233
10269
  import { readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync4, realpathSync } from "node:fs";
@@ -10312,6 +10348,14 @@ function renderDump(value) {
10312
10348
  const escaped = dumped.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10313
10349
  return new SafeString(`<pre>${escaped}</pre>`);
10314
10350
  }
10351
+ function stripTags(input) {
10352
+ let s = input, prev;
10353
+ do {
10354
+ prev = s;
10355
+ s = s.replace(STRIP_TAGS_RE, "");
10356
+ } while (s !== prev);
10357
+ return s;
10358
+ }
10315
10359
  function liveAttr(value) {
10316
10360
  return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
10317
10361
  }
@@ -10329,35 +10373,58 @@ function sweepExpiredCache(cache) {
10329
10373
  if (expiresAt2 <= now) cache.delete(key);
10330
10374
  }
10331
10375
  }
10332
- function tokenize(source) {
10376
+ function extractRawBlocks(source) {
10333
10377
  const rawBlocks = [];
10334
- source = source.replace(RAW_BLOCK_RE, (_match, content) => {
10378
+ let out = "";
10379
+ let pos = 0;
10380
+ RAW_OPEN_RE.lastIndex = 0;
10381
+ let om;
10382
+ while ((om = RAW_OPEN_RE.exec(source)) !== null) {
10383
+ const openStart = om.index;
10384
+ const openEnd = RAW_OPEN_RE.lastIndex;
10385
+ RAW_CLOSE_RE.lastIndex = openEnd;
10386
+ const cm = RAW_CLOSE_RE.exec(source);
10387
+ if (cm === null) break;
10335
10388
  const idx = rawBlocks.length;
10336
- rawBlocks.push(content);
10337
- return `\0RAW_${idx}\0`;
10338
- });
10389
+ rawBlocks.push(source.slice(openEnd, cm.index));
10390
+ out += source.slice(pos, openStart) + `\0RAW_${idx}\0`;
10391
+ pos = RAW_CLOSE_RE.lastIndex;
10392
+ RAW_OPEN_RE.lastIndex = pos;
10393
+ }
10394
+ out += source.slice(pos);
10395
+ return { source: out, rawBlocks };
10396
+ }
10397
+ function tokenize(rawSource) {
10398
+ const { source, rawBlocks } = extractRawBlocks(rawSource);
10339
10399
  const tokens = [];
10400
+ const n = source.length;
10340
10401
  let pos = 0;
10341
- TOKEN_RE.lastIndex = 0;
10342
- let m;
10343
- while ((m = TOKEN_RE.exec(source)) !== null) {
10344
- const start2 = m.index;
10345
- if (start2 > pos) {
10346
- tokens.push(["TEXT", source.slice(pos, start2)]);
10402
+ let scan = 0;
10403
+ const exhausted = { "%}": false, "}}": false, "#}": false };
10404
+ while (scan < n) {
10405
+ const brace = source.indexOf("{", scan);
10406
+ if (brace === -1) break;
10407
+ const kind = TAG_KINDS[source[brace + 1]];
10408
+ if (!kind) {
10409
+ scan = brace + 1;
10410
+ continue;
10347
10411
  }
10348
- const raw = m[0];
10349
- if (raw.startsWith("{#")) {
10350
- tokens.push(["COMMENT", raw]);
10351
- } else if (raw.startsWith("{{")) {
10352
- tokens.push(["VAR", raw]);
10353
- } else if (raw.startsWith("{%")) {
10354
- tokens.push(["BLOCK", raw]);
10412
+ if (exhausted[kind.close]) {
10413
+ scan = brace + 1;
10414
+ continue;
10355
10415
  }
10356
- pos = m.index + raw.length;
10357
- }
10358
- if (pos < source.length) {
10359
- tokens.push(["TEXT", source.slice(pos)]);
10416
+ const close = source.indexOf(kind.close, brace + 2);
10417
+ if (close === -1) {
10418
+ exhausted[kind.close] = true;
10419
+ scan = brace + 1;
10420
+ continue;
10421
+ }
10422
+ if (brace > pos) tokens.push(["TEXT", source.slice(pos, brace)]);
10423
+ tokens.push([kind.type, source.slice(brace, close + 2)]);
10424
+ pos = close + 2;
10425
+ scan = close + 2;
10360
10426
  }
10427
+ if (pos < n) tokens.push(["TEXT", source.slice(pos)]);
10361
10428
  if (rawBlocks.length > 0) {
10362
10429
  for (let i = 0; i < tokens.length; i++) {
10363
10430
  if (tokens[i][0] === "TEXT" && tokens[i][1].includes("\0RAW_")) {
@@ -10683,26 +10750,123 @@ function evalConcatOrComparison(expr, context) {
10683
10750
  }
10684
10751
  return EXPR_NOT_MATCHED;
10685
10752
  }
10753
+ function topLevelPositions(expr) {
10754
+ const positions = [];
10755
+ let inQuote = null;
10756
+ let depth = 0;
10757
+ for (let pos = 0; pos < expr.length; pos++) {
10758
+ const ch = expr[pos];
10759
+ if (inQuote) {
10760
+ if (ch === inQuote) inQuote = null;
10761
+ } else if (ch === '"' || ch === "'") {
10762
+ inQuote = ch;
10763
+ } else if ("([{".includes(ch)) {
10764
+ depth++;
10765
+ } else if (")]}".includes(ch)) {
10766
+ depth--;
10767
+ } else if (depth === 0) {
10768
+ positions.push(pos);
10769
+ }
10770
+ }
10771
+ return positions;
10772
+ }
10773
+ function followsOperand(expr, pos) {
10774
+ return /[\w)\]'"]$/.test(expr.slice(0, pos).trimEnd());
10775
+ }
10776
+ function topLevelArithmeticOperators(expr) {
10777
+ const operators = [];
10778
+ let skipUntil = -1;
10779
+ for (const pos of topLevelPositions(expr)) {
10780
+ if (pos < skipUntil || !"+-*/%".includes(expr[pos])) continue;
10781
+ const pair = expr.slice(pos, pos + 2);
10782
+ const op = pair === "**" || pair === "//" ? pair : expr[pos];
10783
+ skipUntil = pos + op.length;
10784
+ if (followsOperand(expr, pos)) operators.push([pos, op]);
10785
+ }
10786
+ return operators;
10787
+ }
10788
+ function splitArithmetic(expr) {
10789
+ const operators = topLevelArithmeticOperators(expr);
10790
+ for (const level of ARITHMETIC_LEVELS) {
10791
+ const matches = operators.filter(([, op]) => level.includes(op));
10792
+ if (matches.length > 0) return level[0] === "**" ? matches[0] : matches[matches.length - 1];
10793
+ }
10794
+ return null;
10795
+ }
10686
10796
  function evalArithmeticExpression(expr, context) {
10687
- for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
10688
- const pos = findOutsideQuotes(expr, op);
10689
- if (pos >= 0) {
10690
- const left = expr.slice(0, pos).trim();
10691
- const right = expr.slice(pos + op.length).trim();
10692
- const lVal = evalExpr(left, context);
10693
- const rVal = evalExpr(right, context);
10694
- try {
10695
- let lNum = lVal != null ? Number(lVal) : 0;
10696
- let rNum = rVal != null ? Number(rVal) : 0;
10697
- if (isNaN(lNum)) lNum = 0;
10698
- if (isNaN(rNum)) rNum = 0;
10699
- return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
10700
- } catch {
10701
- return null;
10702
- }
10797
+ const split = splitArithmetic(expr);
10798
+ if (!split) return EXPR_NOT_MATCHED;
10799
+ const [pos, op] = split;
10800
+ const lVal = evalExpr(expr.slice(0, pos).trim(), context);
10801
+ const rVal = evalExpr(expr.slice(pos + op.length).trim(), context);
10802
+ let lNum = lVal != null ? Number(lVal) : 0;
10803
+ let rNum = rVal != null ? Number(rVal) : 0;
10804
+ if (isNaN(lNum)) lNum = 0;
10805
+ if (isNaN(rNum)) rNum = 0;
10806
+ return ARITHMETIC_OPERATIONS[op](lNum, rNum);
10807
+ }
10808
+ function frondRange(start2, end, step = 1) {
10809
+ const numeric = (value) => typeof value === "number" || typeof value === "string" && NUMERIC_RE.test(value);
10810
+ if (!numeric(start2) || !numeric(end) || !numeric(step)) return [];
10811
+ const first = Math.trunc(Number(start2));
10812
+ const last = Math.trunc(Number(end));
10813
+ const stride = Math.abs(Math.trunc(Number(step)));
10814
+ if (stride === 0) return [];
10815
+ const values = [];
10816
+ if (first <= last) for (let value = first; value <= last; value += stride) values.push(value);
10817
+ else for (let value = first; value >= last; value -= stride) values.push(value);
10818
+ return values;
10819
+ }
10820
+ function findRangeOperator(expr) {
10821
+ let pos = findOutsideQuotes(expr, "..");
10822
+ while (pos >= 0) {
10823
+ const partOfEllipsis = expr.startsWith("...", pos) || pos > 0 && expr[pos - 1] === ".";
10824
+ if (pos > 0 && !partOfEllipsis) return findOutsideQuotes(expr.slice(pos + 2), "|") >= 0 ? -1 : pos;
10825
+ const next = findOutsideQuotes(expr.slice(pos + 2), "..");
10826
+ pos = next >= 0 ? pos + 2 + next : -1;
10827
+ }
10828
+ return -1;
10829
+ }
10830
+ function evalRangeExpression(expr, context) {
10831
+ const pos = findRangeOperator(expr);
10832
+ if (pos < 0) return EXPR_NOT_MATCHED;
10833
+ return frondRange(evalExpr(expr.slice(0, pos).trim(), context), evalExpr(expr.slice(pos + 2).trim(), context));
10834
+ }
10835
+ function closingBracketIndex(expr) {
10836
+ let depth = 0;
10837
+ let inQuote = null;
10838
+ for (let i = 0; i < expr.length; i++) {
10839
+ const ch = expr[i];
10840
+ if (inQuote) {
10841
+ if (ch === inQuote) inQuote = null;
10842
+ } else if (ch === '"' || ch === "'") {
10843
+ inQuote = ch;
10844
+ } else if ("([{".includes(ch)) {
10845
+ depth++;
10846
+ } else if (")]}".includes(ch) && --depth === 0) {
10847
+ return i;
10703
10848
  }
10704
10849
  }
10705
- return EXPR_NOT_MATCHED;
10850
+ return -1;
10851
+ }
10852
+ function mapLiteralKey(rawKey, context) {
10853
+ const quoted = rawKey.length >= 2 && (rawKey[0] === '"' || rawKey[0] === "'") && rawKey.endsWith(rawKey[0]);
10854
+ if (quoted) return rawKey.slice(1, -1);
10855
+ return BARE_KEY_RE.test(rawKey) ? rawKey : String(evalExpr(rawKey, context));
10856
+ }
10857
+ function evalMapLiteral(inner, context) {
10858
+ const map = {};
10859
+ for (const pair of inner === "" ? [] : splitArgs(inner)) {
10860
+ const colon = findOutsideQuotes(pair, ":");
10861
+ if (colon > 0) map[mapLiteralKey(pair.slice(0, colon).trim(), context)] = evalExpr(pair.slice(colon + 1).trim(), context);
10862
+ }
10863
+ return map;
10864
+ }
10865
+ function evalCollectionLiteral(expr, context) {
10866
+ if (expr[0] !== "[" && expr[0] !== "{" || closingBracketIndex(expr) !== expr.length - 1) return EXPR_NOT_MATCHED;
10867
+ const inner = expr.slice(1, -1).trim();
10868
+ if (expr[0] === "{") return evalMapLiteral(inner, context);
10869
+ return inner === "" ? [] : splitArgs(inner).map((item) => evalExpr(item.trim(), context));
10706
10870
  }
10707
10871
  function evalFilterExpression(expr, context) {
10708
10872
  if (findOutsideQuotes(expr, "|") >= 0) {
@@ -11124,12 +11288,12 @@ function splitArgs(raw) {
11124
11288
  current += ch;
11125
11289
  continue;
11126
11290
  }
11127
- if (ch === "(" || ch === "[") {
11291
+ if (ch === "(" || ch === "[" || ch === "{") {
11128
11292
  depth++;
11129
11293
  current += ch;
11130
11294
  continue;
11131
11295
  }
11132
- if (ch === ")" || ch === "]") {
11296
+ if (ch === ")" || ch === "]" || ch === "}") {
11133
11297
  depth--;
11134
11298
  current += ch;
11135
11299
  continue;
@@ -11294,11 +11458,14 @@ function formatMatch(match, flags, width, prec, type2, arg) {
11294
11458
  return padFormattedValue(formatValue(type2, precision, arg), flags, width);
11295
11459
  }
11296
11460
  function _b64url(data) {
11297
- return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11461
+ return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
11298
11462
  }
11299
11463
  function setFormTokenSessionId(sessionId) {
11300
11464
  _formTokenSessionId = sessionId || "";
11301
11465
  }
11466
+ function setFormTokenSessionProvider(fn) {
11467
+ _formTokenSessionProvider = fn;
11468
+ }
11302
11469
  function setCspNonceProvider(fn) {
11303
11470
  _cspNonceProvider = fn;
11304
11471
  }
@@ -11323,8 +11490,9 @@ function _buildFormTokenJwt(descriptor = "") {
11323
11490
  payload.context = descriptor;
11324
11491
  }
11325
11492
  }
11326
- if (_formTokenSessionId) {
11327
- payload.session_id = _formTokenSessionId;
11493
+ const sessionId = _formTokenSessionProvider() || _formTokenSessionId;
11494
+ if (sessionId) {
11495
+ payload.session_id = sessionId;
11328
11496
  }
11329
11497
  const h2 = _b64url(Buffer.from(JSON.stringify(header)));
11330
11498
  const p = _b64url(Buffer.from(JSON.stringify(payload)));
@@ -11340,7 +11508,7 @@ function _generateFormToken(descriptor = "") {
11340
11508
  function _generateFormTokenValue(descriptor = "") {
11341
11509
  return new SafeString(_buildFormTokenJwt(descriptor));
11342
11510
  }
11343
- 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;
11511
+ 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;
11344
11512
  var init_engine = __esm({
11345
11513
  "../frond/src/engine.ts"() {
11346
11514
  "use strict";
@@ -11442,7 +11610,7 @@ var init_engine = __esm({
11442
11610
  int: (value) => value ? parseInt(String(value), 10) || 0 : 0,
11443
11611
  float: (value) => value ? parseFloat(String(value)) || 0 : 0,
11444
11612
  abs: (value) => typeof value === "number" ? Math.abs(value) : value,
11445
- striptags: (value) => String(value).replace(STRIP_TAGS_RE, ""),
11613
+ striptags: (value) => stripTags(String(value)),
11446
11614
  first: (value) => Array.isArray(value) ? value[0] ?? null : null,
11447
11615
  last: (value) => Array.isArray(value) ? value[value.length - 1] ?? null : null,
11448
11616
  keys: (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : [],
@@ -11456,20 +11624,23 @@ var init_engine = __esm({
11456
11624
  filter: (value) => Array.isArray(value) ? value.filter(Boolean) : value
11457
11625
  };
11458
11626
  FORMAT_RE = /%%|%([-+ 0]*)(\d+)?(?:\.(\d+))?([sdifFeEgGxXob])/g;
11459
- LEADING_WS_RE = /^\s+/;
11460
- TRAILING_WS_RE = /\s+$/;
11461
11627
  THOUSANDS_RE = /\B(?=(\d{3})+(?!\d))/g;
11462
11628
  LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
11463
11629
  LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
11464
11630
  LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
11465
- EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
11466
- EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
11631
+ EXTENDS_RE = /\{%[-\s]*extends\s+["']([^"']+)["']\s*[-]?%\}/;
11632
+ EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["']([^"']+)["']\s*[-]?%\}/g;
11467
11633
  filterChainCache = /* @__PURE__ */ new Map();
11468
11634
  pathParseCache = /* @__PURE__ */ new Map();
11469
11635
  TEMPLATE_CACHE_MAX = 256;
11470
11636
  MEMO_CACHE_MAX = 1024;
11471
- TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
11472
- RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
11637
+ RAW_OPEN_RE = /\{%-?\s*raw\s*-?%\}/g;
11638
+ RAW_CLOSE_RE = /\{%-?\s*endraw\s*-?%\}/g;
11639
+ TAG_KINDS = {
11640
+ "%": { close: "%}", type: "BLOCK" },
11641
+ "{": { close: "}}", type: "VAR" },
11642
+ "#": { close: "#}", type: "COMMENT" }
11643
+ };
11473
11644
  EXPR_NOT_MATCHED = /* @__PURE__ */ Symbol("frond-expression-not-matched");
11474
11645
  ARITHMETIC_OPERATIONS = {
11475
11646
  "+": (left, right) => left + right,
@@ -11480,10 +11651,14 @@ var init_engine = __esm({
11480
11651
  "%": (left, right) => right !== 0 ? left % right : 0,
11481
11652
  "**": (left, right) => left ** right
11482
11653
  };
11654
+ ARITHMETIC_LEVELS = [["+", "-"], ["*", "//", "/", "%"], ["**"]];
11655
+ BARE_KEY_RE = /^[A-Za-z_]\w*$/;
11483
11656
  EXPR_EVALUATORS = [
11657
+ evalCollectionLiteral,
11484
11658
  evalPrimary,
11485
11659
  evalConditional,
11486
11660
  evalConcatOrComparison,
11661
+ evalRangeExpression,
11487
11662
  evalArithmeticExpression,
11488
11663
  evalFilterExpression,
11489
11664
  evalFunctionExpression
@@ -11539,8 +11714,8 @@ var init_engine = __esm({
11539
11714
  },
11540
11715
  title: (v) => String(v).replace(TITLE_WORD_RE, (c) => c.toUpperCase()),
11541
11716
  trim: (v) => String(v).trim(),
11542
- ltrim: (v) => String(v).replace(LEADING_WS_RE, ""),
11543
- rtrim: (v) => String(v).replace(TRAILING_WS_RE, ""),
11717
+ ltrim: (v) => String(v).trimStart(),
11718
+ rtrim: (v) => String(v).trimEnd(),
11544
11719
  length: (v) => {
11545
11720
  if (Array.isArray(v)) return v.length;
11546
11721
  if (typeof v === "string") return v.length;
@@ -11581,7 +11756,7 @@ var init_engine = __esm({
11581
11756
  safe: (v) => v,
11582
11757
  escape: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
11583
11758
  e: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
11584
- striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
11759
+ striptags: (v) => stripTags(String(v)),
11585
11760
  nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
11586
11761
  abs: (v) => typeof v === "number" ? Math.abs(v) : v,
11587
11762
  round: (v, decimals) => {
@@ -11694,6 +11869,7 @@ var init_engine = __esm({
11694
11869
  js_escape: (v) => new SafeString(jsEscape(v))
11695
11870
  };
11696
11871
  _formTokenSessionId = "";
11872
+ _formTokenSessionProvider = () => "";
11697
11873
  _cspNonceProvider = () => "";
11698
11874
  Frond = class _Frond {
11699
11875
  // ── Class-level registries ──────────────────────────────────
@@ -11847,6 +12023,7 @@ var init_engine = __esm({
11847
12023
  this.globals.form_token = (descriptor) => _generateFormToken(descriptor || "");
11848
12024
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
11849
12025
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
12026
+ this.globals.range = frondRange;
11850
12027
  this.globals.dump = (value) => renderDump(value);
11851
12028
  this.globals.csp_nonce = () => _cspNonceProvider();
11852
12029
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
@@ -12183,20 +12360,20 @@ var init_engine = __esm({
12183
12360
  }
12184
12361
  renderVarToken(tokens, index, context, output) {
12185
12362
  const [content, stripB, stripA] = stripTag(tokens[index][1]);
12186
- if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
12363
+ if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].trimEnd();
12187
12364
  const result = this.evalVar(content, context);
12188
12365
  output.push(result !== null && result !== void 0 ? String(result) : "");
12189
12366
  if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
12190
- tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
12367
+ tokens[index + 1] = ["TEXT", tokens[index + 1][1].trimStart()];
12191
12368
  }
12192
12369
  return index + 1;
12193
12370
  }
12194
12371
  renderBlockToken(tokens, index, context, output) {
12195
12372
  const [content, stripB, stripA] = stripTag(tokens[index][1]);
12196
- if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
12373
+ if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].trimEnd();
12197
12374
  const tag = content.split(/\s+/)[0] || "";
12198
12375
  if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
12199
- tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
12376
+ tokens[index + 1] = ["TEXT", tokens[index + 1][1].trimStart()];
12200
12377
  }
12201
12378
  let next;
12202
12379
  if (tag === "if" && this.tagPermitted(tag)) {
@@ -12213,7 +12390,7 @@ var init_engine = __esm({
12213
12390
  next = block.next;
12214
12391
  }
12215
12392
  if (stripA && next < tokens.length && tokens[next][0] === "TEXT") {
12216
- tokens[next] = ["TEXT", tokens[next][1].replace(LEADING_WS_RE, "")];
12393
+ tokens[next] = ["TEXT", tokens[next][1].trimStart()];
12217
12394
  }
12218
12395
  return next;
12219
12396
  }
@@ -12472,7 +12649,7 @@ var init_engine = __esm({
12472
12649
  pushIfBranch(branches, condition, branchTokens, stripBefore) {
12473
12650
  if (stripBefore && branchTokens.length > 0 && branchTokens[branchTokens.length - 1][0] === "TEXT") {
12474
12651
  const last = branchTokens[branchTokens.length - 1];
12475
- branchTokens[branchTokens.length - 1] = ["TEXT", last[1].replace(TRAILING_WS_RE, "")];
12652
+ branchTokens[branchTokens.length - 1] = ["TEXT", last[1].trimEnd()];
12476
12653
  }
12477
12654
  branches.push([condition, branchTokens]);
12478
12655
  }
@@ -12500,7 +12677,7 @@ var init_engine = __esm({
12500
12677
  } else if (tag === "endif") {
12501
12678
  this.pushIfBranch(branches, currentCond, currentTokens, tagStripB);
12502
12679
  if (tagStripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
12503
- tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
12680
+ tokens[i + 1] = ["TEXT", tokens[i + 1][1].trimStart()];
12504
12681
  }
12505
12682
  return { branches, next: i + 1 };
12506
12683
  } else if ((tag === "elseif" || tag === "elif") && depth === 0) {
@@ -13286,7 +13463,7 @@ function tableData(opts) {
13286
13463
  readonly: !opts.editable,
13287
13464
  inline_script: opts.inlineScript,
13288
13465
  table_name: h(opts.tableName),
13289
- table_id_attr: opts.tableId ? ` id="${h(opts.tableId)}"` : ""
13466
+ table_id: opts.tableId ? String(opts.tableId) : ""
13290
13467
  };
13291
13468
  }
13292
13469
  var init_crudTable = __esm({
@@ -13394,13 +13571,13 @@ function renderModalForm(mode, columns, pk, edit) {
13394
13571
  value: "",
13395
13572
  placeholder: h(`Enter ${label.toLowerCase()}`),
13396
13573
  type: "text",
13397
- required_attr: "",
13574
+ required: false,
13398
13575
  input: true
13399
13576
  };
13400
13577
  });
13401
13578
  return renderCrudTemplate("form", {
13402
13579
  wrap: true,
13403
- form_id_attr: ` id="form-${mode}"`,
13580
+ form_id: `form-${mode}`,
13404
13581
  action: "",
13405
13582
  form_method: "POST",
13406
13583
  method_override: null,
@@ -13457,7 +13634,7 @@ function fieldInputShape(field, base) {
13457
13634
  case "text":
13458
13635
  return { ...base, textarea: true };
13459
13636
  case "boolean":
13460
- return { ...base, checkbox: true, checked_attr: value ? " checked" : "" };
13637
+ return { ...base, checkbox: true, checked: !!value };
13461
13638
  case "select":
13462
13639
  return { ...base, select: true, options_html: buildOptions(field.options, value) };
13463
13640
  case "date":
@@ -13480,7 +13657,7 @@ function buildCustomField(field) {
13480
13657
  label: h(String(label)),
13481
13658
  value: h(field.value ?? ""),
13482
13659
  placeholder: "",
13483
- required_attr: field.required ? " required" : ""
13660
+ required: !!field.required
13484
13661
  };
13485
13662
  return fieldInputShape(field, base);
13486
13663
  }
@@ -13488,7 +13665,7 @@ function generateForm(fields, options = {}) {
13488
13665
  const verb = String(options.method ?? "POST").toUpperCase();
13489
13666
  return renderCrudTemplate("form", {
13490
13667
  wrap: true,
13491
- form_id_attr: "",
13668
+ form_id: "",
13492
13669
  action: h(options.action ?? "/"),
13493
13670
  form_method: h(verb),
13494
13671
  method_override: ["PUT", "PATCH", "DELETE"].includes(verb) ? verb : null,
@@ -18139,20 +18316,210 @@ var init_attachment = __esm({
18139
18316
  }
18140
18317
  });
18141
18318
 
18319
+ // ../orm/src/realtime/s3Client.ts
18320
+ import { createHash as createHash4, createHmac as createHmac3 } from "node:crypto";
18321
+ function retryAfterSeconds(raw) {
18322
+ if (raw == null) return null;
18323
+ const n = Number(raw);
18324
+ return Number.isFinite(n) ? Math.max(0, n) : null;
18325
+ }
18326
+ function uriEncode(value, isPath) {
18327
+ let out = "";
18328
+ for (const byte of Buffer.from(value, "utf8")) {
18329
+ const ch = String.fromCharCode(byte);
18330
+ if (UNRESERVED.test(ch) || isPath && ch === "/") out += ch;
18331
+ else out += "%" + byte.toString(16).toUpperCase().padStart(2, "0");
18332
+ }
18333
+ return out;
18334
+ }
18335
+ function sha256Hex2(data) {
18336
+ return createHash4("sha256").update(data).digest("hex");
18337
+ }
18338
+ function hmac(key, msg) {
18339
+ return createHmac3("sha256", key).update(msg, "utf8").digest();
18340
+ }
18341
+ var ALGORITHM, SERVICE, UNRESERVED, MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, S3Error, S3ConfigError, sleep, S3Client;
18342
+ var init_s3Client = __esm({
18343
+ "../orm/src/realtime/s3Client.ts"() {
18344
+ "use strict";
18345
+ ALGORITHM = "AWS4-HMAC-SHA256";
18346
+ SERVICE = "s3";
18347
+ UNRESERVED = /[A-Za-z0-9\-_.~]/;
18348
+ MAX_RETRIES = 2;
18349
+ BACKOFF_BASE = 0.5;
18350
+ BACKOFF_CAP = 20;
18351
+ S3Error = class extends Error {
18352
+ constructor(status2, message) {
18353
+ super(message);
18354
+ this.status = status2;
18355
+ this.name = "S3Error";
18356
+ }
18357
+ status;
18358
+ /** Seconds from a Retry-After header in its delta-seconds form, else null. */
18359
+ retryAfter = null;
18360
+ };
18361
+ S3ConfigError = class extends Error {
18362
+ constructor(message) {
18363
+ super(message);
18364
+ this.name = "S3ConfigError";
18365
+ }
18366
+ };
18367
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18368
+ S3Client = class {
18369
+ scheme;
18370
+ host;
18371
+ // authority (host[:port]) — matches the Host header
18372
+ prefix;
18373
+ // path an endpoint carries (https://host/s3 -> "/s3"), "" if none
18374
+ region;
18375
+ accessKey;
18376
+ secretKey;
18377
+ timeout;
18378
+ constructor(opts = {}) {
18379
+ this.region = opts.region || "us-east-1";
18380
+ this.accessKey = opts.accessKey || "";
18381
+ this.secretKey = opts.secretKey || "";
18382
+ this.timeout = opts.timeout ?? 3e4;
18383
+ let base = opts.endpoint || `https://s3.${this.region}.amazonaws.com`;
18384
+ while (base.endsWith("/")) base = base.slice(0, -1);
18385
+ const m = /^([a-z][a-z0-9+.-]*):\/\/([^/]+)(\/.*)?$/i.exec(base);
18386
+ this.scheme = m ? m[1] : "https";
18387
+ this.host = m ? m[2] : base;
18388
+ let p = m && m[3] ? m[3] : "";
18389
+ while (p.endsWith("/")) p = p.slice(0, -1);
18390
+ this.prefix = p;
18391
+ }
18392
+ // -- public API (the subset S3Storage uses) --------------------------------
18393
+ async putObject(bucket, key, data, contentType = "application/octet-stream") {
18394
+ const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
18395
+ await this.request("PUT", this.canonicalUri(bucket, key), body, { "content-type": contentType });
18396
+ }
18397
+ async getObject(bucket, key) {
18398
+ return (await this.request("GET", this.canonicalUri(bucket, key), Buffer.alloc(0))).body;
18399
+ }
18400
+ async deleteObject(bucket, key) {
18401
+ await this.request("DELETE", this.canonicalUri(bucket, key), Buffer.alloc(0));
18402
+ }
18403
+ async headObject(bucket, key) {
18404
+ await this.request("HEAD", this.canonicalUri(bucket, key), Buffer.alloc(0));
18405
+ }
18406
+ /** Create the bucket (path-style `PUT /<bucket>`); an already-owned bucket (409) is success. */
18407
+ async createBucket(bucket) {
18408
+ try {
18409
+ await this.request("PUT", `${this.prefix}/${uriEncode(bucket, true)}`, Buffer.alloc(0));
18410
+ } catch (e) {
18411
+ if (!(e instanceof S3Error) || e.status !== 409) throw e;
18412
+ }
18413
+ }
18414
+ /** A URL that fetches the object directly, signed via SigV4 query params. */
18415
+ presignedGetUrl(bucket, key, expires = 3600) {
18416
+ this.requireCredentials();
18417
+ const { amzDate, dateStamp } = this.now();
18418
+ const uri = this.canonicalUri(bucket, key);
18419
+ const scope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
18420
+ const query = {
18421
+ "X-Amz-Algorithm": ALGORITHM,
18422
+ "X-Amz-Credential": `${this.accessKey}/${scope}`,
18423
+ "X-Amz-Date": amzDate,
18424
+ "X-Amz-Expires": String(expires),
18425
+ "X-Amz-SignedHeaders": "host"
18426
+ };
18427
+ const canonicalQs = this.canonicalQuery(query);
18428
+ const canonicalRequest = ["GET", uri, canonicalQs, `host:${this.host}
18429
+ `, "host", "UNSIGNED-PAYLOAD"].join("\n");
18430
+ const signature = this.sign(canonicalRequest, amzDate, dateStamp, scope);
18431
+ return `${this.scheme}://${this.host}${uri}?${canonicalQs}&X-Amz-Signature=${signature}`;
18432
+ }
18433
+ // -- signing + transport ----------------------------------------------------
18434
+ requireCredentials() {
18435
+ if (!this.accessKey || !this.secretKey) {
18436
+ throw new S3ConfigError(
18437
+ "S3 credentials are not set: set TINA4_STORAGE_KEY and TINA4_STORAGE_SECRET"
18438
+ );
18439
+ }
18440
+ }
18441
+ now() {
18442
+ const iso2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
18443
+ return { amzDate: iso2, dateStamp: iso2.slice(0, 8) };
18444
+ }
18445
+ canonicalUri(bucket, key) {
18446
+ return `${this.prefix}/${uriEncode(bucket, true)}/${uriEncode(key, true)}`;
18447
+ }
18448
+ canonicalQuery(query) {
18449
+ return Object.keys(query).sort().map((k) => `${uriEncode(k, false)}=${uriEncode(query[k], false)}`).join("&");
18450
+ }
18451
+ signingKey(dateStamp) {
18452
+ const kDate = hmac(`AWS4${this.secretKey}`, dateStamp);
18453
+ const kRegion = hmac(kDate, this.region);
18454
+ const kService = hmac(kRegion, SERVICE);
18455
+ return hmac(kService, "aws4_request");
18456
+ }
18457
+ sign(canonicalRequest, amzDate, dateStamp, scope) {
18458
+ const stringToSign = [ALGORITHM, amzDate, scope, sha256Hex2(canonicalRequest)].join("\n");
18459
+ return createHmac3("sha256", this.signingKey(dateStamp)).update(stringToSign, "utf8").digest("hex");
18460
+ }
18461
+ async request(method, uri, body, extraHeaders = {}) {
18462
+ this.requireCredentials();
18463
+ for (let attempt = 0; ; attempt++) {
18464
+ const last = attempt === MAX_RETRIES;
18465
+ let delay2 = null;
18466
+ try {
18467
+ return await this.send(method, uri, body, extraHeaders);
18468
+ } catch (e) {
18469
+ if (e instanceof S3Error) {
18470
+ if (last || !(e.status === 429 || e.status >= 500)) throw e;
18471
+ delay2 = e.retryAfter;
18472
+ } else {
18473
+ if (last) throw e;
18474
+ delay2 = null;
18475
+ }
18476
+ }
18477
+ if (delay2 == null) delay2 = Math.random() * (BACKOFF_BASE * 2 ** attempt);
18478
+ await sleep(Math.min(delay2, BACKOFF_CAP) * 1e3);
18479
+ }
18480
+ }
18481
+ async send(method, uri, body, extraHeaders = {}) {
18482
+ const { amzDate, dateStamp } = this.now();
18483
+ const payloadHash = sha256Hex2(body);
18484
+ const signed = {
18485
+ host: this.host,
18486
+ "x-amz-content-sha256": payloadHash,
18487
+ "x-amz-date": amzDate
18488
+ };
18489
+ const signedKeys = Object.keys(signed).sort();
18490
+ const canonicalHeaders = signedKeys.map((k) => `${k}:${signed[k]}
18491
+ `).join("");
18492
+ const signedHeaders = signedKeys.join(";");
18493
+ const canonicalRequest = [method, uri, "", canonicalHeaders, signedHeaders, payloadHash].join("\n");
18494
+ const scope = `${dateStamp}/${this.region}/${SERVICE}/aws4_request`;
18495
+ const signature = this.sign(canonicalRequest, amzDate, dateStamp, scope);
18496
+ const authorization = `${ALGORITHM} Credential=${this.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
18497
+ const headers = {
18498
+ Authorization: authorization,
18499
+ "x-amz-content-sha256": payloadHash,
18500
+ "x-amz-date": amzDate,
18501
+ ...extraHeaders
18502
+ };
18503
+ const url = `${this.scheme}://${this.host}${uri}`;
18504
+ const init = { method, headers, redirect: "manual" };
18505
+ if (method === "PUT" || method === "POST") init.body = body;
18506
+ if (AbortSignal.timeout) init.signal = AbortSignal.timeout(this.timeout);
18507
+ const response = await fetch(url, init);
18508
+ if (response.status < 200 || response.status >= 300) {
18509
+ const err = new S3Error(response.status, `${method} ${uri}: HTTP ${response.status}`);
18510
+ err.retryAfter = retryAfterSeconds(response.headers.get("retry-after"));
18511
+ throw err;
18512
+ }
18513
+ return { status: response.status, body: Buffer.from(await response.arrayBuffer()) };
18514
+ }
18515
+ };
18516
+ }
18517
+ });
18518
+
18142
18519
  // ../orm/src/realtime/storage.ts
18143
18520
  import { randomBytes as randomBytes5 } from "node:crypto";
18144
18521
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, statSync as statSync5 } from "node:fs";
18145
18522
  import { resolve as resolve7, sep as sep3 } from "node:path";
18146
- import { createRequire as createRequire6 } from "node:module";
18147
- function loadAwsSdk(packageName) {
18148
- try {
18149
- return require2(packageName);
18150
- } catch (error2) {
18151
- const missing = error2?.code === "MODULE_NOT_FOUND" && String(error2?.message ?? "").includes(`'${packageName}'`);
18152
- if (!missing) throw error2;
18153
- throw optionalPackageMissing("S3Storage", S3_PACKAGES);
18154
- }
18155
- }
18156
18523
  function storageKey(filename = "") {
18157
18524
  let ext = "";
18158
18525
  if (filename && filename.includes(".")) {
@@ -18177,14 +18544,12 @@ function selectStorage(storage) {
18177
18544
  }
18178
18545
  return new LocalStorage();
18179
18546
  }
18180
- var require2, S3_PACKAGES, UNSAFE, LocalStorage, S3Storage;
18547
+ var UNSAFE, LocalStorage, S3Storage;
18181
18548
  var init_storage = __esm({
18182
18549
  "../orm/src/realtime/storage.ts"() {
18183
18550
  "use strict";
18184
18551
  init_src3();
18185
- init_optionalPackage();
18186
- require2 = createRequire6(import.meta.url);
18187
- S3_PACKAGES = ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"];
18552
+ init_s3Client();
18188
18553
  UNSAFE = /[^A-Za-z0-9]/g;
18189
18554
  LocalStorage = class {
18190
18555
  directory;
@@ -18234,51 +18599,36 @@ var init_storage = __esm({
18234
18599
  constructor(opts = {}) {
18235
18600
  this.bucket = opts.bucket || process.env.TINA4_STORAGE_BUCKET || "";
18236
18601
  if (!this.bucket) throw new Error("S3Storage requires TINA4_STORAGE_BUCKET");
18237
- const { S3Client } = loadAwsSdk("@aws-sdk/client-s3");
18238
- loadAwsSdk("@aws-sdk/s3-request-presigner");
18239
- const endpoint = opts.endpoint || process.env.TINA4_STORAGE_URL;
18240
18602
  this.client = new S3Client({
18241
- endpoint: endpoint || void 0,
18242
- forcePathStyle: true,
18603
+ endpoint: opts.endpoint || process.env.TINA4_STORAGE_URL || void 0,
18243
18604
  region: opts.region || process.env.TINA4_STORAGE_REGION || "us-east-1",
18244
- credentials: {
18245
- accessKeyId: opts.key || process.env.TINA4_STORAGE_KEY || "",
18246
- secretAccessKey: opts.secret || process.env.TINA4_STORAGE_SECRET || ""
18247
- }
18605
+ accessKey: opts.key || process.env.TINA4_STORAGE_KEY || "",
18606
+ secretKey: opts.secret || process.env.TINA4_STORAGE_SECRET || ""
18248
18607
  });
18249
18608
  }
18250
18609
  async put(key, data, mime = "application/octet-stream") {
18251
- const { PutObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18252
- await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data, ContentType: mime }));
18610
+ await this.client.putObject(this.bucket, key, data, mime);
18253
18611
  }
18254
18612
  async get(key) {
18255
18613
  try {
18256
- const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18257
- const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
18258
- const chunks = [];
18259
- for await (const chunk of res.Body) chunks.push(Buffer.from(chunk));
18260
- return Buffer.concat(chunks);
18614
+ return await this.client.getObject(this.bucket, key);
18261
18615
  } catch {
18262
18616
  return null;
18263
18617
  }
18264
18618
  }
18265
18619
  async url(key, ttl = 3600) {
18266
- const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18267
- const { getSignedUrl } = loadAwsSdk("@aws-sdk/s3-request-presigner");
18268
- return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn: ttl });
18620
+ return this.client.presignedGetUrl(this.bucket, key, ttl);
18269
18621
  }
18270
18622
  async delete(key) {
18271
18623
  try {
18272
- const { DeleteObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18273
- await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
18624
+ await this.client.deleteObject(this.bucket, key);
18274
18625
  } catch (e) {
18275
18626
  Log.error(`S3Storage delete failed for ${key}: ${e.message}`);
18276
18627
  }
18277
18628
  }
18278
18629
  async exists(key) {
18279
18630
  try {
18280
- const { HeadObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
18281
- await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
18631
+ await this.client.headObject(this.bucket, key);
18282
18632
  return true;
18283
18633
  } catch {
18284
18634
  return false;
@@ -18289,7 +18639,7 @@ var init_storage = __esm({
18289
18639
  });
18290
18640
 
18291
18641
  // ../orm/src/realtime/realtime.ts
18292
- import { createHmac as createHmac3 } from "node:crypto";
18642
+ import { createHmac as createHmac4 } from "node:crypto";
18293
18643
  function iceServers() {
18294
18644
  const stun = process.env.TINA4_RTC_STUN_URLS || DEFAULT_STUN;
18295
18645
  const servers = [{ urls: splitUrls(stun) }];
@@ -18298,14 +18648,14 @@ function iceServers() {
18298
18648
  if (turnUrl && secret) {
18299
18649
  const ttl = parseInt(process.env.TINA4_RTC_TURN_TTL || "3600", 10);
18300
18650
  const username = String(Math.floor(Date.now() / 1e3) + ttl);
18301
- const credential = createHmac3("sha1", secret).update(username).digest("base64");
18651
+ const credential = createHmac4("sha1", secret).update(username).digest("base64");
18302
18652
  servers.push({ urls: splitUrls(turnUrl), username, credential });
18303
18653
  }
18304
18654
  return servers;
18305
18655
  }
18306
18656
  async function realtime(options = {}) {
18307
18657
  const features = options.features ?? ["calls"];
18308
- const raw = (options.prefix ?? "").replace(/^\/+|\/+$/g, "");
18658
+ const raw = trimSlashes(options.prefix ?? "");
18309
18659
  const p = raw ? `/${raw}` : "";
18310
18660
  _authorize = options.authorize;
18311
18661
  if (features.includes("chat") || features.includes("files")) {
@@ -18577,6 +18927,7 @@ var DEFAULT_STUN, _authorize;
18577
18927
  var init_realtime = __esm({
18578
18928
  "../orm/src/realtime/realtime.ts"() {
18579
18929
  "use strict";
18930
+ init_slashTrim();
18580
18931
  init_src3();
18581
18932
  init_workspace();
18582
18933
  init_channel();
@@ -18594,6 +18945,7 @@ var init_realtime2 = __esm({
18594
18945
  "use strict";
18595
18946
  init_realtime();
18596
18947
  init_storage();
18948
+ init_s3Client();
18597
18949
  init_workspace();
18598
18950
  init_channel();
18599
18951
  init_channelMember();
@@ -18959,6 +19311,7 @@ function mayStore(req2, vary, cacheControl, setCookie) {
18959
19311
  if (requestHeader(req2, "authorization") !== void 0) return sharedCacheAllowed(cacheControl);
18960
19312
  if (requestHeader(req2, "cookie") !== void 0) return sharedCacheAllowed(cacheControl);
18961
19313
  if (hasSetCookie(setCookie)) return sharedCacheAllowed(cacheControl);
19314
+ if (req2.session?.isFresh && !req2.session.isFresh()) return sharedCacheAllowed(cacheControl);
18962
19315
  return true;
18963
19316
  }
18964
19317
  function varyMatches(entry, req2) {
@@ -20311,7 +20664,7 @@ function coerceParam(raw, type2) {
20311
20664
  }
20312
20665
  function joinGroupPath(prefix, path10) {
20313
20666
  const full = `${prefix}/${path10.replace(/^\/+/, "")}`;
20314
- const trimmed = `/${full.replace(/^\/+|\/+$/g, "")}`;
20667
+ const trimmed = `/${trimSlashes(full)}`;
20315
20668
  return trimmed.replace(/\/+/g, "/");
20316
20669
  }
20317
20670
  async function resolveStringMiddleware(spec) {
@@ -20376,6 +20729,7 @@ var WsRouteRef, RouteRef, Router, RouteGroup, TINA4_DEFAULT_ROUTER, globalRouter
20376
20729
  var init_router = __esm({
20377
20730
  "../core/src/router.ts"() {
20378
20731
  "use strict";
20732
+ init_slashTrim();
20379
20733
  init_dotenv();
20380
20734
  init_middleware();
20381
20735
  WsRouteRef = class {
@@ -20585,7 +20939,7 @@ var init_router = __esm({
20585
20939
  const direct = this.matchRoute(routes, path10);
20586
20940
  if (direct) return direct;
20587
20941
  if (isTrailingSlashRedirectEnabled() && path10.length > 1 && path10.endsWith("/")) {
20588
- const stripped = path10.replace(/\/+$/, "");
20942
+ const stripped = rtrimSlashes(path10);
20589
20943
  if (stripped.length > 0) {
20590
20944
  const retry = this.matchRoute(routes, stripped);
20591
20945
  if (retry) return retry;
@@ -22261,7 +22615,8 @@ var init_middleware = __esm({
22261
22615
  "Content-Security-Policy": resolveCspHeader(currentCspNonce()),
22262
22616
  "Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
22263
22617
  "X-XSS-Protection": "0",
22264
- "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
22618
+ "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()",
22619
+ "Cross-Origin-Opener-Policy": process.env.TINA4_COOP ?? "same-origin-allow-popups"
22265
22620
  };
22266
22621
  }
22267
22622
  static beforeSecurity(req2, res) {
@@ -22278,7 +22633,7 @@ var init_middleware = __esm({
22278
22633
  if (process.env.TINA4_CSP === void 0) {
22279
22634
  _SecurityHeadersMiddleware.warnCspDefaultOnce();
22280
22635
  }
22281
- for (const name of ["Content-Security-Policy", "Referrer-Policy", "X-XSS-Protection", "Permissions-Policy"]) {
22636
+ for (const name of ["Content-Security-Policy", "Referrer-Policy", "X-XSS-Protection", "Permissions-Policy", "Cross-Origin-Opener-Policy"]) {
22282
22637
  res.header(name, canonical[name]);
22283
22638
  }
22284
22639
  return [req2, res];
@@ -22362,7 +22717,7 @@ var init_middleware = __esm({
22362
22717
  });
22363
22718
 
22364
22719
  // ../core/src/request.ts
22365
- import { mkdirSync as mkdirSync8, realpathSync as realpathSync2, writeFileSync as writeFileSync5 } from "node:fs";
22720
+ import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, ftruncateSync, lstatSync, mkdirSync as mkdirSync8, openSync as openSync3, realpathSync as realpathSync2, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
22366
22721
  import { dirname as dirname7, join as join12 } from "node:path";
22367
22722
  function makeCaseInsensitiveHeaders(raw) {
22368
22723
  return new Proxy(raw, {
@@ -22398,7 +22753,8 @@ function createRequest(req2) {
22398
22753
  try {
22399
22754
  const url = new URL(req2.url ?? "/", `${proto}://${host}`);
22400
22755
  for (const [key, value] of url.searchParams) {
22401
- query[key] = value;
22756
+ if (isUnsafeFieldName(key)) continue;
22757
+ Reflect.set(query, key, value);
22402
22758
  }
22403
22759
  path10 = url.pathname;
22404
22760
  queryString = url.search.replace(/^\?/, "");
@@ -22410,7 +22766,8 @@ function createRequest(req2) {
22410
22766
  queryString = questionIdx >= 0 ? rawTarget.slice(questionIdx + 1) : "";
22411
22767
  try {
22412
22768
  for (const [key, value] of new URLSearchParams(queryString)) {
22413
- query[key] = value;
22769
+ if (isUnsafeFieldName(key)) continue;
22770
+ Reflect.set(query, key, value);
22414
22771
  }
22415
22772
  } catch {
22416
22773
  }
@@ -22429,7 +22786,8 @@ function createRequest(req2) {
22429
22786
  if (cookieHeader) {
22430
22787
  for (const pair of cookieHeader.split(";")) {
22431
22788
  const [k, ...v] = pair.trim().split("=");
22432
- if (k) cookies[k.trim()] = v.join("=").trim();
22789
+ const name = k ? k.trim() : "";
22790
+ if (name && !isUnsafeFieldName(name)) Reflect.set(cookies, name, v.join("=").trim());
22433
22791
  }
22434
22792
  }
22435
22793
  w.cookies = cookies;
@@ -22520,13 +22878,7 @@ async function parseBody(req2) {
22520
22878
  req2.body = str;
22521
22879
  }
22522
22880
  } else if (contentType.includes("application/x-www-form-urlencoded")) {
22523
- const str = raw.toString("utf-8");
22524
- const params = new URLSearchParams(str);
22525
- const obj = {};
22526
- for (const [key, value] of params) {
22527
- obj[key] = value;
22528
- }
22529
- req2.body = obj;
22881
+ req2.body = nestFormFields([...new URLSearchParams(raw.toString("utf-8"))]);
22530
22882
  } else {
22531
22883
  req2.body = raw.toString("utf-8");
22532
22884
  }
@@ -22536,7 +22888,7 @@ function extractBoundary(contentType) {
22536
22888
  return match ? match[1] ?? match[2] : null;
22537
22889
  }
22538
22890
  function parseMultipart(body, boundary) {
22539
- const fields = {};
22891
+ const pairs = [];
22540
22892
  const files = {};
22541
22893
  const delimiter4 = Buffer.from(`--${boundary}`);
22542
22894
  const closeDelimiter = Buffer.from(`--${boundary}--`);
@@ -22544,7 +22896,7 @@ function parseMultipart(body, boundary) {
22544
22896
  const doubleCrlf = Buffer.from("\r\n\r\n");
22545
22897
  let offset = 0;
22546
22898
  const firstIdx = bufferIndexOf(body, delimiter4, offset);
22547
- if (firstIdx === -1) return { fields, files };
22899
+ if (firstIdx === -1) return { fields: {}, files };
22548
22900
  offset = firstIdx + delimiter4.length;
22549
22901
  if (body[offset] === 13 && body[offset + 1] === 10) {
22550
22902
  offset += 2;
@@ -22568,14 +22920,15 @@ function parseMultipart(body, boundary) {
22568
22920
  content: Buffer.from(content),
22569
22921
  size: content.length
22570
22922
  };
22571
- if (files[disposition.name]) {
22923
+ if (isUnsafeFieldName(disposition.name)) {
22924
+ } else if (files[disposition.name]) {
22572
22925
  const existing = files[disposition.name];
22573
- files[disposition.name] = Array.isArray(existing) ? [...existing, file] : [existing, file];
22926
+ Reflect.set(files, disposition.name, Array.isArray(existing) ? [...existing, file] : [existing, file]);
22574
22927
  } else {
22575
- files[disposition.name] = file;
22928
+ Reflect.set(files, disposition.name, file);
22576
22929
  }
22577
22930
  } else if (disposition.name) {
22578
- fields[disposition.name] = content.toString("utf-8");
22931
+ pairs.push([disposition.name, content.toString("utf-8")]);
22579
22932
  }
22580
22933
  offset = nextDelimIdx + delimiter4.length;
22581
22934
  if (body[offset] === 45 && body[offset + 1] === 45) {
@@ -22585,7 +22938,7 @@ function parseMultipart(body, boundary) {
22585
22938
  offset += 2;
22586
22939
  }
22587
22940
  }
22588
- return { fields, files };
22941
+ return { fields: nestFormFields(pairs), files };
22589
22942
  }
22590
22943
  function saveUpload(file, targetDir, filename) {
22591
22944
  const raw = String(filename ?? file.filename ?? "");
@@ -22601,7 +22954,21 @@ function saveUpload(file, targetDir, filename) {
22601
22954
  if (realParent !== realDir) {
22602
22955
  throw new Error(`refusing to write outside ${targetDir}: ${JSON.stringify(raw)}`);
22603
22956
  }
22604
- writeFileSync5(dest, file.content);
22957
+ if (lstatSync(dest, { throwIfNoEntry: false })?.isSymbolicLink()) unlinkSync4(dest);
22958
+ const flags = constants2.O_WRONLY | constants2.O_CREAT | (constants2.O_NOFOLLOW ?? 0);
22959
+ let fd = openSync3(dest, flags, 384);
22960
+ try {
22961
+ if (fstatSync2(fd).nlink > 1) {
22962
+ closeSync3(fd);
22963
+ fd = -1;
22964
+ unlinkSync4(dest);
22965
+ fd = openSync3(dest, flags | constants2.O_EXCL, 384);
22966
+ }
22967
+ ftruncateSync(fd, 0);
22968
+ writeFileSync5(fd, file.content);
22969
+ } finally {
22970
+ if (fd >= 0) closeSync3(fd);
22971
+ }
22605
22972
  return dest;
22606
22973
  }
22607
22974
  function bufferIndexOf(haystack, needle, offset) {
@@ -22626,10 +22993,98 @@ function parseDisposition(headers) {
22626
22993
  };
22627
22994
  }
22628
22995
  function parsePartContentType(headers) {
22629
- const match = headers.match(/Content-Type:\s*(.+?)(?:\r?\n|$)/i);
22630
- return match?.[1]?.trim() ?? null;
22996
+ const match = headers.match(/Content-Type:([^\r\n]*)/i);
22997
+ const value = match?.[1]?.trim();
22998
+ return value ? value : null;
22999
+ }
23000
+ function nestFormFields(pairs) {
23001
+ const kept = pairs.filter(([name]) => !/(^|[[\]])__proto__([[\]]|$)/.test(name));
23002
+ if (kept.length > FORM_FIELD_LIMIT) return Object.fromEntries(kept);
23003
+ const nested = /* @__PURE__ */ new WeakSet();
23004
+ const node = () => {
23005
+ const created = /* @__PURE__ */ Object.create(null);
23006
+ nested.add(created);
23007
+ return created;
23008
+ };
23009
+ const isNode = (value) => typeof value === "object" && value !== null && nested.has(value);
23010
+ const own = (params, key) => Object.prototype.hasOwnProperty.call(params, key) ? params[key] : void 0;
23011
+ const list = (params, key) => {
23012
+ let current = own(params, key);
23013
+ if (current === void 0) {
23014
+ current = [];
23015
+ Reflect.set(params, key, current);
23016
+ }
23017
+ if (!Array.isArray(current)) throw new RangeError(`expected an array for '${key}'`);
23018
+ return current;
23019
+ };
23020
+ const object = (params, key) => {
23021
+ let current = own(params, key);
23022
+ if (current === void 0) {
23023
+ current = node();
23024
+ Reflect.set(params, key, current);
23025
+ }
23026
+ if (!isNode(current)) throw new RangeError(`expected an object for '${key}'`);
23027
+ return current;
23028
+ };
23029
+ const hasKey = (params, key) => {
23030
+ if (key.includes("[]")) return false;
23031
+ let level = params;
23032
+ for (const part of key.split(/[[\]]+/).filter(Boolean)) {
23033
+ if (!isNode(level) || own(level, part) === void 0) return false;
23034
+ level = level[part];
23035
+ }
23036
+ return true;
23037
+ };
23038
+ const splitName = (name, depth) => {
23039
+ if (depth === 0) {
23040
+ const start2 = name.indexOf("[", 1);
23041
+ return start2 === -1 ? [name, ""] : [name.slice(0, start2), name.slice(start2)];
23042
+ }
23043
+ if (name.startsWith("[]")) return ["[]", name.slice(2)];
23044
+ const close = name.indexOf("]", 1);
23045
+ return name.startsWith("[") && close !== -1 ? [name.slice(1, close), name.slice(close + 1)] : [name, ""];
23046
+ };
23047
+ const putListItem = (items, after, value, depth) => {
23048
+ let childKey = after[2] === "[" && after.endsWith("]") ? after.slice(3, -1) : "";
23049
+ if (childKey === "" || childKey.includes("[") || childKey.includes("]")) childKey = after.slice(2);
23050
+ const last = items[items.length - 1];
23051
+ if (isNode(last) && !hasKey(last, childKey)) put2(last, childKey, value, depth + 1);
23052
+ else items.push(put2(node(), childKey, value, depth + 1));
23053
+ };
23054
+ const put2 = (params, name, value, depth) => {
23055
+ const [key, after] = splitName(name, depth);
23056
+ if (key === "") return void 0;
23057
+ if (after === "" && key === "[]" && depth !== 0) return [value];
23058
+ if (after === "") Reflect.set(params, key, value);
23059
+ else if (after === "[") Reflect.set(params, name, value);
23060
+ else if (after === "[]") list(params, key).push(value);
23061
+ else if (after.startsWith("[]")) putListItem(list(params, key), after, value, depth);
23062
+ else put2(object(params, key), after, value, depth + 1);
23063
+ return params;
23064
+ };
23065
+ const tooDeep = (name) => {
23066
+ let count = 0;
23067
+ for (let at = name.indexOf("["); at !== -1; at = name.indexOf("[", at + 1)) {
23068
+ if (++count >= FORM_DEPTH_LIMIT) return true;
23069
+ }
23070
+ return false;
23071
+ };
23072
+ const fields = node();
23073
+ for (const [name, value] of kept) {
23074
+ if (name.length > FORM_NAME_LIMIT || tooDeep(name)) {
23075
+ Reflect.set(fields, name, value);
23076
+ continue;
23077
+ }
23078
+ try {
23079
+ put2(fields, name, value, 0);
23080
+ } catch (error2) {
23081
+ if (!(error2 instanceof RangeError)) throw error2;
23082
+ Reflect.set(fields, name, value);
23083
+ }
23084
+ }
23085
+ return fields;
22631
23086
  }
22632
- var DEFAULT_MAX_UPLOAD_SIZE, warnedLimits, PayloadTooLargeError;
23087
+ var DEFAULT_MAX_UPLOAD_SIZE, warnedLimits, PayloadTooLargeError, FORM_DEPTH_LIMIT, FORM_FIELD_LIMIT, FORM_NAME_LIMIT, isUnsafeFieldName;
22633
23088
  var init_request = __esm({
22634
23089
  "../core/src/request.ts"() {
22635
23090
  "use strict";
@@ -22644,6 +23099,10 @@ var init_request = __esm({
22644
23099
  this.name = "PayloadTooLargeError";
22645
23100
  }
22646
23101
  };
23102
+ FORM_DEPTH_LIMIT = 32;
23103
+ FORM_FIELD_LIMIT = 4096;
23104
+ FORM_NAME_LIMIT = 1024;
23105
+ isUnsafeFieldName = (key) => key === "__proto__" || key === "constructor" || key === "prototype";
22647
23106
  }
22648
23107
  });
22649
23108
 
@@ -23170,7 +23629,7 @@ __export(portTakeover_exports, {
23170
23629
  writePidfile: () => writePidfile
23171
23630
  });
23172
23631
  import { execFileSync } from "node:child_process";
23173
- import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10, unlinkSync as unlinkSync4 } from "node:fs";
23632
+ import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10, unlinkSync as unlinkSync5 } from "node:fs";
23174
23633
  import { join as join14 } from "node:path";
23175
23634
  function isTruthy3(value) {
23176
23635
  return ["true", "1", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
@@ -23224,7 +23683,7 @@ function readPidfile(port, baseDir) {
23224
23683
  }
23225
23684
  function removePidfile(port, baseDir) {
23226
23685
  try {
23227
- unlinkSync4(pidfilePath(port, baseDir));
23686
+ unlinkSync5(pidfilePath(port, baseDir));
23228
23687
  } catch {
23229
23688
  }
23230
23689
  }
@@ -23325,21 +23784,6 @@ var init_portTakeover = __esm({
23325
23784
  }
23326
23785
  });
23327
23786
 
23328
- // ../core/src/etag.ts
23329
- function etagMatches(ifNoneMatch, etag) {
23330
- const strip = (tag) => tag.trim().replace(/^W\//, "");
23331
- const target = strip(etag);
23332
- return ifNoneMatch.split(",").some((candidate) => {
23333
- const trimmed = candidate.trim();
23334
- return trimmed === "*" || strip(trimmed) === target;
23335
- });
23336
- }
23337
- var init_etag = __esm({
23338
- "../core/src/etag.ts"() {
23339
- "use strict";
23340
- }
23341
- });
23342
-
23343
23787
  // ../core/src/sessionHandlers/syncBridge.ts
23344
23788
  import { Worker } from "node:worker_threads";
23345
23789
  function getBridge(key, workerSource, workerData) {
@@ -23699,7 +24143,7 @@ var init_valkeyHandler = __esm({
23699
24143
  });
23700
24144
 
23701
24145
  // ../core/src/sessionHandlers/memcachedHandler.ts
23702
- import { createHash as createHash5 } from "node:crypto";
24146
+ import { createHash as createHash6 } from "node:crypto";
23703
24147
  var MAX_KEY_BYTES, MAX_RELATIVE_EXPTIME, MemcachedSessionHandler;
23704
24148
  var init_memcachedHandler = __esm({
23705
24149
  "../core/src/sessionHandlers/memcachedHandler.ts"() {
@@ -23721,7 +24165,7 @@ var init_memcachedHandler = __esm({
23721
24165
  key(sessionId) {
23722
24166
  const candidate = `${this.prefix}${sessionId}`;
23723
24167
  if (Buffer.byteLength(candidate) > MAX_KEY_BYTES || /[\x00-\x20\x7f]/.test(candidate)) {
23724
- return `${this.prefix}${createHash5("sha256").update(sessionId).digest("hex")}`;
24168
+ return `${this.prefix}${createHash6("sha256").update(sessionId).digest("hex")}`;
23725
24169
  }
23726
24170
  return candidate;
23727
24171
  }
@@ -24187,7 +24631,7 @@ var init_mongoHandler = __esm({
24187
24631
  });
24188
24632
 
24189
24633
  // ../core/src/sessionHandlers/sqlClient.ts
24190
- import { createRequire as createRequire7 } from "node:module";
24634
+ import { createRequire as createRequire6 } from "node:module";
24191
24635
  function driverPath(engine) {
24192
24636
  const packageName = DRIVER_PACKAGE4[engine];
24193
24637
  try {
@@ -24226,7 +24670,7 @@ var init_sqlClient = __esm({
24226
24670
  mssql: "tedious",
24227
24671
  firebird: "node-firebird"
24228
24672
  };
24229
- requireFromHere = createRequire7(import.meta.url);
24673
+ requireFromHere = createRequire6(import.meta.url);
24230
24674
  SQL_WORKER = `
24231
24675
  const target = workerData.target;
24232
24676
  const engine = target.engine;
@@ -24708,14 +25152,18 @@ __export(session_exports, {
24708
25152
  Session: () => Session,
24709
25153
  VALID_SESSION_BACKENDS: () => VALID_SESSION_BACKENDS,
24710
25154
  buildSessionCookie: () => buildSessionCookie,
25155
+ currentFormTokenSessionId: () => currentFormTokenSessionId,
24711
25156
  isSecureScheme: () => isSecureScheme,
24712
25157
  isValidSessionId: () => isValidSessionId,
25158
+ runWithFormTokenSession: () => runWithFormTokenSession,
24713
25159
  sessionCookieName: () => sessionCookieName,
24714
25160
  sessionCookieToSend: () => sessionCookieToSend,
24715
- sessionStrictMode: () => sessionStrictMode
25161
+ sessionStrictMode: () => sessionStrictMode,
25162
+ setFormTokenSession: () => setFormTokenSession
24716
25163
  });
24717
- import { createHash as createHash6, randomBytes as randomBytes6 } from "node:crypto";
24718
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync5, readdirSync as readdirSync6 } from "node:fs";
25164
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "node:async_hooks";
25165
+ import { createHash as createHash7, randomBytes as randomBytes6 } from "node:crypto";
25166
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, readdirSync as readdirSync6 } from "node:fs";
24719
25167
  import { join as join15 } from "node:path";
24720
25168
  function isValidSessionId(sessionId) {
24721
25169
  return typeof sessionId === "string" && SESSION_ID_PATTERN.test(sessionId);
@@ -24781,7 +25229,21 @@ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
24781
25229
  const socketEncrypted = rawReq.socket?.encrypted === true;
24782
25230
  return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
24783
25231
  }
24784
- var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
25232
+ function runWithFormTokenSession(fn) {
25233
+ return sessionStore.run({ session: null }, fn);
25234
+ }
25235
+ function setFormTokenSession(session) {
25236
+ const store = sessionStore.getStore();
25237
+ if (store) store.session = session;
25238
+ }
25239
+ function currentFormTokenSessionId() {
25240
+ const session = sessionStore.getStore()?.session;
25241
+ const sid = session?.getSessionId();
25242
+ if (!session || !sid) return "";
25243
+ if (session.isFresh()) session.set("_tina4_csrf", true);
25244
+ return sid;
25245
+ }
25246
+ var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session, sessionStore;
24785
25247
  var init_session = __esm({
24786
25248
  "../core/src/session.ts"() {
24787
25249
  "use strict";
@@ -24855,7 +25317,7 @@ var init_session = __esm({
24855
25317
  `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.`
24856
25318
  );
24857
25319
  }
24858
- return join15(this.storagePath, `${createHash6("sha256").update(id).digest("hex")}.json`);
25320
+ return join15(this.storagePath, `${createHash7("sha256").update(id).digest("hex")}.json`);
24859
25321
  }
24860
25322
  read(sessionId) {
24861
25323
  const filePath = this.filePath(sessionId);
@@ -24865,7 +25327,7 @@ var init_session = __esm({
24865
25327
  const wrapper = JSON.parse(raw);
24866
25328
  if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
24867
25329
  try {
24868
- unlinkSync5(filePath);
25330
+ unlinkSync6(filePath);
24869
25331
  } catch {
24870
25332
  }
24871
25333
  return null;
@@ -24884,7 +25346,7 @@ var init_session = __esm({
24884
25346
  destroy(sessionId) {
24885
25347
  const filePath = this.filePath(sessionId);
24886
25348
  try {
24887
- if (existsSync12(filePath)) unlinkSync5(filePath);
25349
+ if (existsSync12(filePath)) unlinkSync6(filePath);
24888
25350
  } catch {
24889
25351
  }
24890
25352
  }
@@ -24900,11 +25362,11 @@ var init_session = __esm({
24900
25362
  const raw = readFileSync8(fullPath, "utf-8");
24901
25363
  const wrapper = JSON.parse(raw);
24902
25364
  if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
24903
- unlinkSync5(fullPath);
25365
+ unlinkSync6(fullPath);
24904
25366
  }
24905
25367
  } catch {
24906
25368
  try {
24907
- unlinkSync5(fullPath);
25369
+ unlinkSync6(fullPath);
24908
25370
  } catch {
24909
25371
  }
24910
25372
  }
@@ -25424,12 +25886,28 @@ var init_session = __esm({
25424
25886
  this.loaded = /* @__PURE__ */ new Map();
25425
25887
  }
25426
25888
  };
25889
+ sessionStore = new AsyncLocalStorage4();
25890
+ }
25891
+ });
25892
+
25893
+ // ../core/src/etag.ts
25894
+ function etagMatches(ifNoneMatch, etag) {
25895
+ const strip = (tag) => tag.trim().replace(/^W\//, "");
25896
+ const target = strip(etag);
25897
+ return ifNoneMatch.split(",").some((candidate) => {
25898
+ const trimmed = candidate.trim();
25899
+ return trimmed === "*" || strip(trimmed) === target;
25900
+ });
25901
+ }
25902
+ var init_etag = __esm({
25903
+ "../core/src/etag.ts"() {
25904
+ "use strict";
25427
25905
  }
25428
25906
  });
25429
25907
 
25430
25908
  // ../core/src/dispatchPipeline.ts
25431
25909
  import { gzipSync } from "node:zlib";
25432
- import { createHash as createHash7 } from "node:crypto";
25910
+ import { createHash as createHash8 } from "node:crypto";
25433
25911
  async function resetRequestCaches2() {
25434
25912
  if (_resetRequestCaches === void 0) {
25435
25913
  _resetRequestCaches = Promise.resolve().then(() => (init_src(), src_exports)).then((orm) => orm.resetRequestCaches).catch(() => null);
@@ -25502,7 +25980,7 @@ function applyResponseValidator(rawReq, rawRes, body) {
25502
25980
  if ((rawRes.statusCode || 200) !== 200 || body.length === 0) return false;
25503
25981
  let etag = rawRes.getHeader("etag");
25504
25982
  if (!etag) {
25505
- etag = `"${createHash7("md5").update(body).digest("hex").slice(0, 16)}"`;
25983
+ etag = `"${createHash8("md5").update(body).digest("hex").slice(0, 16)}"`;
25506
25984
  rawRes.setHeader("ETag", etag);
25507
25985
  }
25508
25986
  if (!responseIsNotModified(rawReq, rawRes, String(etag))) return false;
@@ -25577,6 +26055,7 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
25577
26055
  return;
25578
26056
  }
25579
26057
  req2.session = sess;
26058
+ setFormTokenSession(sess);
25580
26059
  const origEnd = rawRes.end.bind(rawRes);
25581
26060
  let finalised = false;
25582
26061
  rawRes.end = function(...args) {
@@ -25605,6 +26084,7 @@ var _resetRequestCaches, COMPRESSIBLE_PREFIXES;
25605
26084
  var init_dispatchPipeline = __esm({
25606
26085
  "../core/src/dispatchPipeline.ts"() {
25607
26086
  "use strict";
26087
+ init_session();
25608
26088
  init_logger();
25609
26089
  init_etag();
25610
26090
  COMPRESSIBLE_PREFIXES = [
@@ -25754,14 +26234,21 @@ function parseStack(stack) {
25754
26234
  const frames = [];
25755
26235
  const lines = stack.split("\n");
25756
26236
  for (const line of lines) {
25757
- let match = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
25758
- if (match) {
25759
- frames.push({ func: match[1], file: match[2], line: parseInt(match[3], 10), column: parseInt(match[4], 10) });
25760
- continue;
26237
+ const t = line.trim();
26238
+ if (!t.startsWith("at ")) continue;
26239
+ const rest = t.slice(3);
26240
+ let func = "{anonymous}";
26241
+ let loc = rest;
26242
+ if (rest.endsWith(")")) {
26243
+ const open = rest.lastIndexOf(" (");
26244
+ if (open >= 0) {
26245
+ func = rest.slice(0, open);
26246
+ loc = rest.slice(open + 2, -1);
26247
+ }
25761
26248
  }
25762
- match = line.match(/^\s*at\s+(.+?):(\d+):(\d+)/);
25763
- if (match) {
25764
- frames.push({ func: "{anonymous}", file: match[1], line: parseInt(match[2], 10), column: parseInt(match[3], 10) });
26249
+ const lc = loc.match(/:(\d+):(\d+)$/);
26250
+ if (lc && lc.index !== void 0) {
26251
+ frames.push({ func, file: loc.slice(0, lc.index), line: parseInt(lc[1], 10), column: parseInt(lc[2], 10) });
25765
26252
  }
25766
26253
  }
25767
26254
  return frames;
@@ -26086,7 +26573,7 @@ var init_rateLimiter = __esm({
26086
26573
  });
26087
26574
 
26088
26575
  // ../core/src/devMailbox.ts
26089
- import { mkdirSync as mkdirSync12, readdirSync as readdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync13 } from "node:fs";
26576
+ import { mkdirSync as mkdirSync12, readdirSync as readdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync7, existsSync as existsSync13 } from "node:fs";
26090
26577
  import { join as join17 } from "node:path";
26091
26578
  import { randomUUID as randomUUID2 } from "node:crypto";
26092
26579
  var DevMailbox;
@@ -26218,7 +26705,7 @@ var init_devMailbox = __esm({
26218
26705
  const filePath = join17(this.mailboxDir, folder, `${msgId}.json`);
26219
26706
  if (existsSync13(filePath)) {
26220
26707
  try {
26221
- unlinkSync6(filePath);
26708
+ unlinkSync7(filePath);
26222
26709
  deleted = true;
26223
26710
  } catch {
26224
26711
  }
@@ -26237,7 +26724,7 @@ var init_devMailbox = __esm({
26237
26724
  if (existsSync13(dir)) {
26238
26725
  const files = readdirSync7(dir).filter((file) => file.endsWith(".json"));
26239
26726
  for (const file of files) {
26240
- unlinkSync6(join17(dir, file));
26727
+ unlinkSync7(join17(dir, file));
26241
26728
  }
26242
26729
  }
26243
26730
  } catch {
@@ -26381,7 +26868,7 @@ function enginePath() {
26381
26868
  function runEngine(target) {
26382
26869
  const binary = enginePath();
26383
26870
  if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
26384
- const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
26871
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json", "--no-history"], {
26385
26872
  encoding: "utf8",
26386
26873
  timeout: 6e4,
26387
26874
  maxBuffer: 64 * 1024 * 1024
@@ -28906,7 +29393,7 @@ import * as fs4 from "node:fs";
28906
29393
  import * as path3 from "node:path";
28907
29394
  import * as os from "node:os";
28908
29395
  import { spawnSync as spawnSync2 } from "node:child_process";
28909
- import { createRequire as createRequire8 } from "node:module";
29396
+ import { createRequire as createRequire7 } from "node:module";
28910
29397
  import { randomBytes as randomBytes7 } from "node:crypto";
28911
29398
  function reqSibling(pkg) {
28912
29399
  return req(`../../${pkg}/src/index.ts`);
@@ -29065,21 +29552,45 @@ function safePath(projectRoot3, relPath) {
29065
29552
  if (resolved !== projectRoot3 && !resolved.startsWith(rootPrefix)) {
29066
29553
  throw new Error(`Path escapes project directory: ${relPath}`);
29067
29554
  }
29068
- let real = null;
29555
+ let realRoot;
29069
29556
  try {
29070
- real = fs4.realpathSync(resolved);
29557
+ realRoot = fs4.realpathSync(projectRoot3);
29071
29558
  } catch {
29072
- real = null;
29559
+ return resolved;
29073
29560
  }
29074
- if (real !== null) {
29075
- const realRoot = fs4.realpathSync(projectRoot3);
29076
- const realPrefix = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
29077
- if (real !== realRoot && !real.startsWith(realPrefix)) {
29078
- throw new Error(`Path escapes project directory: ${relPath}`);
29079
- }
29561
+ const real = realPathOf(resolved);
29562
+ const realPrefix = realRoot.endsWith(path3.sep) ? realRoot : realRoot + path3.sep;
29563
+ if (real === null || real !== realRoot && !real.startsWith(realPrefix)) {
29564
+ throw new Error(`Path escapes project directory: ${relPath}`);
29080
29565
  }
29081
29566
  return resolved;
29082
29567
  }
29568
+ function realPathOf(p, hops = 0) {
29569
+ let probe = p;
29570
+ while (!fs4.existsSync(probe) && !isSymlink(probe) && path3.dirname(probe) !== probe) {
29571
+ probe = path3.dirname(probe);
29572
+ }
29573
+ try {
29574
+ return fs4.realpathSync.native(probe);
29575
+ } catch {
29576
+ if (hops >= 40) return null;
29577
+ let target;
29578
+ try {
29579
+ target = fs4.readlinkSync(probe);
29580
+ } catch {
29581
+ return null;
29582
+ }
29583
+ const base = path3.isAbsolute(target) ? target : path3.dirname(probe) + path3.sep + target;
29584
+ return realPathOf(base + p.slice(probe.length), hops + 1);
29585
+ }
29586
+ }
29587
+ function isSymlink(p) {
29588
+ try {
29589
+ return fs4.lstatSync(p).isSymbolicLink();
29590
+ } catch {
29591
+ return false;
29592
+ }
29593
+ }
29083
29594
  function redactEnv(key, value) {
29084
29595
  const sensitive = ["secret", "password", "token", "key", "credential", "api_key"];
29085
29596
  if (sensitive.some((s) => key.toLowerCase().includes(s))) {
@@ -29924,8 +30435,10 @@ function registerDevTools(server) {
29924
30435
  return { error: `Invalid path ${JSON.stringify(rel)}: ${proseErr}` };
29925
30436
  }
29926
30437
  rel = normalizeCoderPath(projectRoot4, rel);
29927
- const resolved = path3.resolve(projectRoot4, rel);
29928
- if (!resolved.startsWith(projectRoot4)) {
30438
+ let resolved;
30439
+ try {
30440
+ resolved = safePath(projectRoot4, rel);
30441
+ } catch {
29929
30442
  return { error: `Path escapes project directory: ${rel}` };
29930
30443
  }
29931
30444
  if (!fs4.existsSync(resolved) || !fs4.statSync(resolved).isFile()) {
@@ -30222,8 +30735,9 @@ var req, PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNA
30222
30735
  var init_mcp = __esm({
30223
30736
  "../core/src/mcp.ts"() {
30224
30737
  "use strict";
30738
+ init_slashTrim();
30225
30739
  init_version();
30226
- req = createRequire8(import.meta.url);
30740
+ req = createRequire7(import.meta.url);
30227
30741
  PARSE_ERROR = -32700;
30228
30742
  INVALID_REQUEST = -32600;
30229
30743
  METHOD_NOT_FOUND = -32601;
@@ -30248,7 +30762,7 @@ var init_mcp = __esm({
30248
30762
  // threaded, so this in-process map is the whole coordination mechanism.
30249
30763
  _sseChannels = /* @__PURE__ */ new Map();
30250
30764
  constructor(mcpPath, name = "Tina4 MCP", version = "1.0.0") {
30251
- this.path = mcpPath.replace(/\/+$/, "");
30765
+ this.path = rtrimSlashes(mcpPath);
30252
30766
  this.name = name;
30253
30767
  this.version = version;
30254
30768
  _McpServer._instances.push(this);
@@ -32331,7 +32845,7 @@ __export(websocket_exports, {
32331
32845
  wsToken: () => wsToken
32332
32846
  });
32333
32847
  import { createServer as createServer2 } from "node:http";
32334
- import { createHash as createHash8 } from "node:crypto";
32848
+ import { createHash as createHash9 } from "node:crypto";
32335
32849
  import { randomUUID as randomUUID4 } from "node:crypto";
32336
32850
  function buildCloseFrame(code, reason = "") {
32337
32851
  const reasonBytes = Buffer.from(reason, "utf-8");
@@ -32341,7 +32855,7 @@ function buildCloseFrame(code, reason = "") {
32341
32855
  return buildFrame(OP_CLOSE, payload);
32342
32856
  }
32343
32857
  function computeAcceptKey(key) {
32344
- return createHash8("sha1").update(key + MAGIC_STRING).digest("base64");
32858
+ return createHash9("sha1").update(key + MAGIC_STRING).digest("base64");
32345
32859
  }
32346
32860
  function originAllowed(headers) {
32347
32861
  const raw = (process.env.TINA4_WS_ALLOWED_ORIGINS ?? "").trim();
@@ -34032,7 +34546,7 @@ var init_job = __esm({
34032
34546
  });
34033
34547
 
34034
34548
  // ../core/src/queueBackends/liteBackend.ts
34035
- import { mkdirSync as mkdirSync16, readdirSync as readdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync7, existsSync as existsSync20 } from "node:fs";
34549
+ import { mkdirSync as mkdirSync16, readdirSync as readdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync8, existsSync as existsSync20 } from "node:fs";
34036
34550
  import { join as join25 } from "node:path";
34037
34551
  import { randomUUID as randomUUID6 } from "node:crypto";
34038
34552
  var LiteBackend;
@@ -34202,7 +34716,7 @@ var init_liteBackend = __esm({
34202
34716
  }
34203
34717
  if (record.availableAt && record.availableAt > now) continue;
34204
34718
  try {
34205
- unlinkSync7(filePath);
34719
+ unlinkSync8(filePath);
34206
34720
  } catch {
34207
34721
  continue;
34208
34722
  }
@@ -34236,10 +34750,10 @@ var init_liteBackend = __esm({
34236
34750
  job.priority = job.priority ?? 0;
34237
34751
  this.writeReserved(queue, job);
34238
34752
  try {
34239
- unlinkSync7(filePath);
34753
+ unlinkSync8(filePath);
34240
34754
  } catch {
34241
34755
  try {
34242
- unlinkSync7(this.reservedPath(queue, job.id));
34756
+ unlinkSync8(this.reservedPath(queue, job.id));
34243
34757
  } catch {
34244
34758
  }
34245
34759
  continue;
@@ -34261,10 +34775,10 @@ var init_liteBackend = __esm({
34261
34775
  job.priority = job.priority ?? 0;
34262
34776
  this.writeReserved(queue, job);
34263
34777
  try {
34264
- unlinkSync7(filePath);
34778
+ unlinkSync8(filePath);
34265
34779
  } catch {
34266
34780
  try {
34267
- unlinkSync7(this.reservedPath(queue, job.id));
34781
+ unlinkSync8(this.reservedPath(queue, job.id));
34268
34782
  } catch {
34269
34783
  }
34270
34784
  continue;
@@ -34279,7 +34793,7 @@ var init_liteBackend = __esm({
34279
34793
  */
34280
34794
  clearReservation(queue, jobId) {
34281
34795
  try {
34282
- unlinkSync7(this.reservedPath(queue, jobId));
34796
+ unlinkSync8(this.reservedPath(queue, jobId));
34283
34797
  } catch {
34284
34798
  }
34285
34799
  }
@@ -34322,7 +34836,7 @@ var init_liteBackend = __esm({
34322
34836
  try {
34323
34837
  const files = readdirSync12(dir).filter((f) => f.endsWith(".queue-data"));
34324
34838
  for (const file of files) {
34325
- unlinkSync7(join25(dir, file));
34839
+ unlinkSync8(join25(dir, file));
34326
34840
  count++;
34327
34841
  }
34328
34842
  } catch {
@@ -34332,7 +34846,7 @@ var init_liteBackend = __esm({
34332
34846
  if (existsSync20(failedDir)) {
34333
34847
  const files = readdirSync12(failedDir).filter((f) => f.endsWith(".queue-data"));
34334
34848
  for (const file of files) {
34335
- unlinkSync7(join25(failedDir, file));
34849
+ unlinkSync8(join25(failedDir, file));
34336
34850
  count++;
34337
34851
  }
34338
34852
  }
@@ -34343,7 +34857,7 @@ var init_liteBackend = __esm({
34343
34857
  if (existsSync20(reservedDir)) {
34344
34858
  const files = readdirSync12(reservedDir).filter((f) => f.endsWith(".queue-data"));
34345
34859
  for (const file of files) {
34346
- unlinkSync7(join25(reservedDir, file));
34860
+ unlinkSync8(join25(reservedDir, file));
34347
34861
  count++;
34348
34862
  }
34349
34863
  }
@@ -34401,7 +34915,7 @@ var init_liteBackend = __esm({
34401
34915
  const prefix = this.nextPrefix();
34402
34916
  const queueDir = join25(this.basePath, q);
34403
34917
  writeFileSync12(join25(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
34404
- unlinkSync7(filePath);
34918
+ unlinkSync8(filePath);
34405
34919
  return true;
34406
34920
  }
34407
34921
  }
@@ -34437,7 +34951,7 @@ var init_liteBackend = __esm({
34437
34951
  const files = readdirSync12(failedDir).filter((f) => f.endsWith(".queue-data"));
34438
34952
  for (const file of files) {
34439
34953
  try {
34440
- unlinkSync7(join25(failedDir, file));
34954
+ unlinkSync8(join25(failedDir, file));
34441
34955
  count++;
34442
34956
  } catch {
34443
34957
  }
@@ -34452,7 +34966,7 @@ var init_liteBackend = __esm({
34452
34966
  try {
34453
34967
  const job = JSON.parse(readFileSync16(join25(dir, file), "utf-8"));
34454
34968
  if (job.status === status2) {
34455
- unlinkSync7(join25(dir, file));
34969
+ unlinkSync8(join25(dir, file));
34456
34970
  count++;
34457
34971
  }
34458
34972
  } catch {
@@ -34487,7 +35001,7 @@ var init_liteBackend = __esm({
34487
35001
  job.delayUntil = null;
34488
35002
  const prefix = this.nextPrefix();
34489
35003
  writeFileSync12(join25(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
34490
- unlinkSync7(filePath);
35004
+ unlinkSync8(filePath);
34491
35005
  count++;
34492
35006
  } catch {
34493
35007
  }
@@ -34519,10 +35033,10 @@ var init_liteBackend = __esm({
34519
35033
  job.priority = job.priority ?? 0;
34520
35034
  this.writeReserved(queue, job);
34521
35035
  try {
34522
- unlinkSync7(filePath);
35036
+ unlinkSync8(filePath);
34523
35037
  } catch {
34524
35038
  try {
34525
- unlinkSync7(this.reservedPath(queue, job.id));
35039
+ unlinkSync8(this.reservedPath(queue, job.id));
34526
35040
  } catch {
34527
35041
  }
34528
35042
  continue;
@@ -34624,7 +35138,7 @@ var init_liteBackend = __esm({
34624
35138
  retryJob(queue, job, delaySeconds) {
34625
35139
  this.clearReservation(queue, job.id);
34626
35140
  try {
34627
- unlinkSync7(join25(this.ensureFailedDir(queue), `${job.id}.queue-data`));
35141
+ unlinkSync8(join25(this.ensureFailedDir(queue), `${job.id}.queue-data`));
34628
35142
  } catch {
34629
35143
  }
34630
35144
  job.attempts = (job.attempts || 0) + 1;
@@ -35174,7 +35688,8 @@ function archiveDir() {
35174
35688
  return p;
35175
35689
  }
35176
35690
  function slugify(title) {
35177
- const slug = (title || "").trim().toLowerCase().replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
35691
+ const dashed = (title || "").trim().toLowerCase().replace(/[^A-Za-z0-9_-]+/g, "-");
35692
+ const slug = trimChar(dashed, 45);
35178
35693
  return slug.slice(0, 80) || `plan-${Math.floor(Date.now() / 1e3)}`;
35179
35694
  }
35180
35695
  function parse(text) {
@@ -35311,6 +35826,7 @@ var PLAN_DIR, CURRENT_FILE, ARCHIVE_SUBDIR, STEP_RE, Plan;
35311
35826
  var init_plan = __esm({
35312
35827
  "../core/src/plan.ts"() {
35313
35828
  "use strict";
35829
+ init_slashTrim();
35314
35830
  PLAN_DIR = "plan";
35315
35831
  CURRENT_FILE = ".current";
35316
35832
  ARCHIVE_SUBDIR = "done";
@@ -35582,7 +36098,7 @@ var init_plan = __esm({
35582
36098
  const existing = (currentPlan.steps || []).map((s) => s.text || "");
35583
36099
  const title = currentPlan.title || target;
35584
36100
  const goal = currentPlan.goal || "";
35585
- const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11437/api/chat";
36101
+ const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11435/api/chat";
35586
36102
  const aiModel = process.env.TINA4_AI_MODEL || "qwen2.5-coder:14b";
35587
36103
  const prompts = fleshPrompt(title, goal, existing, prompt);
35588
36104
  const fetched = await fetchFleshReply(aiUrl, aiModel, prompts);
@@ -36550,7 +37066,7 @@ function relativePath(absPath, projectRoot3, frameworkRoots) {
36550
37066
  }
36551
37067
  function tokenise(text) {
36552
37068
  if (!text) return [];
36553
- const expanded = text.replace(CAMEL_SPLIT, (_m, a, b, c, d) => a ? `${a} ${b}` : `${c} ${d}`).toLowerCase();
37069
+ const expanded = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ").toLowerCase();
36554
37070
  return expanded.split(/[\s_\-./:,;()\[\]{}\\]+/).filter((p) => p.length > 0);
36555
37071
  }
36556
37072
  function summariseDoc(doc) {
@@ -36781,7 +37297,7 @@ function returnFromSignature(signature) {
36781
37297
  const rest = signature.slice(close + 1).trim();
36782
37298
  return rest.startsWith(":") ? rest.slice(1).trim() : "";
36783
37299
  }
36784
- var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
37300
+ var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
36785
37301
  var init_docs = __esm({
36786
37302
  "../core/src/docs.ts"() {
36787
37303
  "use strict";
@@ -36920,7 +37436,6 @@ var init_docs = __esm({
36920
37436
  __FILENAME = fileURLToPath7(import.meta.url);
36921
37437
  __DIRNAME = path8.dirname(__FILENAME);
36922
37438
  USER_DIRS = ["src/orm", "src/routes", "src/app", "src/services", "src/models"];
36923
- CAMEL_SPLIT = /([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
36924
37439
  FENCE_RE = new RegExp(
36925
37440
  String.fromCharCode(96, 96, 96) + "[a-zA-Z0-9_+-]*\\n([\\s\\S]*?)\\n" + String.fromCharCode(96, 96, 96),
36926
37441
  "g"
@@ -37294,7 +37809,7 @@ ${end}
37294
37809
 
37295
37810
  // ../core/src/devAdmin.ts
37296
37811
  import { cpus as osCpus } from "node:os";
37297
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync24, readdirSync as readdirSync16, mkdirSync as mkdirSync19, copyFileSync as copyFileSync3, statSync as statSync18, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
37812
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync24, readdirSync as readdirSync16, mkdirSync as mkdirSync19, copyFileSync as copyFileSync3, statSync as statSync18, 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";
37298
37813
  import { join as join29, dirname as dirname15, resolve as resolve17, relative as relative9, sep as sep8, basename as basename6 } from "node:path";
37299
37814
  import { fileURLToPath as fileURLToPath8 } from "node:url";
37300
37815
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
@@ -37493,8 +38008,41 @@ function quoteTableRef(name) {
37493
38008
  }
37494
38009
  return parts.map((p) => `"${p}"`).join(".");
37495
38010
  }
38011
+ function parseSeedRequest(url, body) {
38012
+ const b = body ?? {};
38013
+ const table2 = String(url.searchParams.get("table") ?? b.table ?? "");
38014
+ const count = parseInt(String(url.searchParams.get("count") ?? b.count ?? "10"), 10) || 10;
38015
+ const seedRaw = url.searchParams.get("seed") ?? b.seed;
38016
+ const seed = seedRaw !== void 0 && seedRaw !== null && String(seedRaw) !== "" ? Number.isNaN(parseInt(String(seedRaw), 10)) ? void 0 : parseInt(String(seedRaw), 10) : void 0;
38017
+ const clear = String(url.searchParams.get("clear") ?? b.clear ?? "") === "true" || b.clear === true;
38018
+ const strict = String(url.searchParams.get("strict") ?? b.strict ?? "") === "true" || b.strict === true;
38019
+ return { table: table2, count, seed, clear, strict };
38020
+ }
38021
+ function buildSeedFieldMap(columns, fake) {
38022
+ const rules = [
38023
+ [(n) => n.includes("email"), () => fake.email()],
38024
+ [(n) => n.includes("name"), () => fake.name()],
38025
+ [(n) => n.includes("phone"), () => fake.phone()],
38026
+ [(n) => n.includes("address") || n.includes("city") || n.includes("country"), () => fake.address()],
38027
+ [(n) => n.includes("url") || n.includes("website"), () => fake.url()],
38028
+ [(_n, t) => t.includes("int"), () => fake.integer(1, 1e3)],
38029
+ [(_n, t) => /real|float|double|numeric|decimal/.test(t), () => fake.numeric(0, 1e3, 2)],
38030
+ [(_n, t) => t.includes("bool"), () => fake.boolean()],
38031
+ [(_n, t) => t.includes("date") || t.includes("time"), () => fake.date()]
38032
+ ];
38033
+ const sentence = () => fake.sentence(3);
38034
+ const fieldMap = {};
38035
+ for (const col of columns) {
38036
+ if (col.name.toLowerCase() === "id" || col.primaryKey === true) continue;
38037
+ const name = col.name.toLowerCase();
38038
+ const type2 = col.type.toLowerCase();
38039
+ const rule = rules.find(([matches]) => matches(name, type2));
38040
+ fieldMap[col.name] = rule ? rule[1] : sentence;
38041
+ }
38042
+ return fieldMap;
38043
+ }
37496
38044
  function supervisorBaseUrl() {
37497
- const explicit = (process.env.TINA4_SUPERVISOR_URL ?? "").replace(/\/+$/, "");
38045
+ const explicit = rtrimSlashes(process.env.TINA4_SUPERVISOR_URL ?? "");
37498
38046
  if (explicit) return explicit;
37499
38047
  const agentPort = (process.env.TINA4_AGENT_PORT ?? "").trim();
37500
38048
  if (/^\d+$/.test(agentPort)) return `http://127.0.0.1:${parseInt(agentPort, 10)}`;
@@ -37502,6 +38050,42 @@ function supervisorBaseUrl() {
37502
38050
  if (/^\d+$/.test(fwPort)) return `http://127.0.0.1:${parseInt(fwPort, 10) + 2e3}`;
37503
38051
  return "http://127.0.0.1:9145";
37504
38052
  }
38053
+ function buildSupervisorBody(method, body) {
38054
+ if (method !== "POST" && method !== "PUT" && method !== "PATCH" && method !== "DELETE") return void 0;
38055
+ if (body === void 0 || body === null) return void 0;
38056
+ if (typeof body === "string") return body;
38057
+ if (typeof body !== "object") return void 0;
38058
+ let outBody = body;
38059
+ if (!Array.isArray(body)) {
38060
+ const pf = body.plan_file;
38061
+ if (typeof pf === "string" && pf && !pf.includes("/")) outBody = { ...body, plan_file: `plan/${pf}` };
38062
+ }
38063
+ return JSON.stringify(outBody);
38064
+ }
38065
+ async function streamEventStream(res, upstream, timer) {
38066
+ res.raw.writeHead(upstream.status || 200, {
38067
+ "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream",
38068
+ "Cache-Control": "no-cache",
38069
+ Connection: "keep-alive"
38070
+ });
38071
+ if (typeof res.raw.flushHeaders === "function") res.raw.flushHeaders();
38072
+ if (!upstream.body) {
38073
+ res.raw.end();
38074
+ clearTimeout(timer);
38075
+ return;
38076
+ }
38077
+ const reader = upstream.body.getReader();
38078
+ try {
38079
+ while (true) {
38080
+ const { done, value } = await reader.read();
38081
+ if (done) break;
38082
+ if (value) res.raw.write(Buffer.from(value));
38083
+ }
38084
+ } finally {
38085
+ clearTimeout(timer);
38086
+ res.raw.end();
38087
+ }
38088
+ }
37505
38089
  async function proxyToSupervisor(req2, res, downstreamPath) {
37506
38090
  const base = supervisorBaseUrl();
37507
38091
  let qs = "";
@@ -37512,24 +38096,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
37512
38096
  }
37513
38097
  const target = `${base}${downstreamPath}${qs}`;
37514
38098
  const method = (req2.method ?? "GET").toUpperCase();
37515
- let bodyText;
37516
- if (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") {
37517
- const body = req2.body;
37518
- if (body !== void 0 && body !== null) {
37519
- if (typeof body === "string") {
37520
- bodyText = body;
37521
- } else if (typeof body === "object") {
37522
- let outBody = body;
37523
- if (!Array.isArray(body)) {
37524
- const pf = body.plan_file;
37525
- if (typeof pf === "string" && pf && !pf.includes("/")) {
37526
- outBody = { ...body, plan_file: `plan/${pf}` };
37527
- }
37528
- }
37529
- bodyText = JSON.stringify(outBody);
37530
- }
37531
- }
37532
- }
38099
+ const bodyText = buildSupervisorBody(method, req2.body);
37533
38100
  const timeoutMs = downstreamPath === "/execute" || downstreamPath === "/chat" ? 6e5 : 3e4;
37534
38101
  const ctrl = new AbortController();
37535
38102
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -37555,30 +38122,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
37555
38122
  }
37556
38123
  const ct = (upstream.headers.get("content-type") ?? "").toLowerCase();
37557
38124
  if (ct.includes("text/event-stream")) {
37558
- res.raw.writeHead(upstream.status || 200, {
37559
- "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream",
37560
- "Cache-Control": "no-cache",
37561
- Connection: "keep-alive"
37562
- });
37563
- if (typeof res.raw.flushHeaders === "function") {
37564
- res.raw.flushHeaders();
37565
- }
37566
- if (!upstream.body) {
37567
- res.raw.end();
37568
- clearTimeout(timer);
37569
- return;
37570
- }
37571
- const reader = upstream.body.getReader();
37572
- try {
37573
- while (true) {
37574
- const { done, value } = await reader.read();
37575
- if (done) break;
37576
- if (value) res.raw.write(Buffer.from(value));
37577
- }
37578
- } finally {
37579
- clearTimeout(timer);
37580
- res.raw.end();
37581
- }
38125
+ await streamEventStream(res, upstream, timer);
37582
38126
  return;
37583
38127
  }
37584
38128
  clearTimeout(timer);
@@ -37695,13 +38239,13 @@ function handleGalleryDeploy(router) {
37695
38239
  res.json({ deployed: name, files: copied });
37696
38240
  };
37697
38241
  }
37698
- function realPathOf(target) {
38242
+ function realPathOf2(target) {
37699
38243
  let existing = target;
37700
38244
  const tail = [];
37701
38245
  for (; ; ) {
37702
38246
  let present = false;
37703
38247
  try {
37704
- lstatSync(existing);
38248
+ lstatSync3(existing);
37705
38249
  present = true;
37706
38250
  } catch {
37707
38251
  }
@@ -37713,9 +38257,9 @@ function realPathOf(target) {
37713
38257
  }
37714
38258
  function safeJoin(projectRoot3, rel, extraRoots = []) {
37715
38259
  try {
37716
- const root = realPathOf(resolve17(projectRoot3));
37717
- const resolved = realPathOf(resolve17(root, rel));
37718
- const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf(resolve17(r)))];
38260
+ const root = realPathOf2(resolve17(projectRoot3));
38261
+ const resolved = realPathOf2(resolve17(root, rel));
38262
+ const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf2(resolve17(r)))];
37719
38263
  for (const r of roots) {
37720
38264
  if (resolved === r || resolved.startsWith(r.endsWith(sep8) ? r : r + sep8)) return resolved;
37721
38265
  }
@@ -37724,7 +38268,7 @@ function safeJoin(projectRoot3, rel, extraRoots = []) {
37724
38268
  return null;
37725
38269
  }
37726
38270
  function resolvedRelative(projectRoot3, resolvedPath) {
37727
- return relative9(realPathOf(resolve17(projectRoot3)), resolvedPath);
38271
+ return relative9(realPathOf2(resolve17(projectRoot3)), resolvedPath);
37728
38272
  }
37729
38273
  function devFilesHidden(name) {
37730
38274
  if (DEV_FILES_IGNORED.has(name)) return true;
@@ -37752,7 +38296,7 @@ async function devGitInfo(root) {
37752
38296
  if (!inside || inside.trim() !== "true") return empty;
37753
38297
  const branch = (git(["rev-parse", "--abbrev-ref", "HEAD"]) ?? "").trim();
37754
38298
  const topRaw = git(["rev-parse", "--show-toplevel"]);
37755
- const gitRoot = topRaw && topRaw.trim() !== "" ? topRaw.trim().replace(/\\/g, "/").replace(/\/+$/, "") : null;
38299
+ const gitRoot = topRaw && topRaw.trim() !== "" ? rtrimSlashes(topRaw.trim().replace(/\\/g, "/")) : null;
37756
38300
  const status2 = /* @__PURE__ */ new Map();
37757
38301
  const porcelain = git(["status", "--porcelain", "-uall"]);
37758
38302
  if (porcelain) {
@@ -37771,6 +38315,44 @@ async function devGitInfo(root) {
37771
38315
  return empty;
37772
38316
  }
37773
38317
  }
38318
+ function devGitLabel(gitPath, isDir, gitStatus) {
38319
+ const code = gitStatus.get(gitPath);
38320
+ if (code !== void 0) return devGitStatusLabel(code);
38321
+ if (isDir) {
38322
+ const prefix = gitPath + "/";
38323
+ for (const [gf, gc] of gitStatus) {
38324
+ if (gf.startsWith(prefix)) return gc === "??" ? "untracked" : "modified";
38325
+ }
38326
+ }
38327
+ return "clean";
38328
+ }
38329
+ function devDirHasVisibleChild(full) {
38330
+ try {
38331
+ for (const c of readdirSync16(full)) {
38332
+ if (!devFilesHidden(c)) return true;
38333
+ }
38334
+ } catch {
38335
+ }
38336
+ return false;
38337
+ }
38338
+ function buildFileEntry(name, full, entryRel, cwdInGit, gitStatus) {
38339
+ let isDir = false;
38340
+ let size = null;
38341
+ try {
38342
+ const st = statSync18(full);
38343
+ isDir = st.isDirectory();
38344
+ if (!isDir) size = st.size;
38345
+ } catch {
38346
+ }
38347
+ return {
38348
+ name,
38349
+ path: entryRel,
38350
+ is_dir: isDir,
38351
+ has_children: isDir ? devDirHasVisibleChild(full) : null,
38352
+ git_status: devGitLabel(cwdInGit + entryRel, isDir, gitStatus),
38353
+ size
38354
+ };
38355
+ }
37774
38356
  function devAdminLanguage(rel) {
37775
38357
  const base = (rel.split(/[\\/]/).pop() ?? "").toLowerCase();
37776
38358
  if (base === "dockerfile" || base === "dockerfile.dev" || base === "dockerfile.prod") {
@@ -37785,14 +38367,14 @@ function devAdminLanguage(rel) {
37785
38367
  function readDevFile(target) {
37786
38368
  let fd;
37787
38369
  try {
37788
- fd = openSync4(target, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
37789
- if (!fstatSync2(fd).isFile()) return null;
38370
+ fd = openSync5(target, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
38371
+ if (!fstatSync3(fd).isFile()) return null;
37790
38372
  return readFileSync20(fd);
37791
38373
  } catch (error2) {
37792
38374
  if (["ENOENT", "ENOTDIR", "ELOOP"].includes(error2.code ?? "")) return null;
37793
38375
  throw error2;
37794
38376
  } finally {
37795
- if (fd !== void 0) closeSync4(fd);
38377
+ if (fd !== void 0) closeSync5(fd);
37796
38378
  }
37797
38379
  }
37798
38380
  function mcpSecureEqual(expected, provided) {
@@ -38019,10 +38601,11 @@ function toolbarJs() {
38019
38601
  connect();
38020
38602
  })();`;
38021
38603
  }
38022
- 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;
38604
+ 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;
38023
38605
  var init_devAdmin = __esm({
38024
38606
  "../core/src/devAdmin.ts"() {
38025
38607
  "use strict";
38608
+ init_slashTrim();
38026
38609
  init_devMailbox();
38027
38610
  init_upstreamResponse();
38028
38611
  init_dotenv();
@@ -38489,6 +39072,7 @@ var init_devAdmin = __esm({
38489
39072
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
38490
39073
  // JS asset
38491
39074
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
39075
+ { method: "GET", pattern: "/__dev/split", handler: handleDevAdminSplit },
38492
39076
  // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
38493
39077
  // CSP-clean (no inline style=, onclick=, or <script>) under a strict
38494
39078
  // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
@@ -38855,13 +39439,7 @@ var init_devAdmin = __esm({
38855
39439
  };
38856
39440
  handleSeed = async (req2, res) => {
38857
39441
  const url = new URL(req2.url ?? "/", "http://localhost");
38858
- const body = req2.body ?? {};
38859
- const table2 = url.searchParams.get("table") ?? body?.table ?? "";
38860
- const count = parseInt(String(url.searchParams.get("count") ?? body?.count ?? "10"), 10) || 10;
38861
- const seedRaw = url.searchParams.get("seed") ?? body?.seed;
38862
- const seed = seedRaw !== void 0 && seedRaw !== null && String(seedRaw) !== "" ? Number.isNaN(parseInt(String(seedRaw), 10)) ? void 0 : parseInt(String(seedRaw), 10) : void 0;
38863
- const clear = String(url.searchParams.get("clear") ?? body?.clear ?? "") === "true" || body?.clear === true;
38864
- const strict = String(url.searchParams.get("strict") ?? body?.strict ?? "") === "true" || body?.strict === true;
39442
+ const { table: table2, count, seed, clear, strict } = parseSeedRequest(url, req2.body);
38865
39443
  if (!table2) {
38866
39444
  res.json({ error: "Missing table parameter" });
38867
39445
  return;
@@ -38876,33 +39454,7 @@ var init_devAdmin = __esm({
38876
39454
  res.json({ error: `Table '${table2}' not found or has no columns` });
38877
39455
  return;
38878
39456
  }
38879
- const fieldMap = {};
38880
- for (const col of columns) {
38881
- const name = col.name.toLowerCase();
38882
- const type2 = col.type.toLowerCase();
38883
- if (name === "id" || col.primaryKey === true) continue;
38884
- if (name.includes("email")) {
38885
- fieldMap[col.name] = () => fake.email();
38886
- } else if (name.includes("name")) {
38887
- fieldMap[col.name] = () => fake.name();
38888
- } else if (name.includes("phone")) {
38889
- fieldMap[col.name] = () => fake.phone();
38890
- } else if (name.includes("address") || name.includes("city") || name.includes("country")) {
38891
- fieldMap[col.name] = () => fake.address();
38892
- } else if (name.includes("url") || name.includes("website")) {
38893
- fieldMap[col.name] = () => fake.url();
38894
- } else if (type2.includes("int")) {
38895
- fieldMap[col.name] = () => fake.integer(1, 1e3);
38896
- } else if (type2.includes("real") || type2.includes("float") || type2.includes("double") || type2.includes("numeric") || type2.includes("decimal")) {
38897
- fieldMap[col.name] = () => fake.numeric(0, 1e3, 2);
38898
- } else if (type2.includes("bool")) {
38899
- fieldMap[col.name] = () => fake.boolean();
38900
- } else if (type2.includes("date") || type2.includes("time")) {
38901
- fieldMap[col.name] = () => fake.date();
38902
- } else {
38903
- fieldMap[col.name] = () => fake.sentence(3);
38904
- }
38905
- }
39457
+ const fieldMap = buildSeedFieldMap(columns, fake);
38906
39458
  const summary = await seedTable2(db, table2, count, fieldMap, void 0, { clear, strict });
38907
39459
  res.json({ seeded: summary.seeded, failed: summary.failed, errors: summary.errors, table: table2 });
38908
39460
  } catch (e) {
@@ -39075,9 +39627,9 @@ var init_devAdmin = __esm({
39075
39627
  };
39076
39628
  handleTest = async (_req, res) => {
39077
39629
  try {
39078
- const { execFile } = await import("node:child_process");
39630
+ const { execFile: execFile2 } = await import("node:child_process");
39079
39631
  const { promisify } = await import("node:util");
39080
- const run = promisify(execFile);
39632
+ const run = promisify(execFile2);
39081
39633
  try {
39082
39634
  const { stdout, stderr } = await run("npm", ["test"], {
39083
39635
  cwd: resolve17(process.cwd()),
@@ -39195,9 +39747,9 @@ var init_devAdmin = __esm({
39195
39747
  }
39196
39748
  try {
39197
39749
  const envPath = join29(process.cwd(), ".env");
39198
- const fd = openSync4(envPath, constants3.O_RDWR | constants3.O_APPEND | constants3.O_CREAT | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0), 384);
39750
+ const fd = openSync5(envPath, constants4.O_RDWR | constants4.O_APPEND | constants4.O_CREAT | (constants4.O_NOFOLLOW ?? 0) | (constants4.O_NONBLOCK ?? 0), 384);
39199
39751
  try {
39200
- const info = fstatSync2(fd);
39752
+ const info = fstatSync3(fd);
39201
39753
  if (!info.isFile() || info.nlink !== 1) throw new Error("Refusing a non-regular or multiply-linked configuration file");
39202
39754
  fchmodSync2(fd, 384);
39203
39755
  const lines = readFileSync20(fd, "utf-8").split("\n");
@@ -39227,10 +39779,10 @@ var init_devAdmin = __esm({
39227
39779
  for (const [key, found] of Object.entries(keysFound)) {
39228
39780
  if (!found) newLines.push(`${key}=${values[key]}`);
39229
39781
  }
39230
- ftruncateSync(fd, 0);
39782
+ ftruncateSync2(fd, 0);
39231
39783
  writeFileSync16(fd, newLines.join("\n") + "\n");
39232
39784
  } finally {
39233
- closeSync4(fd);
39785
+ closeSync5(fd);
39234
39786
  }
39235
39787
  res.json({ success: true });
39236
39788
  } catch (e) {
@@ -39353,48 +39905,7 @@ var init_devAdmin = __esm({
39353
39905
  const full = join29(target, name);
39354
39906
  const entryRel = relative9(root, full).replace(/\\/g, "/");
39355
39907
  if (isSecretPath(entryRel)) continue;
39356
- let isDir = false;
39357
- let size = null;
39358
- try {
39359
- const st = statSync18(full);
39360
- isDir = st.isDirectory();
39361
- if (!isDir) size = st.size;
39362
- } catch {
39363
- }
39364
- const gitPath = cwdInGit + entryRel;
39365
- let gitLabel = "clean";
39366
- const code = gitStatus.get(gitPath);
39367
- if (code !== void 0) {
39368
- gitLabel = devGitStatusLabel(code);
39369
- } else if (isDir) {
39370
- const prefix = gitPath + "/";
39371
- for (const [gf, gc] of gitStatus) {
39372
- if (gf.startsWith(prefix)) {
39373
- gitLabel = gc === "??" ? "untracked" : "modified";
39374
- break;
39375
- }
39376
- }
39377
- }
39378
- let hasChildren = null;
39379
- if (isDir) {
39380
- hasChildren = false;
39381
- try {
39382
- for (const c of readdirSync16(full)) {
39383
- if (devFilesHidden(c)) continue;
39384
- hasChildren = true;
39385
- break;
39386
- }
39387
- } catch {
39388
- }
39389
- }
39390
- entries.push({
39391
- name,
39392
- path: entryRel,
39393
- is_dir: isDir,
39394
- has_children: hasChildren,
39395
- git_status: gitLabel,
39396
- size
39397
- });
39908
+ entries.push(buildFileEntry(name, full, entryRel, cwdInGit, gitStatus));
39398
39909
  }
39399
39910
  res.json({ path: relative9(root, target).replace(/\\/g, "/") || ".", branch, entries });
39400
39911
  };
@@ -39927,6 +40438,31 @@ var init_devAdmin = __esm({
39927
40438
  res.raw.writeHead(404, { "Content-Type": "text/plain" });
39928
40439
  res.raw.end("tina4-dev-admin.min.js not found");
39929
40440
  };
40441
+ handleDevAdminSplit = async (_req, res) => {
40442
+ const { readFileSync: readFileSync30, existsSync: existsSync38 } = await import("node:fs");
40443
+ const { dirname: dirname20, join: join41, resolve: resolve29 } = await import("node:path");
40444
+ const { fileURLToPath: fileURLToPath12 } = await import("node:url");
40445
+ const dir = dirname20(fileURLToPath12(import.meta.url));
40446
+ const candidates = [
40447
+ join41(dir, "..", "public", "js", "tina4-dev-admin-split.html"),
40448
+ join41(dir, "..", "..", "public", "js", "tina4-dev-admin-split.html"),
40449
+ resolve29(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin-split.html"),
40450
+ resolve29(process.cwd(), "public", "js", "tina4-dev-admin-split.html")
40451
+ ];
40452
+ for (const path10 of candidates) {
40453
+ if (existsSync38(path10)) {
40454
+ try {
40455
+ const content = readFileSync30(path10, "utf-8");
40456
+ res.raw.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
40457
+ res.raw.end(content);
40458
+ return;
40459
+ } catch {
40460
+ }
40461
+ }
40462
+ }
40463
+ res.raw.writeHead(404, { "Content-Type": "text/plain" });
40464
+ res.raw.end("tina4-dev-admin-split.html not found");
40465
+ };
39930
40466
  handleToolbarCss = (_req, res) => {
39931
40467
  res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
39932
40468
  res.raw.end(toolbarCss());
@@ -40398,7 +40934,10 @@ function generate2(routes, models = []) {
40398
40934
  openapi: resolveOpenApiVersion(),
40399
40935
  info: buildInfo(),
40400
40936
  servers: resolveServers(),
40401
- paths: {},
40937
+ // Prototype-less: route paths are developer-defined, but a null prototype
40938
+ // makes `spec.paths[path][method] = op` structurally pollution-proof
40939
+ // regardless (js/prototype-polluting-assignment). Serialises to JSON the same.
40940
+ paths: /* @__PURE__ */ Object.create(null),
40402
40941
  components: {
40403
40942
  schemas: {},
40404
40943
  // Configurable security schemes (v3.13.42): bearerFormat via env, optional
@@ -40463,7 +41002,7 @@ function buildOperation(route, spec, ctx) {
40463
41002
  const openApiPath = patternToOpenAPI(route.pattern);
40464
41003
  const method = route.method.toLowerCase();
40465
41004
  if (!spec.paths[openApiPath]) {
40466
- spec.paths[openApiPath] = {};
41005
+ spec.paths[openApiPath] = /* @__PURE__ */ Object.create(null);
40467
41006
  }
40468
41007
  const tags = route.meta?.tags ?? inferTags(route.pattern);
40469
41008
  for (const t of tags) {
@@ -40769,7 +41308,7 @@ function inferModelFromPath(pattern) {
40769
41308
  return null;
40770
41309
  }
40771
41310
  function operationIdBase(method, openApiPath) {
40772
- const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
41311
+ const clean = trimSlashes(openApiPath).replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
40773
41312
  return clean ? `${method}_${clean}` : method;
40774
41313
  }
40775
41314
  function uniqueOperationId(method, openApiPath, seen) {
@@ -40787,6 +41326,7 @@ var WRITE_METHODS, INTERNAL_PREFIXES, registeredSchemes, registeredSchemas, PARA
40787
41326
  var init_generator = __esm({
40788
41327
  "../swagger/src/generator.ts"() {
40789
41328
  "use strict";
41329
+ init_slashTrim();
40790
41330
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
40791
41331
  INTERNAL_PREFIXES = ["/swagger", "/__dev", "/__feedback", "/ai", "/rag", "/vision", "/embed", "/image"];
40792
41332
  registeredSchemes = {};
@@ -40808,7 +41348,7 @@ var init_generator = __esm({
40808
41348
 
40809
41349
  // ../swagger/src/ui.ts
40810
41350
  function swaggerUiCdn() {
40811
- return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
41351
+ return rtrimSlashes(process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5");
40812
41352
  }
40813
41353
  function swaggerEnabled() {
40814
41354
  const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
@@ -40864,6 +41404,7 @@ var SWAGGER_UI_HTML;
40864
41404
  var init_ui = __esm({
40865
41405
  "../swagger/src/ui.ts"() {
40866
41406
  "use strict";
41407
+ init_slashTrim();
40867
41408
  SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
40868
41409
  <html lang="en">
40869
41410
  <head>
@@ -40918,7 +41459,7 @@ __export(sso_exports, {
40918
41459
  SsoError: () => SsoError,
40919
41460
  liveSessionIdentity: () => liveSessionIdentity
40920
41461
  });
40921
- import { createHash as createHash10, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
41462
+ import { createHash as createHash11, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
40922
41463
  function expiresAt(tokens) {
40923
41464
  const lifetime = Number(tokens.expires_in ?? 0);
40924
41465
  return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
@@ -41049,7 +41590,7 @@ var init_sso = __esm({
41049
41590
  const state = randomBytes8(32).toString("base64url");
41050
41591
  const nonce = randomBytes8(32).toString("base64url");
41051
41592
  const verifier = randomBytes8(64).toString("base64url");
41052
- const challenge = createHash10("sha256").update(verifier).digest("base64url");
41593
+ const challenge = createHash11("sha256").update(verifier).digest("base64url");
41053
41594
  session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
41054
41595
  const metadata = await this.discover();
41055
41596
  const query = new URLSearchParams({
@@ -41281,7 +41822,7 @@ import { resolve as resolve20, dirname as dirname16, join as join32, relative as
41281
41822
  import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync23, statSync as statSync19 } from "node:fs";
41282
41823
  import { isatty } from "node:tty";
41283
41824
  import { fileURLToPath as fileURLToPath9 } from "node:url";
41284
- import { execFileSync as execFileSync3, exec } from "node:child_process";
41825
+ import { execFileSync as execFileSync3, execFile } from "node:child_process";
41285
41826
  import cluster from "node:cluster";
41286
41827
  import os2 from "node:os";
41287
41828
  function isSwaggerAssetPath(pathname) {
@@ -41417,10 +41958,11 @@ function shouldOpenBrowser(isDevelopment, noBrowser = false) {
41417
41958
  });
41418
41959
  }
41419
41960
  function openBrowser(url) {
41961
+ if (!/^https?:\/\/([A-Za-z0-9.-]+|\[[0-9A-Fa-f:]+\])(:\d{1,5})?\/?$/.test(url)) return;
41420
41962
  setTimeout(() => {
41421
- if (process.platform === "darwin") exec(`open ${url}`);
41422
- else if (process.platform === "win32") exec(`start "" "${url}"`);
41423
- else exec(`xdg-open ${url}`);
41963
+ if (process.platform === "darwin") execFile("open", [url]);
41964
+ else if (process.platform === "win32") execFile("rundll32", ["url.dll,FileProtocolHandler", url]);
41965
+ else execFile("xdg-open", [url]);
41424
41966
  }, 2e3);
41425
41967
  }
41426
41968
  function resolvePortAndHost(config) {
@@ -41535,7 +42077,7 @@ function resetTemplateCache() {
41535
42077
  }
41536
42078
  function resolveTemplate(pathname, templatesDir) {
41537
42079
  if (!templateAutoRoutingEnabled()) return null;
41538
- const cleanPath = pathname.replace(/^\/+/, "").replace(/\/+$/, "") || "index";
42080
+ const cleanPath = trimSlashes(pathname) || "index";
41539
42081
  const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
41540
42082
  if (isDev2) {
41541
42083
  if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
@@ -41803,7 +42345,7 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
41803
42345
  }
41804
42346
  function resolveHandlerArgs(handler, req2, res, routeParams) {
41805
42347
  const fnStr = handler.toString();
41806
- const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
42348
+ const argMatch = fnStr.match(/^(?:async\s*)?(?:function(?:\s+\w+)?\s*)?\(([^)]*)\)/);
41807
42349
  const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
41808
42350
  const filteredArgs = argNames.filter((n) => n.length > 0);
41809
42351
  if (filteredArgs.length === 0) return [];
@@ -42039,8 +42581,9 @@ async function buildDispatchContext(router, base) {
42039
42581
  const templatesDir = resolve20(root, "src/templates");
42040
42582
  let frondEngine = null;
42041
42583
  try {
42042
- const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42584
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2, setFormTokenSessionProvider: setFormTokenSessionProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42043
42585
  setCspNonceProvider2(currentCspNonce);
42586
+ setFormTokenSessionProvider2(currentFormTokenSessionId);
42044
42587
  frondEngine = new Frond2(templatesDir);
42045
42588
  } catch {
42046
42589
  }
@@ -42155,7 +42698,10 @@ async function runDispatch(ctx, rawReq, rawRes) {
42155
42698
  const requestNonce = generateNonce();
42156
42699
  return Log.runWithRequestId(
42157
42700
  requestId,
42158
- () => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
42701
+ () => runWithCspNonce(
42702
+ requestNonce,
42703
+ () => runWithFormTokenSession(() => dispatchInner(ctx, rawReq, rawRes, requestId))
42704
+ )
42159
42705
  );
42160
42706
  }
42161
42707
  function loopbackBindHosts(host) {
@@ -42283,8 +42829,9 @@ async function startServer(config) {
42283
42829
  let frondEngine = null;
42284
42830
  setDefaultTemplatesDir(templatesDir);
42285
42831
  try {
42286
- const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42832
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2, setFormTokenSessionProvider: setFormTokenSessionProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
42287
42833
  setCspNonceProvider2(currentCspNonce);
42834
+ setFormTokenSessionProvider2(currentFormTokenSessionId);
42288
42835
  frondEngine = new Frond2(templatesDir);
42289
42836
  router.addRoute({
42290
42837
  method: "GET",
@@ -42633,6 +43180,7 @@ var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swag
42633
43180
  var init_server = __esm({
42634
43181
  "../core/src/server.ts"() {
42635
43182
  "use strict";
43183
+ init_slashTrim();
42636
43184
  init_transport();
42637
43185
  init_router();
42638
43186
  init_authGate();
@@ -42640,6 +43188,7 @@ var init_server = __esm({
42640
43188
  init_portTakeover();
42641
43189
  init_request();
42642
43190
  init_dispatchPipeline();
43191
+ init_session();
42643
43192
  init_response();
42644
43193
  init_middleware();
42645
43194
  init_static();
@@ -44255,6 +44804,7 @@ var ApiStreamError, RETRY_STATUSES, DOWNLOAD_CHUNK_SIZE, MAX_REDIRECTS, STRIP_ON
44255
44804
  var init_api = __esm({
44256
44805
  "../core/src/api.ts"() {
44257
44806
  "use strict";
44807
+ init_slashTrim();
44258
44808
  init_version();
44259
44809
  init_ssrf();
44260
44810
  ApiStreamError = class extends Error {
@@ -44352,7 +44902,7 @@ var init_api = __esm({
44352
44902
  * turns on a per-client, in-memory cookie jar.
44353
44903
  */
44354
44904
  constructor(baseUrl = "", authHeaderOrOptions = "", timeout = 30) {
44355
- this.baseUrl = baseUrl.replace(/\/+$/, "");
44905
+ this.baseUrl = rtrimSlashes(baseUrl);
44356
44906
  this.headers = {};
44357
44907
  this.maxRetries = 0;
44358
44908
  this.retryBackoff = 0.5;
@@ -45118,6 +45668,23 @@ function sendCommand(socket, command) {
45118
45668
  });
45119
45669
  });
45120
45670
  }
45671
+ function pushAlternativeBody(lines, altBoundary, text, body) {
45672
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
45673
+ lines.push("");
45674
+ lines.push(`--${altBoundary}`);
45675
+ lines.push("Content-Type: text/plain; charset=UTF-8");
45676
+ lines.push("Content-Transfer-Encoding: 7bit");
45677
+ lines.push("");
45678
+ lines.push(text);
45679
+ lines.push("");
45680
+ lines.push(`--${altBoundary}`);
45681
+ lines.push("Content-Type: text/html; charset=UTF-8");
45682
+ lines.push("Content-Transfer-Encoding: 7bit");
45683
+ lines.push("");
45684
+ lines.push(body);
45685
+ lines.push("");
45686
+ lines.push(`--${altBoundary}--`);
45687
+ }
45121
45688
  function buildMimeMessage(options) {
45122
45689
  const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
45123
45690
  const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
@@ -45147,21 +45714,7 @@ function buildMimeMessage(options) {
45147
45714
  lines.push("");
45148
45715
  lines.push(`--${boundary}`);
45149
45716
  if (hasTextAlt) {
45150
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
45151
- lines.push("");
45152
- lines.push(`--${altBoundary}`);
45153
- lines.push("Content-Type: text/plain; charset=UTF-8");
45154
- lines.push("Content-Transfer-Encoding: 7bit");
45155
- lines.push("");
45156
- lines.push(options.text);
45157
- lines.push("");
45158
- lines.push(`--${altBoundary}`);
45159
- lines.push("Content-Type: text/html; charset=UTF-8");
45160
- lines.push("Content-Transfer-Encoding: 7bit");
45161
- lines.push("");
45162
- lines.push(options.body);
45163
- lines.push("");
45164
- lines.push(`--${altBoundary}--`);
45717
+ pushAlternativeBody(lines, altBoundary, options.text, options.body);
45165
45718
  } else {
45166
45719
  const contentType = options.html ? "text/html" : "text/plain";
45167
45720
  lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
@@ -45186,21 +45739,7 @@ function buildMimeMessage(options) {
45186
45739
  lines.push("");
45187
45740
  lines.push(`--${boundary}--`);
45188
45741
  } else if (hasTextAlt) {
45189
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
45190
- lines.push("");
45191
- lines.push(`--${altBoundary}`);
45192
- lines.push("Content-Type: text/plain; charset=UTF-8");
45193
- lines.push("Content-Transfer-Encoding: 7bit");
45194
- lines.push("");
45195
- lines.push(options.text);
45196
- lines.push("");
45197
- lines.push(`--${altBoundary}`);
45198
- lines.push("Content-Type: text/html; charset=UTF-8");
45199
- lines.push("Content-Transfer-Encoding: 7bit");
45200
- lines.push("");
45201
- lines.push(options.body);
45202
- lines.push("");
45203
- lines.push(`--${altBoundary}--`);
45742
+ pushAlternativeBody(lines, altBoundary, options.text, options.body);
45204
45743
  } else {
45205
45744
  const contentType = options.html ? "text/html" : "text/plain";
45206
45745
  lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
@@ -45965,61 +46504,72 @@ function decodeEntities(raw) {
45965
46504
  function parseXmlDocument(xml) {
45966
46505
  const stack = [];
45967
46506
  let root = null;
45968
- let position = 0;
45969
46507
  const addText = (text) => {
45970
46508
  const current = stack[stack.length - 1];
45971
46509
  if (current.children.length === 0) current.text += text;
45972
46510
  };
45973
- while (position < xml.length) {
45974
- const nextTag = xml.indexOf("<", position);
46511
+ const skipComment = (at) => {
46512
+ const close = xml.indexOf("-->", at + 4);
46513
+ if (close === -1) malformed();
46514
+ return close + 3;
46515
+ };
46516
+ const readCData = (at) => {
46517
+ const close = xml.indexOf("]]>", at + 9);
46518
+ if (close === -1 || stack.length === 0) malformed();
46519
+ addText(xml.slice(at + 9, close));
46520
+ return close + 3;
46521
+ };
46522
+ const skipProcessingInstruction = (at) => {
46523
+ const close = xml.indexOf("?>", at + 2);
46524
+ if (close === -1) malformed();
46525
+ const target = xml.slice(at + 2, close).split(/\s/, 1)[0];
46526
+ if (target.toLowerCase() === "xml" && at !== 0) malformed();
46527
+ return close + 2;
46528
+ };
46529
+ const closeElement = (at) => {
46530
+ END_TAG.lastIndex = at;
46531
+ const match = END_TAG.exec(xml);
46532
+ const open = stack.pop();
46533
+ if (!match || !open || open.qualifiedName !== match[1]) malformed();
46534
+ return END_TAG.lastIndex;
46535
+ };
46536
+ const openElement = (at) => {
46537
+ START_TAG.lastIndex = at;
46538
+ const match = START_TAG.exec(xml);
46539
+ if (!match || stack.length === 0 && root !== null) malformed();
46540
+ for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
46541
+ const qualifiedName = match[1];
46542
+ const element = {
46543
+ name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
46544
+ qualifiedName,
46545
+ children: [],
46546
+ text: ""
46547
+ };
46548
+ if (stack.length > 0) stack[stack.length - 1].children.push(element);
46549
+ else root = element;
46550
+ if (match[3] !== "/") stack.push(element);
46551
+ return START_TAG.lastIndex;
46552
+ };
46553
+ const readText = (at) => {
46554
+ const nextTag = xml.indexOf("<", at);
45975
46555
  const textEnd = nextTag === -1 ? xml.length : nextTag;
45976
- if (textEnd > position) {
45977
- const raw = xml.slice(position, textEnd);
46556
+ if (textEnd > at) {
46557
+ const raw = xml.slice(at, textEnd);
45978
46558
  if (stack.length > 0) addText(decodeEntities(raw));
45979
46559
  else if (raw.trim() !== "") malformed();
45980
- position = textEnd;
45981
- if (nextTag === -1) break;
45982
- }
45983
- if (xml.startsWith("<!--", position)) {
45984
- const close = xml.indexOf("-->", position + 4);
45985
- if (close === -1) malformed();
45986
- position = close + 3;
45987
- } else if (xml.startsWith("<![CDATA[", position)) {
45988
- const close = xml.indexOf("]]>", position + 9);
45989
- if (close === -1 || stack.length === 0) malformed();
45990
- addText(xml.slice(position + 9, close));
45991
- position = close + 3;
45992
- } else if (xml.startsWith("<?", position)) {
45993
- const close = xml.indexOf("?>", position + 2);
45994
- if (close === -1) malformed();
45995
- const target = xml.slice(position + 2, close).split(/\s/, 1)[0];
45996
- if (target.toLowerCase() === "xml" && position !== 0) malformed();
45997
- position = close + 2;
45998
- } else if (xml.startsWith("<!", position)) {
45999
- malformed();
46000
- } else if (xml.startsWith("</", position)) {
46001
- END_TAG.lastIndex = position;
46002
- const match = END_TAG.exec(xml);
46003
- const open = stack.pop();
46004
- if (!match || !open || open.qualifiedName !== match[1]) malformed();
46005
- position = END_TAG.lastIndex;
46006
- } else {
46007
- START_TAG.lastIndex = position;
46008
- const match = START_TAG.exec(xml);
46009
- if (!match || stack.length === 0 && root !== null) malformed();
46010
- for (const attribute of match[2].matchAll(ATTRIBUTE_VALUE)) decodeEntities(attribute[1] ?? attribute[2]);
46011
- const qualifiedName = match[1];
46012
- const element = {
46013
- name: qualifiedName.slice(qualifiedName.indexOf(":") + 1),
46014
- qualifiedName,
46015
- children: [],
46016
- text: ""
46017
- };
46018
- if (stack.length > 0) stack[stack.length - 1].children.push(element);
46019
- else root = element;
46020
- if (match[3] !== "/") stack.push(element);
46021
- position = START_TAG.lastIndex;
46022
46560
  }
46561
+ return textEnd;
46562
+ };
46563
+ let position = 0;
46564
+ while (position < xml.length) {
46565
+ position = readText(position);
46566
+ if (position >= xml.length) break;
46567
+ if (xml.startsWith("<!--", position)) position = skipComment(position);
46568
+ else if (xml.startsWith("<![CDATA[", position)) position = readCData(position);
46569
+ else if (xml.startsWith("<?", position)) position = skipProcessingInstruction(position);
46570
+ else if (xml.startsWith("<!", position)) malformed();
46571
+ else if (xml.startsWith("</", position)) position = closeElement(position);
46572
+ else position = openElement(position);
46023
46573
  }
46024
46574
  if (root === null || stack.length > 0) malformed();
46025
46575
  return root;
@@ -47521,13 +48071,13 @@ export default class User {
47521
48071
  import {
47522
48072
  createCipheriv,
47523
48073
  createECDH,
47524
- createHmac as createHmac4,
48074
+ createHmac as createHmac5,
47525
48075
  createPrivateKey,
47526
48076
  createSign as createSign2,
47527
48077
  randomBytes as randomBytes12
47528
48078
  } from "node:crypto";
47529
48079
  function encodeBase64Url(value) {
47530
- return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
48080
+ return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/={1,2}$/, "");
47531
48081
  }
47532
48082
  function decodeBase64Url(value, name) {
47533
48083
  if (typeof value !== "string" || value.length === 0) {
@@ -47557,15 +48107,15 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
47557
48107
  format: "jwk"
47558
48108
  });
47559
48109
  }
47560
- function hmac(salt, value) {
47561
- return createHmac4("sha256", salt).update(value).digest();
48110
+ function hmac2(salt, value) {
48111
+ return createHmac5("sha256", salt).update(value).digest();
47562
48112
  }
47563
48113
  function hkdfExpand(prk, info, length) {
47564
48114
  const chunks = [];
47565
48115
  let previous = Buffer.alloc(0);
47566
48116
  for (let counter = 1; Buffer.concat(chunks).length < length; counter++) {
47567
48117
  if (counter > 255) throw new PushError("HKDF output is too large");
47568
- previous = hmac(prk, Buffer.concat([previous, info, Buffer.from([counter])]));
48118
+ previous = hmac2(prk, Buffer.concat([previous, info, Buffer.from([counter])]));
47569
48119
  chunks.push(previous);
47570
48120
  }
47571
48121
  return Buffer.concat(chunks).subarray(0, length);
@@ -47602,10 +48152,10 @@ function encryptPayload(payload, subscription) {
47602
48152
  clientPublic,
47603
48153
  serverPublic
47604
48154
  ]);
47605
- const prkKey = hmac(authSecret, sharedSecret);
48155
+ const prkKey = hmac2(authSecret, sharedSecret);
47606
48156
  const ikm = hkdfExpand(prkKey, keyInfo, 32);
47607
48157
  const salt = randomBytes12(16);
47608
- const contentPrk = hmac(salt, ikm);
48158
+ const contentPrk = hmac2(salt, ikm);
47609
48159
  const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
47610
48160
  const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
47611
48161
  const cipher = createCipheriv("aes-128-gcm", cek, nonce);
@@ -47778,6 +48328,7 @@ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai, Aggre
47778
48328
  var init_aiClient = __esm({
47779
48329
  "../core/src/aiClient.ts"() {
47780
48330
  "use strict";
48331
+ init_slashTrim();
47781
48332
  init_api();
47782
48333
  AiError = class extends Error {
47783
48334
  };
@@ -47982,7 +48533,7 @@ var init_aiClient = __esm({
47982
48533
  throw new AiConfigError("AI URL must be an http or https URL");
47983
48534
  }
47984
48535
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
47985
- const path10 = url.pathname.replace(/\/+$/, "");
48536
+ const path10 = rtrimSlashes(url.pathname);
47986
48537
  if (path10 === "" || path10 === "/v1" || path10 === "/api" || path10 === "/v1beta/openai") {
47987
48538
  const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
47988
48539
  url.pathname = (path10 || "/v1") + suffix;
@@ -50352,6 +50903,7 @@ __export(src_exports3, {
50352
50903
  backgroundTaskCount: () => backgroundTaskCount,
50353
50904
  buildEnvelope: () => buildEnvelope,
50354
50905
  buildFrame: () => buildFrame,
50906
+ buildSeedFieldMap: () => buildSeedFieldMap,
50355
50907
  buildSessionCookie: () => buildSessionCookie,
50356
50908
  cacheBackendStats: () => cacheBackendStats,
50357
50909
  cacheClear: () => cacheClear,
@@ -50457,6 +51009,7 @@ __export(src_exports3, {
50457
51009
  parseFrame: () => parseFrame,
50458
51010
  parseLineStream: () => parseLineStream,
50459
51011
  parseMultipart: () => parseMultipart,
51012
+ parseSeedRequest: () => parseSeedRequest,
50460
51013
  parseSseStream: () => parseSseStream,
50461
51014
  parseUpgradeHeaders: () => parseUpgradeHeaders,
50462
51015
  patch: () => patch,
@@ -51164,7 +51717,7 @@ async function runTests(testPath) {
51164
51717
  // src/commands/lint.ts
51165
51718
  init_util();
51166
51719
  import { existsSync as existsSync34, readdirSync as readdirSync21, readFileSync as readFileSync28, statSync as statSync22, writeFileSync as writeFileSync20 } from "node:fs";
51167
- import { createRequire as createRequire9 } from "node:module";
51720
+ import { createRequire as createRequire8 } from "node:module";
51168
51721
  import { dirname as dirname18, join as join36, relative as relative12 } from "node:path";
51169
51722
  import { spawnSync as spawnSync3 } from "node:child_process";
51170
51723
  var LINT_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
@@ -51236,10 +51789,10 @@ function findEslintConfig(cwd) {
51236
51789
  function resolvePackageBin(cwd, pkg, binRelative) {
51237
51790
  const direct = join36(cwd, "node_modules", pkg, binRelative);
51238
51791
  if (existsSync34(direct)) return direct;
51239
- const require3 = createRequire9(join36(cwd, "package.json"));
51792
+ const require2 = createRequire8(join36(cwd, "package.json"));
51240
51793
  let entry;
51241
51794
  try {
51242
- entry = require3.resolve(pkg);
51795
+ entry = require2.resolve(pkg);
51243
51796
  } catch {
51244
51797
  return null;
51245
51798
  }
@@ -51595,7 +52148,7 @@ async function queueCommand(args = []) {
51595
52148
 
51596
52149
  // src/commands/build.ts
51597
52150
  init_util();
51598
- import { accessSync as accessSync2, constants as constants4, existsSync as existsSync36, statSync as statSync24 } from "node:fs";
52151
+ import { accessSync as accessSync2, constants as constants5, existsSync as existsSync36, statSync as statSync24 } from "node:fs";
51599
52152
  import { basename as basename9, delimiter as delimiter3, join as join39 } from "node:path";
51600
52153
  import { spawnSync as spawnSync4 } from "node:child_process";
51601
52154
  function parseFlags3(args) {
@@ -51610,7 +52163,7 @@ function whichDocker() {
51610
52163
  for (const ext of exts) {
51611
52164
  const candidate = join39(dir, `docker${ext}`);
51612
52165
  try {
51613
- accessSync2(candidate, constants4.X_OK);
52166
+ accessSync2(candidate, constants5.X_OK);
51614
52167
  if (statSync24(candidate).isFile()) return candidate;
51615
52168
  } catch {
51616
52169
  }