tina4-nodejs 3.13.148 → 3.13.149

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 (42) hide show
  1. package/CLAUDE.md +4 -3
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +10875 -9847
  4. package/packages/cli/src/commands/generate.ts +184 -25
  5. package/packages/core/dist/index.js +10848 -9820
  6. package/packages/core/src/devAdmin.ts +72 -1
  7. package/packages/core/src/index.ts +3 -0
  8. package/packages/core/src/messenger.ts +57 -1
  9. package/packages/core/src/router.ts +11 -1
  10. package/packages/core/src/server.ts +5 -2
  11. package/packages/core/templates/crud/form.twig +26 -0
  12. package/packages/core/templates/crud/modals.twig +31 -0
  13. package/packages/core/templates/crud/page.twig +401 -0
  14. package/packages/core/templates/crud/table.twig +69 -0
  15. package/packages/frond/dist/index.js +9 -1
  16. package/packages/frond/src/engine.ts +9 -0
  17. package/packages/frond/src/index.ts +1 -1
  18. package/packages/orm/dist/index.js +1255 -217
  19. package/packages/orm/src/adapters/mongodb.ts +209 -34
  20. package/packages/orm/src/autoCrud.ts +9 -0
  21. package/packages/orm/src/crud.ts +273 -0
  22. package/packages/orm/src/crudHelpers.ts +194 -0
  23. package/packages/orm/src/crudRender.ts +202 -0
  24. package/packages/orm/src/crudTable.ts +171 -0
  25. package/packages/orm/src/index.ts +7 -1
  26. package/packages/orm/src/migration.ts +167 -4
  27. package/packages/orm/src/query.ts +99 -40
  28. package/packages/orm/src/sqlTranslator.ts +8 -1
  29. package/packages/orm/src/types.ts +19 -0
  30. package/types/core/src/devAdmin.d.ts +23 -0
  31. package/types/core/src/messenger.d.ts +4 -0
  32. package/types/core/src/router.d.ts +0 -5
  33. package/types/frond/src/engine.d.ts +6 -0
  34. package/types/frond/src/index.d.ts +1 -1
  35. package/types/orm/src/crud.d.ts +82 -0
  36. package/types/orm/src/crudHelpers.d.ts +63 -0
  37. package/types/orm/src/crudRender.d.ts +38 -0
  38. package/types/orm/src/crudTable.d.ts +45 -0
  39. package/types/orm/src/index.d.ts +3 -1
  40. package/types/orm/src/migration.d.ts +0 -18
  41. package/types/orm/src/query.d.ts +0 -12
  42. package/types/orm/src/types.d.ts +19 -0
@@ -1467,7 +1467,10 @@ var init_sqlTranslator = __esm({
1467
1467
  static appendLimit(sql, limit, offset) {
1468
1468
  if (limit === void 0 || limit === null || limit <= 0) return sql;
1469
1469
  if (_SQLTranslator.hasTrailingLimit(sql)) return sql;
1470
- const trimmed = sql.replace(/[\s;]+$/, "");
1470
+ const trailing = " \n\r\f\v;";
1471
+ let end = sql.length;
1472
+ while (end > 0 && trailing.includes(sql[end - 1])) end--;
1473
+ const trimmed = sql.slice(0, end);
1471
1474
  const suffix = offset !== void 0 && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`;
1472
1475
  return `${trimmed}
1473
1476
  ${suffix}`;
@@ -2791,11 +2794,11 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
2791
2794
  if (resolvedExpiresIn !== 0) {
2792
2795
  claims.exp = now + resolvedExpiresIn * 60;
2793
2796
  }
2794
- const h = base64urlEncode(Buffer.from(JSON.stringify(header)));
2797
+ const h2 = base64urlEncode(Buffer.from(JSON.stringify(header)));
2795
2798
  const p = base64urlEncode(Buffer.from(JSON.stringify(claims)));
2796
- const signingInput = `${h}.${p}`;
2799
+ const signingInput = `${h2}.${p}`;
2797
2800
  const signature = sign(signingInput, resolvedSecret, resolvedAlgorithm);
2798
- return `${h}.${p}.${signature}`;
2801
+ return `${h2}.${p}.${signature}`;
2799
2802
  }
2800
2803
  function validToken(token, secret, algorithm) {
2801
2804
  const resolvedSecret = secret ?? process.env.TINA4_SECRET ?? "";
@@ -2806,8 +2809,8 @@ function validToken(token, secret, algorithm) {
2806
2809
  try {
2807
2810
  const parts = token.split(".");
2808
2811
  if (parts.length !== 3) return null;
2809
- const [h, p, sig] = parts;
2810
- const header = JSON.parse(base64urlDecode(h).toString());
2812
+ const [h2, p, sig] = parts;
2813
+ const header = JSON.parse(base64urlDecode(h2).toString());
2811
2814
  if (header.alg !== resolvedAlgorithm) return null;
2812
2815
  if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
2813
2816
  const weak = insecureSecretMessage(resolvedSecret);
@@ -2816,7 +2819,7 @@ function validToken(token, secret, algorithm) {
2816
2819
  return null;
2817
2820
  }
2818
2821
  }
2819
- const signingInput = `${h}.${p}`;
2822
+ const signingInput = `${h2}.${p}`;
2820
2823
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
2821
2824
  return null;
2822
2825
  }
@@ -4593,7 +4596,7 @@ function any(path10, handler, middlewares, meta) {
4593
4596
  function websocket(path10, handler, options) {
4594
4597
  return defaultRouter.websocket(path10, handler, options);
4595
4598
  }
4596
- var WsRouteRef, RouteRef, Router, RouteGroup, defaultRouter;
4599
+ var WsRouteRef, RouteRef, Router, RouteGroup, TINA4_DEFAULT_ROUTER, globalRouterHost, defaultRouter;
4597
4600
  var init_router = __esm({
4598
4601
  "../core/src/router.ts"() {
4599
4602
  "use strict";
@@ -5230,7 +5233,9 @@ var init_router = __esm({
5230
5233
  callback(nestedGroup);
5231
5234
  }
5232
5235
  };
5233
- defaultRouter = new Router();
5236
+ TINA4_DEFAULT_ROUTER = /* @__PURE__ */ Symbol.for("tina4.defaultRouter");
5237
+ globalRouterHost = globalThis;
5238
+ defaultRouter = globalRouterHost[TINA4_DEFAULT_ROUTER] ?? (globalRouterHost[TINA4_DEFAULT_ROUTER] = new Router());
5234
5239
  }
5235
5240
  });
5236
5241
 
@@ -5480,6 +5485,7 @@ __export(engine_exports, {
5480
5485
  Frond: () => Frond,
5481
5486
  MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
5482
5487
  TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
5488
+ escapeHtml: () => escapeHtml,
5483
5489
  expressionFormCache: () => expressionFormCache,
5484
5490
  filterChainCache: () => filterChainCache,
5485
5491
  pathParseCache: () => pathParseCache,
@@ -6404,6 +6410,9 @@ function splitArgs(raw) {
6404
6410
  function htmlEscape(str) {
6405
6411
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
6406
6412
  }
6413
+ function escapeHtml(value) {
6414
+ return htmlEscape(String(value));
6415
+ }
6407
6416
  function jsEscape(value) {
6408
6417
  let out = "";
6409
6418
  for (const ch of String(value)) {
@@ -6580,11 +6589,11 @@ function _buildFormTokenJwt(descriptor = "") {
6580
6589
  if (_formTokenSessionId) {
6581
6590
  payload.session_id = _formTokenSessionId;
6582
6591
  }
6583
- const h = _b64url(Buffer.from(JSON.stringify(header)));
6592
+ const h2 = _b64url(Buffer.from(JSON.stringify(header)));
6584
6593
  const p = _b64url(Buffer.from(JSON.stringify(payload)));
6585
- const sigInput = `${h}.${p}`;
6594
+ const sigInput = `${h2}.${p}`;
6586
6595
  const sig = _b64url(createHmac2("sha256", secret).update(sigInput).digest());
6587
- return `${h}.${p}.${sig}`;
6596
+ return `${h2}.${p}.${sig}`;
6588
6597
  }
6589
6598
  function _generateFormToken(descriptor = "") {
6590
6599
  const token = _buildFormTokenJwt(descriptor);
@@ -13827,8 +13836,9 @@ __export(generate_exports, {
13827
13836
  toSnake: () => toSnake,
13828
13837
  toTableName: () => toTableName
13829
13838
  });
13830
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
13831
- import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
13839
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, readdirSync as readdirSync6, copyFileSync } from "node:fs";
13840
+ import { join as join15, relative as relative2, resolve as resolve6, sep as sep3, dirname as dirname7 } from "node:path";
13841
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
13832
13842
  function ensureDir(dir) {
13833
13843
  if (__resolution.dryRun) return;
13834
13844
  if (!existsSync13(dir)) {
@@ -13911,7 +13921,7 @@ function currentResolution() {
13911
13921
  transformations: [...__resolution.body.transformations]
13912
13922
  };
13913
13923
  if (__resolution.body.edit_hints) {
13914
- body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
13924
+ body.edit_hints = __resolution.body.edit_hints.map((h2) => ({ ...h2 }));
13915
13925
  }
13916
13926
  if (__resolution.body.next) {
13917
13927
  body.next = [...__resolution.body.next];
@@ -14437,23 +14447,144 @@ ${aiFill(`delete_${singular}`, {
14437
14447
  }
14438
14448
  function generateCrud(name, flags) {
14439
14449
  const table2 = resolveTable(name, flags, { announce: false });
14440
- const routeName = pluralizeReserved(toSnake(name));
14441
14450
  const isPublic = Boolean(flags.public);
14442
14451
  if (!__resolution.jsonMode) console.log(`
14443
14452
  Generating CRUD for ${name}...
14444
14453
  `);
14445
14454
  generateModel(name, flags, false);
14446
- generateRoute(routeName, { ...flags, model: name }, false);
14447
- generateForm(name, flags);
14448
- generateView(name, flags);
14449
- generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
14455
+ generateCrudAdminRoute(name, table2, isPublic);
14456
+ if (!flags["no-templates"]) copyCrudTemplates();
14457
+ generateCrudGateTest(name, table2, isPublic);
14450
14458
  if (!__resolution.jsonMode) {
14451
14459
  console.log(`
14452
14460
  CRUD generation complete for ${name}.`);
14453
14461
  console.log(" Run: tina4nodejs migrate");
14454
- console.log(" Visit: /swagger to see the API docs");
14462
+ console.log(` Visit: /admin/${table2} for the admin UI, or /swagger for the API docs`);
14463
+ }
14464
+ }
14465
+ function generateCrudAdminRoute(model, table2, isPublic) {
14466
+ const dir = resolve6("src/routes/admin", table2);
14467
+ ensureDir(dir);
14468
+ pushRoute(`/admin/${table2}`);
14469
+ if (__resolution.target === "crud") {
14470
+ setResolutionField("file_path", `src/routes/admin/${table2}/get.ts`);
14471
+ }
14472
+ const secureExport = isPublic ? "" : "export const secure = true;\n\n";
14473
+ const publicOpt = isPublic ? ", public: true" : "";
14474
+ const writeDoc = isPublic ? "--public: the admin page and the writes are OPEN (no token)." : "Secure by default: the admin page AND the writes require a valid Bearer token (pass --public to open them).";
14475
+ const pageDoc = isPublic ? "" : `
14476
+ // NOTE: the admin page is secured (export const secure = true). A browser needs a valid
14477
+ // token/session to open it; wire your login (Auth / Session) or run
14478
+ // \`generate crud ${model} --public\` for an open page. The AutoCrud READ API
14479
+ // (GET /api/${table2}, GET /api/${table2}/{id}) stays public by AutoCrud's default.`;
14480
+ const content = `import ${model} from "../../../models/${model}.js";
14481
+ import { Crud } from "tina4-nodejs/orm";
14482
+ import type { Tina4Request, Tina4Response } from "tina4-nodejs";
14483
+
14484
+ // ${model} admin \u2014 one server-rendered CRUD page (searchable, sortable,
14485
+ // paginated table + create/edit/delete modals). The REST backend (GET list,
14486
+ // GET/{id}, POST, PUT, DELETE) is AutoCrud; this file owns only the GET admin
14487
+ // page. ${writeDoc}${pageDoc}
14488
+ //
14489
+ // Restyle the UI by editing src/templates/crud/*.twig (copied into this app).
14490
+ // Register the AutoCrud backend for the model at load (idempotent).
14491
+ await Crud.registerBackend(${model}, { public: ${isPublic} });
14492
+
14493
+ ${secureExport}export const meta = { summary: "${model} admin", tags: ["admin"] };
14494
+
14495
+ export default async function (req: Tina4Request, res: Tina4Response) {
14496
+ return res.html(await Crud.toCrud(req, { model: ${model}, title: "${model} Admin"${publicOpt} }));
14497
+ }
14498
+ `;
14499
+ writeFileSafe(join15(dir, "get.ts"), content);
14500
+ }
14501
+ function frameworkCrudTemplatesDir() {
14502
+ const here = dirname7(fileURLToPath4(import.meta.url));
14503
+ const candidates = [
14504
+ resolve6(here, "../../../core/templates/crud"),
14505
+ // monorepo: packages/cli/src/commands -> packages/core/templates/crud
14506
+ resolve6(here, "../../core/templates/crud"),
14507
+ // built layout fallback
14508
+ resolve6(here, "../templates/crud")
14509
+ // flattened publish fallback
14510
+ ];
14511
+ return candidates.find((c) => existsSync13(c)) ?? null;
14512
+ }
14513
+ function copyCrudTemplates() {
14514
+ const sourceDir = frameworkCrudTemplatesDir();
14515
+ if (!sourceDir) {
14516
+ if (!__resolution.jsonMode) console.log(" Skipped crud templates (framework templates not found)");
14517
+ return;
14518
+ }
14519
+ const targetDir = resolve6("src/templates/crud");
14520
+ ensureDir(targetDir);
14521
+ for (const file of readdirSync6(sourceDir).filter((f) => f.endsWith(".twig")).sort()) {
14522
+ const dest = join15(targetDir, file);
14523
+ if (existsSync13(dest)) {
14524
+ if (!__resolution.jsonMode) console.log(` File already exists: ${dest}`);
14525
+ continue;
14526
+ }
14527
+ if (__resolution.dryRun) continue;
14528
+ copyFileSync(join15(sourceDir, file), dest);
14529
+ __resolution.actionsTaken.push(`wrote ${dest}`);
14530
+ if (!__resolution.jsonMode) console.log(` Created ${dest}`);
14455
14531
  }
14456
14532
  }
14533
+ function generateCrudGateTest(model, table2, isPublic) {
14534
+ const dir = resolve6("tests");
14535
+ ensureDir(dir);
14536
+ const posture = isPublic ? "open (--public)" : "gated";
14537
+ const writeCase = isPublic ? ` // --public opened the write: no 401 gate (the handler, not the gate, answers).
14538
+ assert("anonymous POST is NOT gated (--public)",
14539
+ (await client.post("/api/${table2}", { json: { name: "test" } })).status !== 401);` : ` // Secure by default: a tokenless POST is rejected by the gate with 401.
14540
+ assert("anonymous POST is gated -> 401",
14541
+ (await client.post("/api/${table2}", { json: { name: "test" } })).status === 401);
14542
+ // A valid Bearer token passes the gate (the gate no longer answers 401).
14543
+ const token = getToken({ userId: 1 });
14544
+ assert("authenticated POST passes the gate (not 401)",
14545
+ (await client.post("/api/${table2}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status !== 401);`;
14546
+ const pageCase = isPublic ? ` assert("admin page is NOT gated (--public)", (await client.get("/admin/${table2}")).status !== 401);` : ` assert("admin page is secured -> 401 without a token", (await client.get("/admin/${table2}")).status === 401);`;
14547
+ const content = `/**
14548
+ * ${model} CRUD admin (ADR-0094) \u2014 reads public, writes ${posture}, page ${isPublic ? "public" : "secured"}.
14549
+ *
14550
+ * Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
14551
+ * JWT, real route discovery. The admin route registers the AutoCrud backend at
14552
+ * load; discoverRoutes imports it, so the /api/${table2} REST routes exist here.
14553
+ * This gate test proves the SECURE-BY-DEFAULT posture (what registers, what is
14554
+ * gated vs open) \u2014 adapter-independent, since the auth gate runs before the
14555
+ * handler. Run with: npx tsx tests/${table2}.test.ts
14556
+ */
14557
+ import { dirname, resolve } from "node:path";
14558
+ import { fileURLToPath } from "node:url";
14559
+ import { defaultRouter, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
14560
+
14561
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
14562
+ const here = dirname(fileURLToPath(import.meta.url));
14563
+
14564
+ let pass = 0;
14565
+ let fail = 0;
14566
+ function assert(label: string, ok: boolean): void {
14567
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
14568
+ else { fail++; console.log(\` FAIL \${label}\`); }
14569
+ }
14570
+
14571
+ // Discovering the routes imports src/routes/admin/${table2}/get.ts, whose
14572
+ // top-level Crud.registerBackend() adds the AutoCrud routes to defaultRouter.
14573
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) defaultRouter.addRoute(def);
14574
+ const patterns = defaultRouter.getRoutes().map((r) => \`\${r.method} \${r.pattern}\`);
14575
+ const client = new TestClient();
14576
+
14577
+ assert("AutoCrud GET list route registered", patterns.includes("GET /api/${table2}"));
14578
+ assert("AutoCrud POST route registered", patterns.includes("POST /api/${table2}"));
14579
+ assert("admin page route registered", patterns.includes("GET /admin/${table2}"));
14580
+ ${pageCase}
14581
+ ${writeCase}
14582
+
14583
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
14584
+ process.exit(fail > 0 ? 1 : 0);
14585
+ `;
14586
+ writeFileSafe(join15(dir, `${table2}.test.ts`), content);
14587
+ }
14457
14588
  function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
14458
14589
  const ts = timestamp();
14459
14590
  const dir = resolve6("migrations");
@@ -14595,7 +14726,7 @@ import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
14595
14726
  import { initDatabase } from "tina4-nodejs/orm";
14596
14727
  import ${model} from "../src/models/${model}.js";
14597
14728
 
14598
- process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
14729
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
14599
14730
  const here = dirname(fileURLToPath(import.meta.url));
14600
14731
 
14601
14732
  let pass = 0;
@@ -15558,7 +15689,7 @@ import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
15558
15689
  import { initDatabase } from "tina4-nodejs/orm";
15559
15690
  import User from "../src/models/User.js";
15560
15691
 
15561
- process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
15692
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
15562
15693
  delete process.env.TINA4_API_KEY;
15563
15694
  const here = dirname(fileURLToPath(import.meta.url));`;
15564
15695
  const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
@@ -15745,7 +15876,7 @@ var init_generate = __esm({
15745
15876
  GENERATORS = {
15746
15877
  model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
15747
15878
  route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
15748
- crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
15879
+ crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public] [--no-templates]', summary: "Model + migration + AutoCrud-backed admin page (toCrud) + editable crud/ templates + gate test" },
15749
15880
  migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
15750
15881
  middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
15751
15882
  test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
@@ -15774,8 +15905,9 @@ var init_generate = __esm({
15774
15905
  ],
15775
15906
  crud: ({ name, table: table2 }) => [
15776
15907
  `Apply the migration: npx tina4nodejs migrate`,
15777
- `Serve and try: npx tina4nodejs serve -> visit /swagger`,
15778
- `Run the gate test: npx tsx tests/${pluralizeReserved(toSnake(name))}.test.ts`,
15908
+ `Serve and try: npx tina4nodejs serve -> visit /admin/${table2} (or /swagger)`,
15909
+ `Run the gate test: npx tsx tests/${table2}.test.ts`,
15910
+ `Restyle the UI: edit src/templates/crud/*.twig`,
15779
15911
  `Change fields: edit src/models/${name}.ts then re-run generate crud`
15780
15912
  ],
15781
15913
  migration: () => [
@@ -18724,8 +18856,8 @@ __export(context_exports, {
18724
18856
  fts5Supported: () => fts5Supported
18725
18857
  });
18726
18858
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
18727
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
18728
- import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
18859
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync8, realpathSync as realpathSync5 } from "node:fs";
18860
+ import { basename as basename4, dirname as dirname9, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
18729
18861
  function fts5Supported() {
18730
18862
  try {
18731
18863
  const conn = new DatabaseSync2(":memory:");
@@ -18748,7 +18880,7 @@ function realResolve(abs) {
18748
18880
  } catch {
18749
18881
  }
18750
18882
  try {
18751
- return join17(realpathSync5(dirname8(abs)), basename4(abs));
18883
+ return join17(realpathSync5(dirname9(abs)), basename4(abs));
18752
18884
  } catch {
18753
18885
  return abs;
18754
18886
  }
@@ -18836,7 +18968,7 @@ var init_context = __esm({
18836
18968
  const check = fts5Check ?? fts5Supported;
18837
18969
  this.available = Boolean(check());
18838
18970
  if (!this.available) return;
18839
- const parent = dirname8(this.path);
18971
+ const parent = dirname9(this.path);
18840
18972
  if (parent !== "" && parent !== ".") {
18841
18973
  mkdirSync10(parent, { recursive: true });
18842
18974
  }
@@ -18914,7 +19046,7 @@ var init_context = __esm({
18914
19046
  this.root = rootAbs;
18915
19047
  let total = 0;
18916
19048
  const walk2 = (dir) => {
18917
- const entries = readdirSync7(dir, { withFileTypes: true });
19049
+ const entries = readdirSync8(dir, { withFileTypes: true });
18918
19050
  const files = [];
18919
19051
  const subdirs = [];
18920
19052
  for (const e of entries) {
@@ -20830,7 +20962,7 @@ var init_mongoBackend = __esm({
20830
20962
  return null;
20831
20963
  }
20832
20964
  }
20833
- push(queue, payload, delay, priority) {
20965
+ push(queue, payload, delay2, priority) {
20834
20966
  const id = randomUUID4();
20835
20967
  const now = (/* @__PURE__ */ new Date()).toISOString();
20836
20968
  const job = {
@@ -20839,7 +20971,7 @@ var init_mongoBackend = __esm({
20839
20971
  status: "pending",
20840
20972
  createdAt: now,
20841
20973
  attempts: 0,
20842
- delayUntil: delay ? new Date(Date.now() + delay * 1e3).toISOString() : null,
20974
+ delayUntil: delay2 ? new Date(Date.now() + delay2 * 1e3).toISOString() : null,
20843
20975
  // The pop sort has always been { priority: -1, createdAt: 1 }, but this
20844
20976
  // field was never written, so every job scored undefined and the queue
20845
20977
  // ran pure FIFO. Priority did not even reach here: the backend interface
@@ -21008,7 +21140,7 @@ var init_job = __esm({
21008
21140
  });
21009
21141
 
21010
21142
  // ../core/src/queueBackends/liteBackend.ts
21011
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
21143
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
21012
21144
  import { join as join18 } from "node:path";
21013
21145
  import { randomUUID as randomUUID5 } from "node:crypto";
21014
21146
  var LiteBackend;
@@ -21070,7 +21202,7 @@ var init_liteBackend = __esm({
21070
21202
  */
21071
21203
  close() {
21072
21204
  }
21073
- push(queue, payload, delay, priority) {
21205
+ push(queue, payload, delay2, priority) {
21074
21206
  const dir = this.ensureDir(queue);
21075
21207
  const id = randomUUID5();
21076
21208
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -21080,7 +21212,7 @@ var init_liteBackend = __esm({
21080
21212
  status: "pending",
21081
21213
  createdAt: now,
21082
21214
  attempts: 0,
21083
- delayUntil: delay ? new Date(Date.now() + delay * 1e3).toISOString() : null,
21215
+ delayUntil: delay2 ? new Date(Date.now() + delay2 * 1e3).toISOString() : null,
21084
21216
  priority: priority ?? 0,
21085
21217
  topic: queue,
21086
21218
  error: void 0
@@ -21099,7 +21231,7 @@ var init_liteBackend = __esm({
21099
21231
  const dir = this.ensureDir(queue);
21100
21232
  let filenames;
21101
21233
  try {
21102
- filenames = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21234
+ filenames = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21103
21235
  } catch {
21104
21236
  return [];
21105
21237
  }
@@ -21164,7 +21296,7 @@ var init_liteBackend = __esm({
21164
21296
  const reservedDir = this.ensureReservedDir(queue);
21165
21297
  let filenames;
21166
21298
  try {
21167
- filenames = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
21299
+ filenames = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21168
21300
  } catch {
21169
21301
  return;
21170
21302
  }
@@ -21275,7 +21407,7 @@ var init_liteBackend = __esm({
21275
21407
  const scanDir = isDead ? this.ensureFailedDir(queue) : isReserved ? this.ensureReservedDir(queue) : this.ensureDir(queue);
21276
21408
  let files;
21277
21409
  try {
21278
- files = readdirSync8(scanDir).filter((f) => f.endsWith(".queue-data"));
21410
+ files = readdirSync9(scanDir).filter((f) => f.endsWith(".queue-data"));
21279
21411
  } catch {
21280
21412
  return 0;
21281
21413
  }
@@ -21296,7 +21428,7 @@ var init_liteBackend = __esm({
21296
21428
  const dir = this.ensureDir(queue);
21297
21429
  let count = 0;
21298
21430
  try {
21299
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21431
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21300
21432
  for (const file of files) {
21301
21433
  unlinkSync6(join18(dir, file));
21302
21434
  count++;
@@ -21306,7 +21438,7 @@ var init_liteBackend = __esm({
21306
21438
  const failedDir = join18(dir, "failed");
21307
21439
  try {
21308
21440
  if (existsSync16(failedDir)) {
21309
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21441
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21310
21442
  for (const file of files) {
21311
21443
  unlinkSync6(join18(failedDir, file));
21312
21444
  count++;
@@ -21317,7 +21449,7 @@ var init_liteBackend = __esm({
21317
21449
  const reservedDir = join18(dir, "reserved");
21318
21450
  try {
21319
21451
  if (existsSync16(reservedDir)) {
21320
- const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
21452
+ const files = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21321
21453
  for (const file of files) {
21322
21454
  unlinkSync6(join18(reservedDir, file));
21323
21455
  count++;
@@ -21339,7 +21471,7 @@ var init_liteBackend = __esm({
21339
21471
  const dir = this.ensureDir(queue);
21340
21472
  const results = [];
21341
21473
  try {
21342
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
21474
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data")).sort();
21343
21475
  for (const file of files) {
21344
21476
  try {
21345
21477
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
@@ -21363,7 +21495,7 @@ var init_liteBackend = __esm({
21363
21495
  */
21364
21496
  retry(queue, jobId, delaySeconds) {
21365
21497
  try {
21366
- const queues = readdirSync8(this.basePath);
21498
+ const queues = readdirSync9(this.basePath);
21367
21499
  for (const q of queues) {
21368
21500
  const failedDir = join18(this.basePath, q, "failed");
21369
21501
  const filePath = join18(failedDir, `${jobId}.queue-data`);
@@ -21389,7 +21521,7 @@ var init_liteBackend = __esm({
21389
21521
  const failedDir = this.ensureFailedDir(queue);
21390
21522
  const results = [];
21391
21523
  try {
21392
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
21524
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
21393
21525
  for (const file of files) {
21394
21526
  try {
21395
21527
  const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
@@ -21410,7 +21542,7 @@ var init_liteBackend = __esm({
21410
21542
  if (isDead) {
21411
21543
  const failedDir = this.ensureFailedDir(queue);
21412
21544
  try {
21413
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21545
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21414
21546
  for (const file of files) {
21415
21547
  try {
21416
21548
  unlinkSync6(join18(failedDir, file));
@@ -21423,7 +21555,7 @@ var init_liteBackend = __esm({
21423
21555
  } else {
21424
21556
  const dir = this.ensureDir(queue);
21425
21557
  try {
21426
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21558
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21427
21559
  for (const file of files) {
21428
21560
  try {
21429
21561
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
@@ -21449,7 +21581,7 @@ var init_liteBackend = __esm({
21449
21581
  const queueDir = this.ensureDir(queue);
21450
21582
  let count = 0;
21451
21583
  try {
21452
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21584
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21453
21585
  for (const file of files) {
21454
21586
  try {
21455
21587
  const filePath = join18(failedDir, file);
@@ -21476,7 +21608,7 @@ var init_liteBackend = __esm({
21476
21608
  const dir = this.ensureDir(queue);
21477
21609
  let files;
21478
21610
  try {
21479
- files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21611
+ files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21480
21612
  } catch {
21481
21613
  return null;
21482
21614
  }
@@ -21717,11 +21849,11 @@ var init_queue = __esm({
21717
21849
  *
21718
21850
  * @param priority — Higher value = higher priority. Default 0.
21719
21851
  */
21720
- push(payload, delay, priority = 0) {
21852
+ push(payload, delay2, priority = 0) {
21721
21853
  if (this.externalBackend) {
21722
- return this.externalBackend.push(this.topic, payload, delay, priority);
21854
+ return this.externalBackend.push(this.topic, payload, delay2, priority);
21723
21855
  }
21724
- return this.liteBackend.push(this.topic, payload, delay, priority);
21856
+ return this.liteBackend.push(this.topic, payload, delay2, priority);
21725
21857
  }
21726
21858
  /**
21727
21859
  * Atomically claim the next available job from this queue's topic. Returns null if empty.
@@ -21952,11 +22084,11 @@ var init_queue = __esm({
21952
22084
  * shares the same retarget path consume()/process() use — keeping produce and
21953
22085
  * consume symmetric on the same topic argument.
21954
22086
  */
21955
- produce(topic, payload, priority = 0, delay = 0) {
22087
+ produce(topic, payload, priority = 0, delay2 = 0) {
21956
22088
  const previous = this.topic;
21957
22089
  this.retarget(topic);
21958
22090
  try {
21959
- return this.push(payload, delay, priority);
22091
+ return this.push(payload, delay2, priority);
21960
22092
  } finally {
21961
22093
  this.retarget(previous);
21962
22094
  }
@@ -22957,7 +23089,7 @@ var init_projectIndex = __esm({
22957
23089
  }
22958
23090
  }
22959
23091
  hits.sort((a, b) => b[0] - a[0]);
22960
- return hits.slice(0, Math.max(1, limit)).map((h) => h[1]);
23092
+ return hits.slice(0, Math.max(1, limit)).map((h2) => h2[1]);
22961
23093
  },
22962
23094
  fileEntry(relPath) {
22963
23095
  ProjectIndex.refresh();
@@ -23486,7 +23618,7 @@ __export(docs_exports, {
23486
23618
  });
23487
23619
  import * as fs8 from "node:fs";
23488
23620
  import * as path8 from "node:path";
23489
- import { fileURLToPath as fileURLToPath4 } from "node:url";
23621
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
23490
23622
  function detectFrameworkRoots() {
23491
23623
  const corePackageRoot = path8.resolve(__DIRNAME, "..");
23492
23624
  const packagesRoot = path8.resolve(corePackageRoot, "..");
@@ -23893,7 +24025,7 @@ var init_docs = __esm({
23893
24025
  "hash"
23894
24026
  ]);
23895
24027
  _DOCS_CACHE = /* @__PURE__ */ new Map();
23896
- __FILENAME = fileURLToPath4(import.meta.url);
24028
+ __FILENAME = fileURLToPath5(import.meta.url);
23897
24029
  __DIRNAME = path8.dirname(__FILENAME);
23898
24030
  USER_DIRS = ["src/orm", "src/routes", "src/app", "src/services", "src/models"];
23899
24031
  CAMEL_SPLIT = /([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
@@ -24270,11 +24402,12 @@ ${end}
24270
24402
 
24271
24403
  // ../core/src/devAdmin.ts
24272
24404
  import { cpus as osCpus } from "node:os";
24273
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24274
- import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24275
- import { fileURLToPath as fileURLToPath5 } from "node:url";
24405
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync13, mkdirSync as mkdirSync14, copyFileSync as copyFileSync2, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24406
+ import { join as join22, dirname as dirname11, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24407
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
24276
24408
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
24277
- function escapeHtml(value) {
24409
+ import net2 from "node:net";
24410
+ function escapeHtml2(value) {
24278
24411
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
24279
24412
  }
24280
24413
  function devSameOriginOk(req2) {
@@ -24312,6 +24445,42 @@ function devHostAllowed(hostHeader) {
24312
24445
  if (configured) allowed.add(configured);
24313
24446
  return allowed.has(name);
24314
24447
  }
24448
+ function ipInCidr(ip, entry) {
24449
+ try {
24450
+ ip = (ip ?? "").trim();
24451
+ if (ip.toLowerCase().startsWith("::ffff:")) ip = ip.slice(7);
24452
+ const ipFam = net2.isIP(ip);
24453
+ if (!ipFam) return false;
24454
+ entry = (entry ?? "").trim();
24455
+ const bl = new net2.BlockList();
24456
+ if (entry.includes("/")) {
24457
+ const [addr, bitsStr] = entry.split("/");
24458
+ const bits = Number(bitsStr);
24459
+ const fam = net2.isIP(addr);
24460
+ if (!fam || !Number.isInteger(bits)) return false;
24461
+ bl.addSubnet(addr, bits, fam === 4 ? "ipv4" : "ipv6");
24462
+ } else {
24463
+ const fam = net2.isIP(entry);
24464
+ if (!fam) return false;
24465
+ bl.addAddress(entry, fam === 4 ? "ipv4" : "ipv6");
24466
+ }
24467
+ return bl.check(ip, ipFam === 4 ? "ipv4" : "ipv6");
24468
+ } catch {
24469
+ return false;
24470
+ }
24471
+ }
24472
+ function devPeerAllowed(remoteIp) {
24473
+ const ip = (remoteIp ?? "").trim();
24474
+ if (isLoopback(ip)) return true;
24475
+ const configured = (process.env.TINA4_DEV_ALLOWED_PEERS ?? "").trim();
24476
+ if (!configured) return false;
24477
+ return configured.split(",").some((entry) => entry.trim() !== "" && ipInCidr(ip, entry));
24478
+ }
24479
+ function devToolbarAllowed(req2) {
24480
+ if (!devHostAllowed(req2.header("host"))) return false;
24481
+ const peer = req2.socket?.remoteAddress ?? "";
24482
+ return devPeerAllowed(peer);
24483
+ }
24315
24484
  function devRequestDenial(req2) {
24316
24485
  const path10 = (req2.path ?? "").replace(/\/+/g, "/");
24317
24486
  const isMcp = DEV_MCP_PREFIXES.some((prefix) => path10 === prefix || path10.startsWith(prefix + "/"));
@@ -24321,9 +24490,12 @@ function devRequestDenial(req2) {
24321
24490
  if (!devSameOriginOk(req2)) {
24322
24491
  return { status: 403, error: "dev-admin: refused (cross-origin request)" };
24323
24492
  }
24493
+ if (DEV_PUBLIC_ASSETS.has(path10)) {
24494
+ return null;
24495
+ }
24324
24496
  if (!isMcp) {
24325
24497
  const peer = req2.socket?.remoteAddress ?? "";
24326
- if (!(isLoopback(peer) || mcpTokenOk(req2, true))) {
24498
+ if (!(devPeerAllowed(peer) || mcpTokenOk(req2, true))) {
24327
24499
  return { status: 403, error: "dev-admin: refused (non-loopback peer)" };
24328
24500
  }
24329
24501
  }
@@ -24412,7 +24584,7 @@ function mapQueueJob(job, topic, status2) {
24412
24584
  function readQueueDir(dir, topic, status2) {
24413
24585
  if (!existsSync20(dir)) return [];
24414
24586
  const jobs = [];
24415
- for (const filename of readdirSync12(dir).sort()) {
24587
+ for (const filename of readdirSync13(dir).sort()) {
24416
24588
  if (!filename.endsWith(".queue-data")) continue;
24417
24589
  try {
24418
24590
  jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
@@ -24554,12 +24726,12 @@ function upsertDevEnvVar(key, value) {
24554
24726
  }
24555
24727
  function formatUptime(seconds) {
24556
24728
  const d = Math.floor(seconds / 86400);
24557
- const h = Math.floor(seconds % 86400 / 3600);
24729
+ const h2 = Math.floor(seconds % 86400 / 3600);
24558
24730
  const m = Math.floor(seconds % 3600 / 60);
24559
24731
  const s = Math.floor(seconds % 60);
24560
24732
  const parts = [];
24561
24733
  if (d > 0) parts.push(`${d}d`);
24562
- if (h > 0) parts.push(`${h}h`);
24734
+ if (h2 > 0) parts.push(`${h2}h`);
24563
24735
  if (m > 0) parts.push(`${m}m`);
24564
24736
  parts.push(`${s}s`);
24565
24737
  return parts.join(" ");
@@ -24580,7 +24752,7 @@ function parseEnvFile() {
24580
24752
  function walkDirRecursive(dir) {
24581
24753
  const results = [];
24582
24754
  if (!existsSync20(dir)) return results;
24583
- for (const entry of readdirSync12(dir)) {
24755
+ for (const entry of readdirSync13(dir)) {
24584
24756
  const full = join22(dir, entry);
24585
24757
  if (statSync15(full).isDirectory()) {
24586
24758
  results.push(...walkDirRecursive(full));
@@ -24610,8 +24782,8 @@ function handleGalleryDeploy(router) {
24610
24782
  for (const srcFile of allFiles) {
24611
24783
  const rel = relative9(gallerySrc, srcFile);
24612
24784
  const dest = join22(projectSrc, rel);
24613
- mkdirSync14(dirname10(dest), { recursive: true });
24614
- copyFileSync(srcFile, dest);
24785
+ mkdirSync14(dirname11(dest), { recursive: true });
24786
+ copyFileSync2(srcFile, dest);
24615
24787
  copied.push(rel);
24616
24788
  }
24617
24789
  try {
@@ -24641,9 +24813,9 @@ function realPathOf(target) {
24641
24813
  present = true;
24642
24814
  } catch {
24643
24815
  }
24644
- if (present || dirname10(existing) === existing) break;
24816
+ if (present || dirname11(existing) === existing) break;
24645
24817
  tail.unshift(basename5(existing));
24646
- existing = dirname10(existing);
24818
+ existing = dirname11(existing);
24647
24819
  }
24648
24820
  return join22(realpathSync6(existing), ...tail);
24649
24821
  }
@@ -24767,9 +24939,9 @@ function applyMcpOutcome(res, outcome) {
24767
24939
  }
24768
24940
  function renderToolbarHtml(ctx) {
24769
24941
  const nodeVersion = process.version;
24770
- const method = escapeHtml(ctx.method);
24771
- const path10 = escapeHtml(ctx.path);
24772
- const matchedPattern = escapeHtml(ctx.matchedPattern);
24942
+ const method = escapeHtml2(ctx.method);
24943
+ const path10 = escapeHtml2(ctx.path);
24944
+ const matchedPattern = escapeHtml2(ctx.matchedPattern);
24773
24945
  const reload = ctx.reload === false ? "0" : "1";
24774
24946
  return `<link rel="stylesheet" href="/__dev/toolbar.css">
24775
24947
  <div id="tina4-dev-toolbar" data-reload="${reload}">
@@ -24955,7 +25127,7 @@ function toolbarJs() {
24955
25127
  connect();
24956
25128
  })();`;
24957
25129
  }
24958
- var cpuCount, DEV_MCP_PREFIXES, 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;
25130
+ var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
24959
25131
  var init_devAdmin = __esm({
24960
25132
  "../core/src/devAdmin.ts"() {
24961
25133
  "use strict";
@@ -24968,6 +25140,7 @@ var init_devAdmin = __esm({
24968
25140
  init_version();
24969
25141
  cpuCount = osCpus().length;
24970
25142
  DEV_MCP_PREFIXES = ["/__dev/api/mcp", "/__dev/mcp"];
25143
+ DEV_PUBLIC_ASSETS = /* @__PURE__ */ new Set(["/__dev/toolbar.css", "/__dev/toolbar.js"]);
24971
25144
  DEV_SECRET_BASENAMES = /* @__PURE__ */ new Set([
24972
25145
  ".env",
24973
25146
  ".envrc",
@@ -25669,7 +25842,7 @@ var init_devAdmin = __esm({
25669
25842
  const queueDir = queueBasePath2();
25670
25843
  let topics = [];
25671
25844
  if (existsSync20(queueDir)) {
25672
- topics = readdirSync12(queueDir).filter((d) => {
25845
+ topics = readdirSync13(queueDir).filter((d) => {
25673
25846
  try {
25674
25847
  return statSync15(join22(queueDir, d)).isDirectory();
25675
25848
  } catch {
@@ -26173,13 +26346,13 @@ var init_devAdmin = __esm({
26173
26346
  res.json({ success: false, error: msg });
26174
26347
  }
26175
26348
  };
26176
- __devAdminFilename = fileURLToPath5(import.meta.url);
26177
- __devAdminDirname = dirname10(__devAdminFilename);
26349
+ __devAdminFilename = fileURLToPath6(import.meta.url);
26350
+ __devAdminDirname = dirname11(__devAdminFilename);
26178
26351
  handleGalleryList = (_req, res) => {
26179
26352
  const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
26180
26353
  const items = [];
26181
26354
  if (existsSync20(galleryDir)) {
26182
- const entries = readdirSync12(galleryDir).sort();
26355
+ const entries = readdirSync13(galleryDir).sort();
26183
26356
  for (const entry of entries) {
26184
26357
  const entryPath = join22(galleryDir, entry);
26185
26358
  const metaFile = join22(entryPath, "meta.json");
@@ -26283,7 +26456,7 @@ var init_devAdmin = __esm({
26283
26456
  if (cwdInGit !== "") cwdInGit += "/";
26284
26457
  }
26285
26458
  const entries = [];
26286
- for (const name of readdirSync12(target).sort()) {
26459
+ for (const name of readdirSync13(target).sort()) {
26287
26460
  if (devFilesHidden(name)) continue;
26288
26461
  const full = join22(target, name);
26289
26462
  const entryRel = relative9(root, full).replace(/\\/g, "/");
@@ -26314,7 +26487,7 @@ var init_devAdmin = __esm({
26314
26487
  if (isDir) {
26315
26488
  hasChildren = false;
26316
26489
  try {
26317
- for (const c of readdirSync12(full)) {
26490
+ for (const c of readdirSync13(full)) {
26318
26491
  if (devFilesHidden(c)) continue;
26319
26492
  hasChildren = true;
26320
26493
  break;
@@ -26406,7 +26579,7 @@ var init_devAdmin = __esm({
26406
26579
  return;
26407
26580
  }
26408
26581
  try {
26409
- mkdirSync14(dirname10(target), { recursive: true });
26582
+ mkdirSync14(dirname11(target), { recursive: true });
26410
26583
  const existed = existsSync20(target);
26411
26584
  writeFileSync13(target, content, "utf-8");
26412
26585
  try {
@@ -26478,7 +26651,7 @@ var init_devAdmin = __esm({
26478
26651
  }
26479
26652
  try {
26480
26653
  const { renameSync: renameSync3 } = await import("node:fs");
26481
- mkdirSync14(dirname10(dst), { recursive: true });
26654
+ mkdirSync14(dirname11(dst), { recursive: true });
26482
26655
  renameSync3(src, dst);
26483
26656
  res.json({ ok: true, from: relative9(root, src), to: relative9(root, dst) });
26484
26657
  } catch (e) {
@@ -26499,8 +26672,8 @@ var init_devAdmin = __esm({
26499
26672
  return;
26500
26673
  }
26501
26674
  try {
26502
- const { rmSync } = await import("node:fs");
26503
- rmSync(target, { recursive: true, force: true });
26675
+ const { rmSync: rmSync2 } = await import("node:fs");
26676
+ rmSync2(target, { recursive: true, force: true });
26504
26677
  res.json({ ok: true, deleted: relative9(root, target) });
26505
26678
  } catch (e) {
26506
26679
  res.json({ error: e.message }, 500);
@@ -26835,10 +27008,10 @@ var init_devAdmin = __esm({
26835
27008
  });
26836
27009
  };
26837
27010
  handleDevAdminJs = async (_req, res) => {
26838
- const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
26839
- const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
26840
- const { fileURLToPath: fileURLToPath8 } = await import("node:url");
26841
- const dir = dirname15(fileURLToPath8(import.meta.url));
27011
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
27012
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
27013
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
27014
+ const dir = dirname17(fileURLToPath10(import.meta.url));
26842
27015
  const candidates = [
26843
27016
  join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
26844
27017
  // src/../public/js/
@@ -26849,7 +27022,7 @@ var init_devAdmin = __esm({
26849
27022
  // project public/
26850
27023
  ];
26851
27024
  for (const jsPath of candidates) {
26852
- if (existsSync28(jsPath)) {
27025
+ if (existsSync29(jsPath)) {
26853
27026
  try {
26854
27027
  const content = readFileSync27(jsPath, "utf-8");
26855
27028
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
@@ -26874,7 +27047,7 @@ var init_devAdmin = __esm({
26874
27047
  });
26875
27048
 
26876
27049
  // ../core/src/i18n.ts
26877
- import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
27050
+ import { readFileSync as readFileSync19, readdirSync as readdirSync14, existsSync as existsSync21 } from "node:fs";
26878
27051
  import { join as join23, resolve as resolve13 } from "node:path";
26879
27052
  var I18n;
26880
27053
  var init_i18n = __esm({
@@ -26956,7 +27129,7 @@ var init_i18n = __esm({
26956
27129
  return [this._defaultLocale];
26957
27130
  }
26958
27131
  try {
26959
- const files = readdirSync13(this._localeDir);
27132
+ const files = readdirSync14(this._localeDir);
26960
27133
  const locales = files.filter((f) => f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")).map((f) => f.replace(/\.(json|ya?ml)$/, "")).sort();
26961
27134
  return locales.length > 0 ? locales : [this._defaultLocale];
26962
27135
  } catch {
@@ -28212,10 +28385,10 @@ var init_docsAutoDiscovery = __esm({
28212
28385
 
28213
28386
  // ../core/src/server.ts
28214
28387
  import { randomBytes as randomBytes7 } from "node:crypto";
28215
- import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative10 } from "node:path";
28216
- import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28388
+ import { resolve as resolve15, dirname as dirname12, join as join25, relative as relative10 } from "node:path";
28389
+ import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28217
28390
  import { isatty } from "node:tty";
28218
- import { fileURLToPath as fileURLToPath6 } from "node:url";
28391
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
28219
28392
  import { execFileSync as execFileSync3, exec } from "node:child_process";
28220
28393
  import cluster from "node:cluster";
28221
28394
  import os2 from "node:os";
@@ -28255,7 +28428,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
28255
28428
  if (!existsSync23(dir)) return;
28256
28429
  let hasSql = false;
28257
28430
  try {
28258
- hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
28431
+ hasSql = readdirSync15(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
28259
28432
  } catch {
28260
28433
  return;
28261
28434
  }
@@ -28425,7 +28598,7 @@ function injectDevToolbar(html, ctx) {
28425
28598
  function walkGalleryFiles(dir) {
28426
28599
  const results = [];
28427
28600
  if (!existsSync23(dir)) return results;
28428
- for (const f of readdirSync14(dir)) {
28601
+ for (const f of readdirSync15(dir)) {
28429
28602
  const full = join25(dir, f);
28430
28603
  if (statSync16(full).isDirectory()) results.push(...walkGalleryFiles(full));
28431
28604
  else results.push(full);
@@ -28437,7 +28610,7 @@ function getGalleryDeployedState() {
28437
28610
  const state = {};
28438
28611
  if (!existsSync23(galleryDir)) return state;
28439
28612
  try {
28440
- const entries = readdirSync14(galleryDir).sort();
28613
+ const entries = readdirSync15(galleryDir).sort();
28441
28614
  for (const entry of entries) {
28442
28615
  const entryPath = join25(galleryDir, entry);
28443
28616
  const metaFile = join25(entryPath, "meta.json");
@@ -28488,7 +28661,7 @@ function resolveTemplate(pathname, templatesDir) {
28488
28661
  const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
28489
28662
  if (existsSync23(pagesDir)) {
28490
28663
  const scan = (dir, prefix) => {
28491
- for (const entry of readdirSync14(dir, { withFileTypes: true })) {
28664
+ for (const entry of readdirSync15(dir, { withFileTypes: true })) {
28492
28665
  if (entry.name.startsWith("_")) continue;
28493
28666
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
28494
28667
  if (entry.isDirectory()) {
@@ -28807,7 +28980,7 @@ function isInjectableHtml(res) {
28807
28980
  return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
28808
28981
  }
28809
28982
  function injectIntoHtml(ctx, devToolbar, html) {
28810
- if (!devToolbar) return injectFeedbackWidget(ctx.req, html);
28983
+ if (!devToolbar || !devToolbarAllowed(ctx.req)) return injectFeedbackWidget(ctx.req, html);
28811
28984
  const toolbarCtx = {
28812
28985
  version: TINA4_VERSION,
28813
28986
  method: ctx.req.method ?? "GET",
@@ -29175,7 +29348,7 @@ function wireLocaleGlobal(frondEngine, base) {
29175
29348
  const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
29176
29349
  if (!existsSync23(localeDir)) return;
29177
29350
  try {
29178
- const localeFiles = readdirSync14(localeDir).filter((file) => file.endsWith(".json"));
29351
+ const localeFiles = readdirSync15(localeDir).filter((file) => file.endsWith(".json"));
29179
29352
  if (localeFiles.length === 0 || frondEngine.globals?.t) return;
29180
29353
  const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
29181
29354
  frondEngine.addGlobal("t", (key, params) => i18nInstance.t(key, params));
@@ -29590,8 +29763,8 @@ var init_server = __esm({
29590
29763
  init_i18n();
29591
29764
  init_background();
29592
29765
  init_version();
29593
- __filename = fileURLToPath6(import.meta.url);
29594
- __dirname = dirname11(__filename);
29766
+ __filename = fileURLToPath7(import.meta.url);
29767
+ __dirname = dirname12(__filename);
29595
29768
  BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
29596
29769
  BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
29597
29770
  swaggerAssetsEnabled = false;
@@ -29753,7 +29926,7 @@ var init_env = __esm({
29753
29926
 
29754
29927
  // ../core/src/fakeData.ts
29755
29928
  import { randomInt, randomUUID as randomUUID6 } from "node:crypto";
29756
- import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
29929
+ import { existsSync as existsSync24, readdirSync as readdirSync16 } from "node:fs";
29757
29930
  import { resolve as resolve16, join as join26 } from "node:path";
29758
29931
  function mulberry32(seed) {
29759
29932
  let s = seed | 0;
@@ -30253,7 +30426,7 @@ var init_fakeData = __esm({
30253
30426
  async seedDir(seedDir) {
30254
30427
  const dir = resolve16(seedDir ?? "src/seeds");
30255
30428
  if (!existsSync24(dir)) return [];
30256
- const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
30429
+ const files = readdirSync16(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
30257
30430
  const executed = [];
30258
30431
  for (const file of files) {
30259
30432
  const fullPath = join26(dir, file);
@@ -30359,7 +30532,7 @@ var init_mqttMessage = __esm({
30359
30532
  });
30360
30533
 
30361
30534
  // ../core/src/mqtt.ts
30362
- import net2 from "node:net";
30535
+ import net3 from "node:net";
30363
30536
  import tls from "node:tls";
30364
30537
  import { randomBytes as randomBytes8 } from "node:crypto";
30365
30538
  import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
@@ -30830,7 +31003,7 @@ var init_mqtt = __esm({
30830
31003
  if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
30831
31004
  sock = tls.connect(opts, () => settle(() => resolve21(sock)));
30832
31005
  } else {
30833
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
31006
+ sock = net3.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
30834
31007
  }
30835
31008
  sock.once("error", (err) => {
30836
31009
  settle(() => {
@@ -31047,7 +31220,7 @@ var init_mqtt = __esm({
31047
31220
  });
31048
31221
 
31049
31222
  // ../core/src/service.ts
31050
- import { readdirSync as readdirSync16, statSync as statSync17, watchFile, unwatchFile } from "node:fs";
31223
+ import { readdirSync as readdirSync17, statSync as statSync17, watchFile, unwatchFile } from "node:fs";
31051
31224
  import { join as join27, extname as extname7 } from "node:path";
31052
31225
  import { pathToFileURL } from "node:url";
31053
31226
  function matchCronField(field, value) {
@@ -31215,7 +31388,7 @@ var init_service = __esm({
31215
31388
  const discovered = [];
31216
31389
  let entries;
31217
31390
  try {
31218
- entries = readdirSync16(dir);
31391
+ entries = readdirSync17(dir);
31219
31392
  } catch {
31220
31393
  return discovered;
31221
31394
  }
@@ -31342,7 +31515,7 @@ var init_service = __esm({
31342
31515
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
31343
31516
  let entries;
31344
31517
  try {
31345
- entries = readdirSync16(dir);
31518
+ entries = readdirSync17(dir);
31346
31519
  } catch {
31347
31520
  return;
31348
31521
  }
@@ -31449,9 +31622,9 @@ function ipToBytes(ip) {
31449
31622
  }
31450
31623
  function parseCidr(cidr) {
31451
31624
  const [netStr, bitsStr] = cidr.split("/");
31452
- const net4 = ipToBytes(netStr);
31453
- if (!net4) return null;
31454
- return { net: net4, bits: Number(bitsStr) };
31625
+ const net5 = ipToBytes(netStr);
31626
+ if (!net5) return null;
31627
+ return { net: net5, bits: Number(bitsStr) };
31455
31628
  }
31456
31629
  function inCidr(packed, cidr) {
31457
31630
  if (packed.length !== cidr.net.length) return false;
@@ -32030,11 +32203,11 @@ var init_api = __esm({
32030
32203
  path: null
32031
32204
  };
32032
32205
  }
32033
- const net4 = await this.performRequest("GET", url, headers, data, MAX_REDIRECTS);
32034
- if (net4.kind === "error") {
32035
- return { http_code: null, headers: {}, error: net4.error, path: null };
32206
+ const net5 = await this.performRequest("GET", url, headers, data, MAX_REDIRECTS);
32207
+ if (net5.kind === "error") {
32208
+ return { http_code: null, headers: {}, error: net5.error, path: null };
32036
32209
  }
32037
- const res = net4.res;
32210
+ const res = net5.res;
32038
32211
  const code = res.statusCode ?? null;
32039
32212
  const respHeaders = flattenHeaders(res.headers);
32040
32213
  if (code === null || code >= 400) {
@@ -32292,11 +32465,11 @@ var init_api = __esm({
32292
32465
  if (this.transportFn) {
32293
32466
  return this.callTransport(method, url, headers, data);
32294
32467
  }
32295
- const net4 = await this.performRequest(method, url, headers, data, MAX_REDIRECTS);
32296
- if (net4.kind === "error") {
32297
- return { http_code: null, body: null, headers: {}, error: net4.error };
32468
+ const net5 = await this.performRequest(method, url, headers, data, MAX_REDIRECTS);
32469
+ if (net5.kind === "error") {
32470
+ return { http_code: null, body: null, headers: {}, error: net5.error };
32298
32471
  }
32299
- return this.readResponse(net4.res);
32472
+ return this.readResponse(net5.res);
32300
32473
  }
32301
32474
  /**
32302
32475
  * Invoke a user-injected transport and normalize its result. The transport
@@ -32473,7 +32646,7 @@ var init_api = __esm({
32473
32646
  });
32474
32647
 
32475
32648
  // ../core/src/messenger.ts
32476
- import net3 from "node:net";
32649
+ import net4 from "node:net";
32477
32650
  import tls2 from "node:tls";
32478
32651
  import { readFileSync as readFileSync23 } from "node:fs";
32479
32652
  import { basename as basename7 } from "node:path";
@@ -32485,6 +32658,32 @@ function normaliseEncryption(value, kind) {
32485
32658
  }
32486
32659
  return normalised;
32487
32660
  }
32661
+ function resolveSendTimeoutMs(optionTimeout) {
32662
+ if (optionTimeout === void 0 || optionTimeout === null) {
32663
+ return resolveEnvTimeoutSeconds(process.env.TINA4_MAIL_TIMEOUT) * 1e3;
32664
+ }
32665
+ if (!Number.isInteger(optionTimeout) || optionTimeout < 1) {
32666
+ throw new Error(
32667
+ `Messenger timeout must be a whole number of seconds and at least 1, got ${optionTimeout}.`
32668
+ );
32669
+ }
32670
+ return optionTimeout * 1e3;
32671
+ }
32672
+ function resolveEnvTimeoutSeconds(raw) {
32673
+ if (!raw || raw.trim() === "") return DEFAULT_TIMEOUT_SECONDS;
32674
+ const trimmed = raw.trim();
32675
+ const seconds = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
32676
+ if (Number.isInteger(seconds) && seconds >= 1) return seconds;
32677
+ warnBadMailTimeout(raw);
32678
+ return DEFAULT_TIMEOUT_SECONDS;
32679
+ }
32680
+ function warnBadMailTimeout(raw) {
32681
+ if (_timeoutWarned.has(raw)) return;
32682
+ _timeoutWarned.add(raw);
32683
+ Log.warning(
32684
+ `TINA4_MAIL_TIMEOUT must be a whole number of seconds and at least 1, got ${JSON.stringify(raw)} - using the default of ${DEFAULT_TIMEOUT_SECONDS} seconds`
32685
+ );
32686
+ }
32488
32687
  function failOnIdle(socket, timeoutMs, what) {
32489
32688
  socket.setTimeout(timeoutMs, () => {
32490
32689
  socket.destroy(new Error(`${what} timed out after ${timeoutMs / 1e3}s`));
@@ -32496,7 +32695,7 @@ function connectVerifiedTls(host, port, socket) {
32496
32695
  host,
32497
32696
  port,
32498
32697
  socket,
32499
- servername: net3.isIP(host) ? void 0 : host,
32698
+ servername: net4.isIP(host) ? void 0 : host,
32500
32699
  rejectUnauthorized: true
32501
32700
  });
32502
32701
  return new Promise((resolve21, reject) => {
@@ -32508,7 +32707,7 @@ function connectVerifiedTls(host, port, socket) {
32508
32707
  });
32509
32708
  }
32510
32709
  function connectPlain(host, port) {
32511
- const socket = net3.createConnection({ host, port });
32710
+ const socket = net4.createConnection({ host, port });
32512
32711
  return new Promise((resolve21, reject) => {
32513
32712
  socket.once("connect", () => {
32514
32713
  socket.removeListener("error", reject);
@@ -32746,7 +32945,7 @@ function decodeTransfer(body, encoding) {
32746
32945
  }
32747
32946
  }
32748
32947
  if (enc === "quoted-printable") {
32749
- return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
32948
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h2) => String.fromCharCode(parseInt(h2, 16)));
32750
32949
  }
32751
32950
  return body;
32752
32951
  }
@@ -32863,7 +33062,7 @@ function parseFullMessage(uid, response) {
32863
33062
  function createMessenger() {
32864
33063
  return new Messenger();
32865
33064
  }
32866
- var MAIL_ENCRYPTION_VALUES, SMTP_SEND_TIMEOUT_MS, SMTP_CHECK_TIMEOUT_MS, MessengerConnectionError, SmtpCommandError, Messenger, imapTagCounter;
33065
+ var MAIL_ENCRYPTION_VALUES, SMTP_SEND_TIMEOUT_MS, SMTP_CHECK_TIMEOUT_MS, _timeoutWarned, DEFAULT_TIMEOUT_SECONDS, MessengerConnectionError, SmtpCommandError, Messenger, imapTagCounter;
32867
33066
  var init_messenger = __esm({
32868
33067
  "../core/src/messenger.ts"() {
32869
33068
  "use strict";
@@ -32873,6 +33072,8 @@ var init_messenger = __esm({
32873
33072
  MAIL_ENCRYPTION_VALUES = ["ssl", "tls", "starttls", "none"];
32874
33073
  SMTP_SEND_TIMEOUT_MS = 3e4;
32875
33074
  SMTP_CHECK_TIMEOUT_MS = 1e4;
33075
+ _timeoutWarned = /* @__PURE__ */ new Set();
33076
+ DEFAULT_TIMEOUT_SECONDS = 30;
32876
33077
  MessengerConnectionError = class extends Error {
32877
33078
  constructor(message) {
32878
33079
  super(message);
@@ -32902,6 +33103,8 @@ var init_messenger = __esm({
32902
33103
  imapUser;
32903
33104
  imapPass;
32904
33105
  imapEncryption;
33106
+ /** Resolved SMTP send timeout, in milliseconds (see the constructor). */
33107
+ timeoutMs;
32905
33108
  constructor(options) {
32906
33109
  this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
32907
33110
  this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
@@ -32923,6 +33126,7 @@ var init_messenger = __esm({
32923
33126
  this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
32924
33127
  this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
32925
33128
  this.imapEncryption = normaliseEncryption(options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls", "IMAP");
33129
+ this.timeoutMs = resolveSendTimeoutMs(options?.timeout);
32926
33130
  }
32927
33131
  /**
32928
33132
  * How the SMTP connection is secured (ADR-0071 section 1). Port 465 is the
@@ -33058,7 +33262,7 @@ var init_messenger = __esm({
33058
33262
  async sendSmtp(options, recipients, messageId) {
33059
33263
  let socket = null;
33060
33264
  try {
33061
- socket = await this.openSmtpSession();
33265
+ socket = await this.openSmtpSession(this.timeoutMs);
33062
33266
  await this.authenticateSmtp(socket);
33063
33267
  await this.sendSmtpEnvelope(socket, recipients.allRecipients);
33064
33268
  await this.sendSmtpMessage(socket, options, recipients, messageId);
@@ -34107,13 +34311,13 @@ var init_htmlElement = __esm({
34107
34311
  // ../core/src/ai.ts
34108
34312
  import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
34109
34313
  import { homedir } from "node:os";
34110
- import { join as join28, resolve as resolve17, relative as relative11, dirname as dirname12 } from "node:path";
34111
- import { fileURLToPath as fileURLToPath7 } from "node:url";
34314
+ import { join as join28, resolve as resolve17, relative as relative11, dirname as dirname13 } from "node:path";
34315
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
34112
34316
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
34113
34317
  import { createInterface } from "node:readline";
34114
34318
  function readVersion() {
34115
34319
  try {
34116
- const thisDir = dirname12(fileURLToPath7(import.meta.url));
34320
+ const thisDir = dirname13(fileURLToPath8(import.meta.url));
34117
34321
  const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
34118
34322
  const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
34119
34323
  return pkg.version ?? "0.0.0";
@@ -34329,7 +34533,7 @@ function replaceMarkerBlock(existing, block, start2, end) {
34329
34533
  }
34330
34534
  function looksLikeOldFrameworkInstall(existing) {
34331
34535
  const head = existing.replace(/^\s+/, "").slice(0, 400);
34332
- return OLD_FRAMEWORK_HEADERS.some((h) => head.startsWith(h));
34536
+ return OLD_FRAMEWORK_HEADERS.some((h2) => head.startsWith(h2));
34333
34537
  }
34334
34538
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
34335
34539
  const block = skillBlock(contextFile);
@@ -34359,7 +34563,7 @@ function installForTool(root, tool, context) {
34359
34563
  if (tool.configDir) {
34360
34564
  mkdirSync17(join28(root, tool.configDir), { recursive: true });
34361
34565
  }
34362
- const parentDir = dirname12(contextPath);
34566
+ const parentDir = dirname13(contextPath);
34363
34567
  mkdirSync17(parentDir, { recursive: true });
34364
34568
  const action = writeOrMerge(contextPath, tool.contextFile, context);
34365
34569
  const rel = relative11(root, contextPath);
@@ -34736,7 +34940,7 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
34736
34940
  }
34737
34941
  function generateClaudeCodeContext() {
34738
34942
  try {
34739
- const thisDir = dirname12(fileURLToPath7(import.meta.url));
34943
+ const thisDir = dirname13(fileURLToPath8(import.meta.url));
34740
34944
  const repoRoot = resolve17(thisDir, "..", "..", "..");
34741
34945
  const claudeMdPath = join28(repoRoot, "CLAUDE.md");
34742
34946
  if (existsSync26(claudeMdPath)) {
@@ -35584,8 +35788,8 @@ var init_aiClient = __esm({
35584
35788
  static retryDelay(headers, deadline) {
35585
35789
  const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
35586
35790
  const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
35587
- const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
35588
- return new Promise((resolve21) => setTimeout(resolve21, delay));
35791
+ const delay2 = Math.min(requested, Math.max(0, deadline - performance.now()));
35792
+ return new Promise((resolve21) => setTimeout(resolve21, delay2));
35589
35793
  }
35590
35794
  static async requestJson(config, headers, body) {
35591
35795
  const deadline = performance.now() + config.totalTimeout * 1e3;
@@ -38640,7 +38844,7 @@ __export(sqlite_exports, {
38640
38844
  });
38641
38845
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
38642
38846
  import { mkdirSync as mkdirSync18 } from "node:fs";
38643
- import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
38847
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
38644
38848
  function isIdentifier(str) {
38645
38849
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
38646
38850
  }
@@ -38677,7 +38881,7 @@ function resolveSqlitePath(dbPath) {
38677
38881
  }
38678
38882
  const cwd = resolve18(process.cwd());
38679
38883
  const path10 = join29(cwd, dbPath);
38680
- const parent = dirname13(path10);
38884
+ const parent = dirname14(path10);
38681
38885
  const absParent = resolve18(parent);
38682
38886
  if (absParent !== cwd && !absParent.startsWith(cwd + sep7)) {
38683
38887
  throw new Error(
@@ -41106,9 +41310,75 @@ __export(mongodb_exports, {
41106
41310
  function isMatchAllWhere(where) {
41107
41311
  return where != null && MATCH_ALL_WHERE.test(where);
41108
41312
  }
41313
+ function isSqlWs(ch) {
41314
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
41315
+ }
41316
+ function isWordChar(ch) {
41317
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_";
41318
+ }
41319
+ function onlyWs(sql, start2, end) {
41320
+ for (let i = start2; i < end; i++) if (!isSqlWs(sql[i])) return false;
41321
+ return true;
41322
+ }
41323
+ function keywordAt(upper, keyword, from) {
41324
+ let at = upper.indexOf(keyword, from);
41325
+ while (at >= 0) {
41326
+ const end = at + keyword.length;
41327
+ const okBefore = at === 0 || isSqlWs(upper[at - 1]);
41328
+ const okAfter = end >= upper.length || isSqlWs(upper[end]);
41329
+ if (okBefore && okAfter) return at;
41330
+ at = upper.indexOf(keyword, at + 1);
41331
+ }
41332
+ return -1;
41333
+ }
41334
+ function earliest(positions, fallback) {
41335
+ let min = fallback;
41336
+ for (const p of positions) if (p >= 0 && p < min) min = p;
41337
+ return min;
41338
+ }
41339
+ function readIdentifier(sql, from) {
41340
+ let i = from;
41341
+ while (i < sql.length && isSqlWs(sql[i])) i++;
41342
+ if (i < sql.length && QUOTE_CHARS.includes(sql[i])) i++;
41343
+ const start2 = i;
41344
+ while (i < sql.length && isWordChar(sql[i])) i++;
41345
+ if (i === start2) return null;
41346
+ const name = sql.slice(start2, i);
41347
+ if (i < sql.length && QUOTE_CHARS.includes(sql[i])) i++;
41348
+ return [name, i];
41349
+ }
41350
+ function keywordGapEnd(upper, sql, keyword, from) {
41351
+ const at = keywordAt(upper, keyword, from);
41352
+ if (at < 0 || !onlyWs(sql, from, at)) return -1;
41353
+ return at + keyword.length;
41354
+ }
41355
+ function stripQuotes(value) {
41356
+ let result = value;
41357
+ if (result.length > 0 && (result[0] === '"' || result[0] === "'")) result = result.slice(1);
41358
+ const last = result.length - 1;
41359
+ if (last >= 0 && (result[last] === '"' || result[last] === "'")) result = result.slice(0, last);
41360
+ return result;
41361
+ }
41362
+ function splitOnAnd(where) {
41363
+ const parts = [];
41364
+ const lower = where.toLowerCase();
41365
+ let segStart = 0;
41366
+ let i = 0;
41367
+ while (i < where.length) {
41368
+ if (lower.startsWith("and", i) && i > 0 && isSqlWs(where[i - 1]) && i + 3 < where.length && isSqlWs(where[i + 3])) {
41369
+ parts.push(where.slice(segStart, i));
41370
+ i += 3;
41371
+ segStart = i;
41372
+ continue;
41373
+ }
41374
+ i++;
41375
+ }
41376
+ parts.push(where.slice(segStart));
41377
+ return parts;
41378
+ }
41109
41379
  function parseWhereClause(where, params, paramOffset = 0) {
41110
41380
  const filter = {};
41111
- const parts = where.split(/\s+AND\s+/i);
41381
+ const parts = splitOnAnd(where);
41112
41382
  let paramIndex = paramOffset;
41113
41383
  for (const part of parts) {
41114
41384
  const trimmed = part.trim();
@@ -41181,27 +41451,57 @@ function requireWriteFilter(filter, operation, table2) {
41181
41451
  );
41182
41452
  }
41183
41453
  }
41184
- function parseSelect(sql, params) {
41185
- const match = sql.match(/^SELECT\s+(.*?)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.*?))?(?:\s+ORDER\s+BY\s+(.*?))?(?:\s+LIMIT\s+(\d+))?(?:\s+OFFSET\s+(\d+))?$/is);
41186
- if (!match) return null;
41187
- const [, cols, collection, whereClause, orderBy, limitStr, skipStr] = match;
41454
+ function buildProjection(cols) {
41188
41455
  const projection = {};
41189
- if (cols && cols.trim() !== "*") {
41190
- for (const col of cols.split(",")) {
41191
- const name = col.trim().replace(/^["']|["']$/g, "");
41192
- if (name && name !== "*") projection[name] = 1;
41193
- }
41194
- }
41195
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41456
+ if (!cols || cols.trim() === "*") return projection;
41457
+ for (const col of cols.split(",")) {
41458
+ const name = stripQuotes(col.trim());
41459
+ if (name && name !== "*") projection[name] = 1;
41460
+ }
41461
+ return projection;
41462
+ }
41463
+ function stripTrailingDirection(part) {
41464
+ const lower = part.toLowerCase();
41465
+ let suffix = 0;
41466
+ if (lower.endsWith("asc")) suffix = 3;
41467
+ else if (lower.endsWith("desc")) suffix = 4;
41468
+ else return part;
41469
+ const beforeIdx = part.length - suffix - 1;
41470
+ if (beforeIdx < 0 || !isSqlWs(part[beforeIdx])) return part;
41471
+ let i = beforeIdx;
41472
+ while (i >= 0 && isSqlWs(part[i])) i--;
41473
+ return part.slice(0, i + 1);
41474
+ }
41475
+ function parseOrderBy(orderBy) {
41196
41476
  const sort = {};
41197
- if (orderBy) {
41198
- for (const part of orderBy.split(",")) {
41199
- const trimmed = part.trim();
41200
- const desc = /DESC$/i.test(trimmed);
41201
- const col = trimmed.replace(/\s+(ASC|DESC)$/i, "").replace(/^["']|["']$/g, "").trim();
41202
- sort[col] = desc ? -1 : 1;
41203
- }
41477
+ for (const rawPart of orderBy.split(",")) {
41478
+ const trimmed = rawPart.trim();
41479
+ const desc = trimmed.toLowerCase().endsWith("desc");
41480
+ const col = stripQuotes(stripTrailingDirection(trimmed)).trim();
41481
+ sort[col] = desc ? -1 : 1;
41204
41482
  }
41483
+ return sort;
41484
+ }
41485
+ function parseSelect(sql, params) {
41486
+ const upper = sql.toUpperCase();
41487
+ if (!(upper.startsWith("SELECT") && (sql.length === 6 || isSqlWs(sql[6])))) return null;
41488
+ const fromAt = keywordAt(upper, "FROM", 6);
41489
+ if (fromAt < 0) return null;
41490
+ const cols = sql.slice(6, fromAt).trim();
41491
+ const ident = readIdentifier(sql, fromAt + 4);
41492
+ if (!ident) return null;
41493
+ const [collection, afterColl] = ident;
41494
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41495
+ const orderAt = keywordAt(upper, "ORDER BY", afterColl);
41496
+ const limitAt = keywordAt(upper, "LIMIT", afterColl);
41497
+ const offsetAt = keywordAt(upper, "OFFSET", afterColl);
41498
+ const end = sql.length;
41499
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5, earliest([orderAt, limitAt, offsetAt], end)).trim();
41500
+ const orderBy = orderAt < 0 ? void 0 : sql.slice(orderAt + 8, earliest([limitAt, offsetAt], end)).trim();
41501
+ const limitStr = limitAt < 0 ? void 0 : sql.slice(limitAt + 5, earliest([offsetAt], end)).trim();
41502
+ const skipStr = offsetAt < 0 ? void 0 : sql.slice(offsetAt + 6).trim();
41503
+ const projection = buildProjection(cols);
41504
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41205
41505
  return {
41206
41506
  type: "find",
41207
41507
  collection,
@@ -41209,7 +41509,7 @@ function parseSelect(sql, params) {
41209
41509
  projection: Object.keys(projection).length > 0 ? projection : void 0,
41210
41510
  limit: limitStr ? parseInt(limitStr, 10) : void 0,
41211
41511
  skip: skipStr ? parseInt(skipStr, 10) : void 0,
41212
- sort: orderBy ? sort : void 0
41512
+ sort: orderBy ? parseOrderBy(orderBy) : void 0
41213
41513
  };
41214
41514
  }
41215
41515
  function parseInsert(sql, params) {
@@ -41245,20 +41545,33 @@ function parseSetClause(setClause, params) {
41245
41545
  return { document, consumed };
41246
41546
  }
41247
41547
  function parseUpdate(sql, params) {
41248
- const match = sql.match(/^UPDATE\s+["']?(\w+)["']?\s+SET\s+(.*?)(?:\s+WHERE\s+(.+))?$/is);
41249
- if (!match) return null;
41250
- const [, collection, setClause, whereClause] = match;
41548
+ const upper = sql.toUpperCase();
41549
+ if (!(upper.startsWith("UPDATE") && sql.length > 6 && isSqlWs(sql[6]))) return null;
41550
+ const ident = readIdentifier(sql, 6);
41551
+ if (!ident) return null;
41552
+ const [collection, afterColl] = ident;
41553
+ const setAt = keywordAt(upper, "SET", afterColl);
41554
+ if (setAt < 0 || !onlyWs(sql, afterColl, setAt)) return null;
41555
+ const whereAt = keywordAt(upper, "WHERE", setAt + 3);
41556
+ const setClause = sql.slice(setAt + 3, whereAt < 0 ? sql.length : whereAt).trim();
41557
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41251
41558
  const set = parseSetClause(setClause, params);
41252
- const matchAll = isMatchAllWhere(whereClause?.trim());
41253
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params, set.consumed).filter : {};
41559
+ const matchAll = isMatchAllWhere(whereClause);
41560
+ const filter = whereClause ? parseWhereClause(whereClause, params, set.consumed).filter : {};
41254
41561
  return { type: "updateMany", collection, filter, update: { $set: set.document }, matchAll };
41255
41562
  }
41256
41563
  function parseDelete(sql, params) {
41257
- const match = sql.match(/^DELETE\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
41258
- if (!match) return null;
41259
- const [, collection, whereClause] = match;
41260
- const matchAll = isMatchAllWhere(whereClause?.trim());
41261
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41564
+ const upper = sql.toUpperCase();
41565
+ if (!upper.startsWith("DELETE")) return null;
41566
+ const fromAt = keywordAt(upper, "FROM", 6);
41567
+ if (fromAt < 0 || !onlyWs(sql, 6, fromAt)) return null;
41568
+ const ident = readIdentifier(sql, fromAt + 4);
41569
+ if (!ident) return null;
41570
+ const [collection, afterColl] = ident;
41571
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41572
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41573
+ const matchAll = isMatchAllWhere(whereClause);
41574
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41262
41575
  return { type: "deleteMany", collection, filter, matchAll };
41263
41576
  }
41264
41577
  function parseCreate(sql) {
@@ -41266,10 +41579,23 @@ function parseCreate(sql) {
41266
41579
  return match ? { type: "raw", collection: match[1] } : null;
41267
41580
  }
41268
41581
  function parseCount(sql, params) {
41269
- const match = sql.match(/^SELECT\s+COUNT\(\*\)\s+AS\s+(\w+)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
41270
- if (!match) return null;
41271
- const [, alias, collection, whereClause] = match;
41272
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41582
+ const upper = sql.toUpperCase();
41583
+ if (!upper.startsWith("SELECT")) return null;
41584
+ const afterCount = keywordGapEnd(upper, sql, "COUNT(*)", 6);
41585
+ if (afterCount < 0) return null;
41586
+ const afterAs = keywordGapEnd(upper, sql, "AS", afterCount);
41587
+ if (afterAs < 0) return null;
41588
+ const aliasIdent = readIdentifier(sql, afterAs);
41589
+ if (!aliasIdent) return null;
41590
+ const [alias, afterAlias] = aliasIdent;
41591
+ const afterFrom = keywordGapEnd(upper, sql, "FROM", afterAlias);
41592
+ if (afterFrom < 0) return null;
41593
+ const coll = readIdentifier(sql, afterFrom);
41594
+ if (!coll) return null;
41595
+ const [collection, afterColl] = coll;
41596
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41597
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41598
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41273
41599
  return { type: "aggregate", collection, pipeline: [{ $match: filter }, { $count: alias }] };
41274
41600
  }
41275
41601
  function parseSql(sql, params = []) {
@@ -41295,12 +41621,13 @@ function fieldTypeToJsonSchema(def) {
41295
41621
  return { bsonType: "string" };
41296
41622
  }
41297
41623
  }
41298
- var MATCH_ALL_WHERE, MongodbAdapter;
41624
+ var MATCH_ALL_WHERE, QUOTE_CHARS, MongodbAdapter;
41299
41625
  var init_mongodb = __esm({
41300
41626
  "src/adapters/mongodb.ts"() {
41301
41627
  "use strict";
41302
41628
  init_connectTimeout();
41303
41629
  MATCH_ALL_WHERE = /^\s*1\s*=\s*1\s*$/;
41630
+ QUOTE_CHARS = `"'`;
41304
41631
  MongodbAdapter = class {
41305
41632
  constructor(config) {
41306
41633
  this.config = config;
@@ -43624,13 +43951,13 @@ var init_database = __esm({
43624
43951
  });
43625
43952
 
43626
43953
  // src/model.ts
43627
- import { readdirSync as readdirSync17, statSync as statSync18 } from "node:fs";
43954
+ import { readdirSync as readdirSync18, statSync as statSync18 } from "node:fs";
43628
43955
  import { join as join30, extname as extname8 } from "node:path";
43629
43956
  async function discoverModels(modelsDir) {
43630
43957
  const models = [];
43631
43958
  let files;
43632
43959
  try {
43633
- files = readdirSync17(modelsDir);
43960
+ files = readdirSync18(modelsDir);
43634
43961
  } catch {
43635
43962
  return models;
43636
43963
  }
@@ -43677,8 +44004,10 @@ var init_model = __esm({
43677
44004
  });
43678
44005
 
43679
44006
  // src/migration.ts
43680
- import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
44007
+ import { existsSync as existsSync27, readdirSync as readdirSync19, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17, rmSync } from "node:fs";
43681
44008
  import { join as join31, resolve as resolve19 } from "node:path";
44009
+ import { tmpdir } from "node:os";
44010
+ import { createHash as createHash10 } from "node:crypto";
43682
44011
  function unwrapAdapter(db) {
43683
44012
  let cur = db;
43684
44013
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -44157,17 +44486,113 @@ function warnUnprefixedMigrations(files) {
44157
44486
  );
44158
44487
  }
44159
44488
  }
44489
+ async function acquireMigrationLock(db, dir) {
44490
+ let engine = "";
44491
+ try {
44492
+ engine = (db.getDatabaseType() || "").toLowerCase();
44493
+ } catch {
44494
+ engine = "";
44495
+ }
44496
+ try {
44497
+ if (engine.startsWith("postgres")) {
44498
+ await adapterQuery(db, `SELECT pg_advisory_lock(${PG_ADVISORY_KEY}) AS locked`);
44499
+ return { kind: "postgres" };
44500
+ }
44501
+ if (engine.startsWith("mysql")) {
44502
+ await adapterQuery(db, `SELECT GET_LOCK('${MIGRATION_LOCK_NAME}', -1) AS locked`);
44503
+ return { kind: "mysql" };
44504
+ }
44505
+ if (engine === "mssql" || engine === "sqlserver") {
44506
+ await adapterExecute(
44507
+ db,
44508
+ `DECLARE @res INT; EXEC @res = sp_getapplock @Resource = '${MIGRATION_LOCK_NAME}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = -1`
44509
+ );
44510
+ return { kind: "mssql" };
44511
+ }
44512
+ } catch (err) {
44513
+ Log.debug(`DB migration lock unavailable (${engine}): ${err instanceof Error ? err.message : err}; using a file lock`);
44514
+ }
44515
+ return acquireFileLock(dir);
44516
+ }
44517
+ async function acquireFileLock(dir) {
44518
+ const lockFile = join31(tmpdir(), `tina4-migration-${createHash10("sha256").update(resolve19(dir)).digest("hex")}.lock`);
44519
+ const deadline = Date.now() + 6e4;
44520
+ for (; ; ) {
44521
+ try {
44522
+ writeFileSync17(lockFile, String(process.pid), { flag: "wx" });
44523
+ return { kind: "file", resource: lockFile };
44524
+ } catch (err) {
44525
+ if (err.code !== "EEXIST") {
44526
+ Log.debug(`file migration lock unavailable: ${err instanceof Error ? err.message : err}; running unlocked`);
44527
+ return { kind: "none" };
44528
+ }
44529
+ if (isStaleLock(lockFile)) {
44530
+ try {
44531
+ rmSync(lockFile, { force: true });
44532
+ } catch {
44533
+ }
44534
+ continue;
44535
+ }
44536
+ if (Date.now() > deadline) {
44537
+ Log.debug("file migration lock wait timed out; running unlocked");
44538
+ return { kind: "none" };
44539
+ }
44540
+ await delay(50);
44541
+ }
44542
+ }
44543
+ }
44544
+ function isStaleLock(lockFile) {
44545
+ let raw;
44546
+ try {
44547
+ raw = readFileSync25(lockFile, "utf-8").trim();
44548
+ } catch {
44549
+ return false;
44550
+ }
44551
+ const pid = parseInt(raw, 10);
44552
+ if (!raw || !Number.isFinite(pid) || pid <= 0) {
44553
+ return false;
44554
+ }
44555
+ try {
44556
+ process.kill(pid, 0);
44557
+ return false;
44558
+ } catch (err) {
44559
+ return err.code === "ESRCH";
44560
+ }
44561
+ }
44562
+ async function releaseMigrationLock(db, lock) {
44563
+ try {
44564
+ if (lock.kind === "postgres") {
44565
+ await adapterQuery(db, `SELECT pg_advisory_unlock(${PG_ADVISORY_KEY}) AS released`);
44566
+ } else if (lock.kind === "mysql") {
44567
+ await adapterQuery(db, `SELECT RELEASE_LOCK('${MIGRATION_LOCK_NAME}') AS released`);
44568
+ } else if (lock.kind === "mssql") {
44569
+ await adapterExecute(db, `EXEC sp_releaseapplock @Resource = '${MIGRATION_LOCK_NAME}', @LockOwner = 'Session'`);
44570
+ } else if (lock.kind === "file" && lock.resource) {
44571
+ rmSync(lock.resource, { recursive: true, force: true });
44572
+ }
44573
+ } catch (err) {
44574
+ Log.debug(`migration lock release failed (${lock.kind}): ${err instanceof Error ? err.message : err}`);
44575
+ }
44576
+ }
44160
44577
  async function migrate(adapter, options) {
44161
44578
  const db = adapter ?? getAdapter();
44162
44579
  const dir = resolve19(options?.migrationsDir ?? "migrations");
44163
44580
  const delimiter2 = options?.delimiter ?? ";";
44164
- const result = { applied: [], skipped: [], failed: [] };
44165
44581
  if (!existsSync27(dir)) {
44166
- return result;
44582
+ return { applied: [], skipped: [], failed: [] };
44583
+ }
44584
+ const lock = await acquireMigrationLock(db, dir);
44585
+ try {
44586
+ return await runPendingMigrations(db, dir, delimiter2);
44587
+ } finally {
44588
+ await releaseMigrationLock(db, lock);
44167
44589
  }
44590
+ }
44591
+ async function runPendingMigrations(db, dir, delimiter2) {
44592
+ const result = { applied: [], skipped: [], failed: [] };
44168
44593
  await ensureMigrationTableOn(db);
44169
44594
  const files = sortMigrationFiles(
44170
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44595
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44171
44596
  );
44172
44597
  if (files.length === 0) return result;
44173
44598
  warnUnprefixedMigrations(files);
@@ -44240,14 +44665,14 @@ async function status(adapter, options) {
44240
44665
  }
44241
44666
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
44242
44667
  const files2 = sortMigrationFiles(
44243
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44668
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44244
44669
  );
44245
44670
  result.pending = files2;
44246
44671
  return result;
44247
44672
  }
44248
44673
  await ensureMigrationTableOn(db);
44249
44674
  const files = sortMigrationFiles(
44250
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44675
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44251
44676
  );
44252
44677
  const appliedNames = /* @__PURE__ */ new Set();
44253
44678
  try {
@@ -44347,7 +44772,7 @@ export class ${className} {
44347
44772
  writeFileSync17(filePath, content, "utf-8");
44348
44773
  return filePath;
44349
44774
  }
44350
- var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
44775
+ var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, MIGRATION_LOCK_NAME, PG_ADVISORY_KEY, delay, Migration;
44351
44776
  var init_migration = __esm({
44352
44777
  "src/migration.ts"() {
44353
44778
  "use strict";
@@ -44372,6 +44797,12 @@ var init_migration = __esm({
44372
44797
  };
44373
44798
  SMART_QUOTE_RE = new RegExp(`[${Object.keys(SMART_QUOTES).join("")}]`, "g");
44374
44799
  SET_TERM_RE = /^SET\s+TERM\s+(\S+)$/i;
44800
+ MIGRATION_LOCK_NAME = "tina4_migration_lock";
44801
+ PG_ADVISORY_KEY = BigInt.asIntN(
44802
+ 64,
44803
+ BigInt("0x" + createHash10("sha256").update(MIGRATION_LOCK_NAME).digest("hex").slice(0, 16))
44804
+ ).toString();
44805
+ delay = (ms) => new Promise((r) => setTimeout(r, ms));
44375
44806
  Migration = class {
44376
44807
  db;
44377
44808
  dir;
@@ -44428,7 +44859,7 @@ var init_migration = __esm({
44428
44859
  const dir = resolve19(this.dir);
44429
44860
  if (!existsSync27(dir)) return [];
44430
44861
  return sortMigrationFiles(
44431
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44862
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44432
44863
  );
44433
44864
  }
44434
44865
  };
@@ -44436,17 +44867,40 @@ var init_migration = __esm({
44436
44867
  });
44437
44868
 
44438
44869
  // src/query.ts
44439
- function resolveFieldColumn(fields, columnOf, key) {
44440
- const field = resolveField(fields, columnOf, key);
44441
- return field === null ? null : columnOf(field);
44870
+ function resolveFieldColumn(fields, columnOf2, key) {
44871
+ const field = resolveField(fields, columnOf2, key);
44872
+ return field === null ? null : columnOf2(field);
44442
44873
  }
44443
- function resolveField(fields, columnOf, key) {
44874
+ function resolveField(fields, columnOf2, key) {
44444
44875
  if (Object.hasOwn(fields, key)) return key;
44445
44876
  for (const field of Object.keys(fields)) {
44446
- if (columnOf(field) === key) return field;
44877
+ if (columnOf2(field) === key) return field;
44447
44878
  }
44448
44879
  return null;
44449
44880
  }
44881
+ function applySearch(options, conditions, params, quote) {
44882
+ const searchTerm = (options.search ?? "").trim();
44883
+ if (searchTerm === "" || !options.searchColumns || options.searchColumns.length === 0) return;
44884
+ const likeParts = options.searchColumns.map((col) => `${quote(col)} LIKE ?`);
44885
+ conditions.push(`(${likeParts.join(" OR ")})`);
44886
+ for (const _ of options.searchColumns) params.push(`%${searchTerm}%`);
44887
+ }
44888
+ function orderPart(rawPart, bareDir, column2) {
44889
+ const trimmed = rawPart.trim();
44890
+ if (trimmed === "") return null;
44891
+ const descending = trimmed.startsWith("-");
44892
+ const col = column2("sort", descending ? trimmed.slice(1) : trimmed);
44893
+ return `${col} ${bareDir ?? (descending ? "DESC" : "ASC")}`;
44894
+ }
44895
+ function buildOrderClause(options, column2) {
44896
+ if (!options.sort) return "";
44897
+ const rawParts = options.sort.split(",");
44898
+ const first = rawParts[0].trim();
44899
+ const singleBare = rawParts.length === 1 && first !== "" && !first.startsWith("-");
44900
+ const bareDir = singleBare && String(options.sortDir).toLowerCase() === "desc" ? "DESC" : null;
44901
+ const parts = rawParts.map((part) => orderPart(part, bareDir, column2)).filter((term) => term !== null);
44902
+ return parts.length > 0 ? `ORDER BY ${parts.join(", ")}` : "";
44903
+ }
44450
44904
  function buildQuery(tableName, options, extraConditions, quote = quoteIdentifierAnsi, resolveColumn = () => null) {
44451
44905
  const conditions = [];
44452
44906
  const params = [];
@@ -44474,19 +44928,9 @@ function buildQuery(tableName, options, extraConditions, quote = quoteIdentifier
44474
44928
  }
44475
44929
  }
44476
44930
  }
44931
+ applySearch(options, conditions, params, quote);
44477
44932
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
44478
- let orderClause = "";
44479
- if (options.sort) {
44480
- const parts = [];
44481
- for (const part of options.sort.split(",")) {
44482
- const trimmed = part.trim();
44483
- if (trimmed === "") continue;
44484
- const descending = trimmed.startsWith("-");
44485
- const col = column2("sort", descending ? trimmed.slice(1) : trimmed);
44486
- parts.push(`${col} ${descending ? "DESC" : "ASC"}`);
44487
- }
44488
- if (parts.length > 0) orderClause = `ORDER BY ${parts.join(", ")}`;
44489
- }
44933
+ const orderClause = buildOrderClause(options, column2);
44490
44934
  const limit = Math.min(options.limit ?? DEFAULT_ROW_CAP, DEFAULT_ROW_CAP);
44491
44935
  const page = Math.max(options.page ?? 1, 1);
44492
44936
  const offset = (page - 1) * limit;
@@ -44504,32 +44948,41 @@ function buildQuery(tableName, options, extraConditions, quote = quoteIdentifier
44504
44948
  filterParams: params
44505
44949
  };
44506
44950
  }
44507
- function parseQueryString(query) {
44508
- const options = {};
44951
+ function parseFilterParams(query) {
44509
44952
  const filter = /* @__PURE__ */ Object.create(null);
44510
44953
  for (const [key, value] of Object.entries(query)) {
44511
44954
  if (key.startsWith("sort[")) {
44512
44955
  throw new InvalidQueryParameterError("Query parameter 'sort' must be a single comma-separated string");
44513
44956
  }
44514
44957
  if (!key.startsWith("filter[")) continue;
44515
- const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
44516
- if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
44517
- const [, field, rest] = filterMatch;
44518
- if (rest === "") {
44519
- filter[field] = value;
44520
- continue;
44521
- }
44522
- const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
44523
- if (operator === void 0 || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
44524
- if (!filter[field] || typeof filter[field] !== "object") {
44525
- filter[field] = {};
44526
- }
44527
- filter[field][operator] = value;
44958
+ addFilterKey(filter, key, value);
44959
+ }
44960
+ return filter;
44961
+ }
44962
+ function addFilterKey(filter, key, value) {
44963
+ const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
44964
+ if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
44965
+ const [, field, rest] = filterMatch;
44966
+ if (rest === "") {
44967
+ filter[field] = value;
44968
+ return;
44528
44969
  }
44970
+ const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
44971
+ if (operator === void 0 || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
44972
+ if (!filter[field] || typeof filter[field] !== "object") {
44973
+ filter[field] = {};
44974
+ }
44975
+ filter[field][operator] = value;
44976
+ }
44977
+ function parseQueryString(query) {
44978
+ const options = {};
44979
+ const filter = parseFilterParams(query);
44529
44980
  if (Object.keys(filter).length > 0) {
44530
44981
  options.filter = filter;
44531
44982
  }
44532
44983
  if (query.sort) options.sort = query.sort;
44984
+ if (query.search !== void 0) options.search = query.search;
44985
+ if (query.sort_dir !== void 0) options.sortDir = query.sort_dir;
44533
44986
  if (query.page) options.page = parseInt(query.page, 10);
44534
44987
  if (query.limit) options.limit = parseInt(query.limit, 10);
44535
44988
  if (query.offset !== void 0) {
@@ -44665,12 +45118,12 @@ var init_validation = __esm({
44665
45118
  });
44666
45119
 
44667
45120
  // src/autoCrud.ts
44668
- function allowListedBody(fields, columnOf, pkField, body, isCreate) {
45121
+ function allowListedBody(fields, columnOf2, pkField, body, isCreate) {
44669
45122
  const pkDef = fields[pkField];
44670
45123
  const stripPk = isCreate ? pkDef?.autoIncrement === true : true;
44671
45124
  const allowed = {};
44672
45125
  for (const [key, value] of Object.entries(body ?? {})) {
44673
- const field = resolveField(fields, columnOf, key);
45126
+ const field = resolveField(fields, columnOf2, key);
44674
45127
  if (field === null) continue;
44675
45128
  if (field === "is_deleted") continue;
44676
45129
  if (stripPk && field === pkField) continue;
@@ -44714,6 +45167,9 @@ function generateCrudRoutes(models, options = {}) {
44714
45167
  let built;
44715
45168
  try {
44716
45169
  const qp = parseQueryString(req2.query ?? {});
45170
+ if ((qp.search ?? "").trim() !== "") {
45171
+ qp.searchColumns = Object.entries(fields).filter(([, def]) => def.type === "string" || def.type === "text").map(([name]) => getDbCol(name));
45172
+ }
44717
45173
  built = buildQuery(tableName, qp, extraConditions, q, (key) => resolveFieldColumn(fields, getDbCol, key));
44718
45174
  } catch (err) {
44719
45175
  if (err instanceof UnknownFieldError) {
@@ -44961,6 +45417,567 @@ var init_autoCrud = __esm({
44961
45417
  }
44962
45418
  });
44963
45419
 
45420
+ // src/crudHelpers.ts
45421
+ function columnOf(model, prop) {
45422
+ return model.fieldMapping?.[prop] ?? prop;
45423
+ }
45424
+ function primaryKeyProp(model) {
45425
+ for (const [name, def] of Object.entries(model.fields)) {
45426
+ if (def.primaryKey) return name;
45427
+ }
45428
+ return "id";
45429
+ }
45430
+ function searchableProps(model) {
45431
+ return Object.entries(model.fields).filter(([, def]) => def.type === "string" || def.type === "text").map(([name]) => name);
45432
+ }
45433
+ function columnAlignment(model, col) {
45434
+ const type2 = model?.fields?.[col]?.type;
45435
+ return type2 === "integer" || type2 === "number" || type2 === "float" || type2 === "decimal" ? "text-end" : "text-start";
45436
+ }
45437
+ function prettyLabel(col) {
45438
+ return String(col).split("_").map((p) => p ? p.charAt(0).toUpperCase() + p.slice(1) : p).join(" ");
45439
+ }
45440
+ function crudSortProp(model, requested, pkProp) {
45441
+ const req2 = (requested ?? "").trim();
45442
+ if (req2 === "") return pkProp;
45443
+ if (Object.hasOwn(model.fields, req2)) return req2;
45444
+ for (const name of Object.keys(model.fields)) {
45445
+ if (columnOf(model, name) === req2) return name;
45446
+ }
45447
+ return pkProp;
45448
+ }
45449
+ function cellValue(record, col) {
45450
+ return record[col];
45451
+ }
45452
+ function buildCells(columns, record, editable, aligns) {
45453
+ return columns.map((col, index) => {
45454
+ const raw = cellValue(record, col);
45455
+ const value = h(raw === null || raw === void 0 ? "" : raw);
45456
+ const css = aligns[index];
45457
+ return editable ? `<td class="${css}" contenteditable="true" data-field="${h(col)}">${value}</td>` : `<td class="${css}">${value}</td>`;
45458
+ }).join("");
45459
+ }
45460
+ function stripOrderAndLimit(sql) {
45461
+ const cut = (line, keyword) => {
45462
+ const hasNewline = line.endsWith("\n");
45463
+ const content = hasNewline ? line.slice(0, -1) : line;
45464
+ const at = content.toUpperCase().indexOf(keyword);
45465
+ if (at < 0) return line;
45466
+ const after = at + keyword.length;
45467
+ if (content.length <= after) return line;
45468
+ return content.slice(0, at) + (hasNewline ? "\n" : "");
45469
+ };
45470
+ const src = String(sql);
45471
+ const lines = [];
45472
+ let start2 = 0;
45473
+ for (let i = 0; i < src.length; i++) {
45474
+ if (src[i] === "\n") {
45475
+ lines.push(src.slice(start2, i + 1));
45476
+ start2 = i + 1;
45477
+ }
45478
+ }
45479
+ lines.push(src.slice(start2));
45480
+ return lines.map((line) => cut(cut(line, "ORDER BY "), "LIMIT ")).join("").trim();
45481
+ }
45482
+ function extractColumns(sql) {
45483
+ const colsStr = selectProjection(sql);
45484
+ if (colsStr === null || colsStr === "*") return ["*"];
45485
+ return colsStr.split(",").map((c) => columnAlias(c.trim()));
45486
+ }
45487
+ function selectProjection(sql) {
45488
+ const upper = sql.toUpperCase();
45489
+ const selectAt = keywordIndex(upper, "SELECT", 0);
45490
+ if (selectAt < 0) return null;
45491
+ const colsStart = selectAt + "SELECT".length;
45492
+ const fromAt = keywordIndex(upper, "FROM", colsStart);
45493
+ if (fromAt < 0) return null;
45494
+ return sql.slice(colsStart, fromAt).trim();
45495
+ }
45496
+ function columnAlias(trimmed) {
45497
+ const asAt = keywordIndex(trimmed.toUpperCase(), "AS", 0);
45498
+ if (asAt >= 0) {
45499
+ const after = trimmed.slice(asAt + "AS".length).trim();
45500
+ let endIdx = 0;
45501
+ while (endIdx < after.length && !SQL_WS.includes(after[endIdx]) && after[endIdx] !== ",") endIdx++;
45502
+ const word = after.slice(0, endIdx);
45503
+ if (word) return word;
45504
+ }
45505
+ if (trimmed.includes(".")) return trimmed.split(".").pop().trim();
45506
+ return trimmed;
45507
+ }
45508
+ function keywordIndex(upperHaystack, keyword, from) {
45509
+ let at = upperHaystack.indexOf(keyword, from);
45510
+ while (at >= 0) {
45511
+ const afterIdx = at + keyword.length;
45512
+ const boundaryBefore = at === 0 || SQL_WS.includes(upperHaystack[at - 1]);
45513
+ const boundaryAfter = afterIdx >= upperHaystack.length || SQL_WS.includes(upperHaystack[afterIdx]);
45514
+ if (boundaryBefore && boundaryAfter) return at;
45515
+ at = upperHaystack.indexOf(keyword, at + 1);
45516
+ }
45517
+ return -1;
45518
+ }
45519
+ var h, SQL_WS;
45520
+ var init_crudHelpers = __esm({
45521
+ "src/crudHelpers.ts"() {
45522
+ "use strict";
45523
+ init_engine();
45524
+ h = escapeHtml;
45525
+ SQL_WS = " \n\r\f\v";
45526
+ }
45527
+ });
45528
+
45529
+ // src/crudTable.ts
45530
+ function sortIndicator(sortCol, col, sortDir) {
45531
+ if (String(sortCol) !== String(col)) return "";
45532
+ const arrow = sortDir === "asc" ? "&#9650;" : "&#9660;";
45533
+ return ` <span class="sort-indicator">${arrow}</span>`;
45534
+ }
45535
+ function sortUrl(requestPath, col, nextDir, page, search, limit) {
45536
+ const query = `sort=${encodeURIComponent(col)}&sort_dir=${nextDir}&page=${page}&search=${encodeURIComponent(search)}&limit=${limit}`;
45537
+ return h(`${requestPath}?${query}`);
45538
+ }
45539
+ function pageUrl(requestPath, p, search, sortCol, sortDir, limit) {
45540
+ const query = `page=${p}&search=${encodeURIComponent(search)}&sort=${encodeURIComponent(sortCol)}&sort_dir=${sortDir}&limit=${limit}`;
45541
+ return h(`${requestPath}?${query}`);
45542
+ }
45543
+ function pageControls(page, totalPages, requestPath, search, sortCol, sortDir, limit) {
45544
+ if (totalPages <= 1) return [];
45545
+ const controls = [];
45546
+ if (page > 1) {
45547
+ controls.push({
45548
+ label: "Prev",
45549
+ page: page - 1,
45550
+ active: false,
45551
+ inactive: true,
45552
+ url: pageUrl(requestPath, page - 1, search, sortCol, sortDir, limit)
45553
+ });
45554
+ }
45555
+ let startPage = Math.max(page - 3, 1);
45556
+ const endPage = Math.min(startPage + 6, totalPages);
45557
+ startPage = Math.max(endPage - 6, 1);
45558
+ for (let p = startPage; p <= endPage; p++) {
45559
+ controls.push({
45560
+ label: p,
45561
+ page: p,
45562
+ active: p === page,
45563
+ inactive: p !== page,
45564
+ url: pageUrl(requestPath, p, search, sortCol, sortDir, limit)
45565
+ });
45566
+ }
45567
+ if (page < totalPages) {
45568
+ controls.push({
45569
+ label: "Next",
45570
+ page: page + 1,
45571
+ active: false,
45572
+ inactive: true,
45573
+ url: pageUrl(requestPath, page + 1, search, sortCol, sortDir, limit)
45574
+ });
45575
+ }
45576
+ return controls;
45577
+ }
45578
+ function jsConfig(opts) {
45579
+ const aligns = {};
45580
+ const labels = {};
45581
+ for (const col of opts.columns) {
45582
+ aligns[col] = columnAlignment(opts.model, col);
45583
+ labels[col] = prettyLabel(col);
45584
+ }
45585
+ const config = {
45586
+ api: opts.apiPath,
45587
+ pk: opts.pk,
45588
+ columns: opts.columns,
45589
+ editable: opts.editable,
45590
+ aligns,
45591
+ labels,
45592
+ limit: opts.limit,
45593
+ search: opts.search,
45594
+ sort: opts.sortCol,
45595
+ sort_dir: opts.sortDir,
45596
+ page: opts.page
45597
+ };
45598
+ return JSON.stringify(config).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
45599
+ }
45600
+ function headerCell(opts, col, align) {
45601
+ const header = { label: h(prettyLabel(col)), align };
45602
+ if (!opts.sortable) {
45603
+ header.plain = true;
45604
+ return header;
45605
+ }
45606
+ const nextDir = String(opts.sortCol) === String(col) && opts.sortDir === "asc" ? "desc" : "asc";
45607
+ header.sortable = true;
45608
+ header.col = h(col);
45609
+ header.next_dir = nextDir;
45610
+ header.url = sortUrl(opts.requestPath, col, nextDir, opts.page, opts.search, opts.limit);
45611
+ header.indicator = sortIndicator(opts.sortCol, col, opts.sortDir);
45612
+ return header;
45613
+ }
45614
+ function tableData(opts) {
45615
+ const aligns = opts.columns.map((col) => columnAlignment(opts.model, col));
45616
+ const headers = opts.columns.map((col, index) => headerCell(opts, col, aligns[index]));
45617
+ const rows = opts.records.map((record) => ({
45618
+ id: h(cellValue(record, opts.pk) ?? ""),
45619
+ cells: buildCells(opts.columns, record, opts.editable, aligns)
45620
+ }));
45621
+ return {
45622
+ headers,
45623
+ rows,
45624
+ empty: opts.records.length === 0,
45625
+ colspan: opts.columns.length + 1,
45626
+ editable: opts.editable,
45627
+ readonly: !opts.editable,
45628
+ inline_script: opts.inlineScript,
45629
+ table_name: h(opts.tableName),
45630
+ table_id_attr: opts.tableId ? ` id="${h(opts.tableId)}"` : ""
45631
+ };
45632
+ }
45633
+ var init_crudTable = __esm({
45634
+ "src/crudTable.ts"() {
45635
+ "use strict";
45636
+ init_crudHelpers();
45637
+ }
45638
+ });
45639
+
45640
+ // src/crudRender.ts
45641
+ import { existsSync as existsSync28 } from "node:fs";
45642
+ import { resolve as pathResolve, join as pathJoin, dirname as dirname15 } from "node:path";
45643
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
45644
+ function frondFor(dir) {
45645
+ let engine = frondCache2.get(dir);
45646
+ if (!engine) {
45647
+ setCspNonceProvider(currentCspNonce);
45648
+ engine = new Frond(dir);
45649
+ frondCache2.set(dir, engine);
45650
+ }
45651
+ return engine;
45652
+ }
45653
+ function renderCrudTemplate(name, data) {
45654
+ const templateFile = `crud/${name}.twig`;
45655
+ const userDir = pathResolve(process.cwd(), "src", "templates");
45656
+ if (existsSync28(pathJoin(userDir, templateFile))) {
45657
+ return frondFor(userDir).render(templateFile, data);
45658
+ }
45659
+ return frondFor(FRAMEWORK_TEMPLATES_DIR).render(templateFile, data);
45660
+ }
45661
+ function renderModalForm(mode, columns, pk, edit) {
45662
+ const fields = columns.map((col) => {
45663
+ const label = prettyLabel(col);
45664
+ return {
45665
+ id: `${mode}-${col}`,
45666
+ name: h(col),
45667
+ label: h(label),
45668
+ value: "",
45669
+ placeholder: h(`Enter ${label.toLowerCase()}`),
45670
+ type: "text",
45671
+ required_attr: "",
45672
+ input: true
45673
+ };
45674
+ });
45675
+ return renderCrudTemplate("form", {
45676
+ wrap: true,
45677
+ form_id_attr: ` id="form-${mode}"`,
45678
+ action: "",
45679
+ form_method: "POST",
45680
+ method_override: null,
45681
+ edit,
45682
+ mode,
45683
+ pk: h(pk),
45684
+ modal_footer: true,
45685
+ submit_button: false,
45686
+ fields
45687
+ });
45688
+ }
45689
+ function renderModals(editableColumns, pk) {
45690
+ return renderCrudTemplate("modals", {
45691
+ create_form: renderModalForm("create", editableColumns, pk, false),
45692
+ edit_form: renderModalForm("edit", editableColumns, pk, true)
45693
+ });
45694
+ }
45695
+ function generateTable(records, options = {}) {
45696
+ const rows = records ?? [];
45697
+ const tableName = String(options.tableName ?? "data");
45698
+ const primaryKey = String(options.primaryKey ?? "id");
45699
+ const editable = options.editable ?? true;
45700
+ const columns = rows.length === 0 ? [] : Object.keys(rows[0]);
45701
+ return renderCrudTemplate(
45702
+ "table",
45703
+ tableData({
45704
+ columns,
45705
+ records: rows,
45706
+ pk: primaryKey,
45707
+ tableName,
45708
+ editable,
45709
+ sortable: false,
45710
+ inlineScript: editable,
45711
+ requestPath: "",
45712
+ search: "",
45713
+ sortCol: "",
45714
+ sortDir: "asc",
45715
+ page: 1,
45716
+ limit: 10,
45717
+ tableId: `crud-${tableName}`,
45718
+ model: null
45719
+ })
45720
+ );
45721
+ }
45722
+ function buildOptions(options, selectedValue) {
45723
+ return (options ?? []).map((opt) => {
45724
+ const selected = String(opt.value) === String(selectedValue) ? " selected" : "";
45725
+ return `<option value="${h(opt.value)}"${selected}>${h(opt.label)}</option>`;
45726
+ }).join("");
45727
+ }
45728
+ function fieldInputShape(field, base) {
45729
+ const value = field.value;
45730
+ switch (field.type ?? "string") {
45731
+ case "text":
45732
+ return { ...base, textarea: true };
45733
+ case "boolean":
45734
+ return { ...base, checkbox: true, checked_attr: value ? " checked" : "" };
45735
+ case "select":
45736
+ return { ...base, select: true, options_html: buildOptions(field.options, value) };
45737
+ case "date":
45738
+ return { ...base, input: true, type: "date" };
45739
+ case "integer":
45740
+ case "number":
45741
+ case "float":
45742
+ case "decimal":
45743
+ return { ...base, input: true, type: "number" };
45744
+ default:
45745
+ return { ...base, input: true, type: "text" };
45746
+ }
45747
+ }
45748
+ function buildCustomField(field) {
45749
+ const name = String(field.name ?? "");
45750
+ const label = field.label ?? name.charAt(0).toUpperCase() + name.slice(1);
45751
+ const base = {
45752
+ id: h(name),
45753
+ name: h(name),
45754
+ label: h(String(label)),
45755
+ value: h(field.value ?? ""),
45756
+ placeholder: "",
45757
+ required_attr: field.required ? " required" : ""
45758
+ };
45759
+ return fieldInputShape(field, base);
45760
+ }
45761
+ function generateForm2(fields, options = {}) {
45762
+ const verb = String(options.method ?? "POST").toUpperCase();
45763
+ return renderCrudTemplate("form", {
45764
+ wrap: true,
45765
+ form_id_attr: "",
45766
+ action: h(options.action ?? "/"),
45767
+ form_method: h(verb),
45768
+ method_override: ["PUT", "PATCH", "DELETE"].includes(verb) ? verb : null,
45769
+ edit: false,
45770
+ modal_footer: false,
45771
+ submit_button: true,
45772
+ fields: (fields ?? []).map((f) => buildCustomField(f))
45773
+ });
45774
+ }
45775
+ var FRAMEWORK_TEMPLATES_DIR, frondCache2;
45776
+ var init_crudRender = __esm({
45777
+ "src/crudRender.ts"() {
45778
+ "use strict";
45779
+ init_engine();
45780
+ init_csp();
45781
+ init_crudHelpers();
45782
+ init_crudTable();
45783
+ FRAMEWORK_TEMPLATES_DIR = pathResolve(
45784
+ dirname15(fileURLToPath9(import.meta.url)),
45785
+ "..",
45786
+ "..",
45787
+ "core",
45788
+ "templates"
45789
+ );
45790
+ frondCache2 = /* @__PURE__ */ new Map();
45791
+ }
45792
+ });
45793
+
45794
+ // src/crud.ts
45795
+ function _resetCrudRegistrations() {
45796
+ registeredTables.clear();
45797
+ }
45798
+ function discoveredFrom(model) {
45799
+ return {
45800
+ filePath: `src/models/${model.name ?? model.tableName}.ts`,
45801
+ modelClass: model,
45802
+ definition: {
45803
+ tableName: model.tableName,
45804
+ className: model.name,
45805
+ fields: model.fields,
45806
+ fieldMapping: model.fieldMapping,
45807
+ softDelete: model.softDelete ?? false,
45808
+ tableFilter: model.tableFilter,
45809
+ dbName: model._db
45810
+ }
45811
+ };
45812
+ }
45813
+ async function registerBackend(model, options = {}) {
45814
+ const prefix = options.prefix ?? "/api";
45815
+ const key = `${prefix}::${model.tableName}`;
45816
+ if (registeredTables.has(key)) return;
45817
+ const routes = generateCrudRoutes([discoveredFrom(model)], { public: options.public === true });
45818
+ for (const route of routes) defaultRouter.addRoute(route);
45819
+ registeredTables.add(key);
45820
+ }
45821
+ async function fetchModelData(model, opts) {
45822
+ const orderBy = `${opts.sort} ${opts.sortDir.toUpperCase()}`;
45823
+ const search = opts.search.trim();
45824
+ const searchable = search === "" ? [] : searchableProps(model);
45825
+ let rows;
45826
+ if (searchable.length === 0) {
45827
+ rows = await model.all(opts.limit, opts.offset, void 0, orderBy);
45828
+ } else {
45829
+ const whereClause = searchable.map((prop) => `${columnOf(model, prop)} LIKE ?`).join(" OR ");
45830
+ const params = searchable.map(() => `%${search}%`);
45831
+ rows = await model.where(whereClause, params, opts.limit, opts.offset, void 0, orderBy);
45832
+ }
45833
+ const total = typeof rows.getTotalRecords === "function" ? rows.getTotalRecords() : rows.length;
45834
+ const records = rows.map((r) => typeof r.toDict === "function" ? r.toDict() : r);
45835
+ return [records, total];
45836
+ }
45837
+ async function fetchSqlData(sql, opts) {
45838
+ let adapter;
45839
+ try {
45840
+ adapter = getAdapter();
45841
+ } catch {
45842
+ return [[], 0];
45843
+ }
45844
+ const base = stripOrderAndLimit(sql);
45845
+ const dir = opts.sortDir.toUpperCase();
45846
+ const search = opts.search.trim();
45847
+ if (search === "") {
45848
+ const countRow2 = await adapterQuery(adapter, `SELECT COUNT(*) as cnt FROM (${base}) AS _crud_cnt`, []);
45849
+ const total2 = Number(countRow2[0]?.cnt ?? 0);
45850
+ const rows2 = await adapterFetch(adapter, `${base} ORDER BY ${opts.sort} ${dir}`, [], opts.limit, opts.offset);
45851
+ return [rows2, total2];
45852
+ }
45853
+ const columns = extractColumns(sql);
45854
+ const searchParts = columns.map((col) => `CAST(${col} AS TEXT) LIKE ?`);
45855
+ const params = columns.map(() => `%${search}%`);
45856
+ const countRow = await adapterQuery(
45857
+ adapter,
45858
+ `SELECT COUNT(*) as cnt FROM (${base}) AS _crud_cnt WHERE ${searchParts.join(" OR ")}`,
45859
+ params
45860
+ );
45861
+ const total = Number(countRow[0]?.cnt ?? 0);
45862
+ const rows = await adapterFetch(
45863
+ adapter,
45864
+ `SELECT * FROM (${base}) AS _crud_sub WHERE ${searchParts.join(" OR ")} ORDER BY ${opts.sort} ${dir}`,
45865
+ params,
45866
+ opts.limit,
45867
+ opts.offset
45868
+ );
45869
+ return [rows, total];
45870
+ }
45871
+ function parseCrudRequest(request, model, pkProp, limit) {
45872
+ const query = request && request.query || {};
45873
+ const page = Math.max(parseInt(String(query.page ?? "1"), 10) || 1, 1);
45874
+ const search = String(query.search ?? "").trim();
45875
+ const sortProp = crudSortProp(model, query.sort, pkProp);
45876
+ const sortColumn = columnOf(model, sortProp);
45877
+ const sortDir = query.sort_dir === "desc" ? "desc" : "asc";
45878
+ const requestPath = request && typeof request.path === "string" ? request.path : "/";
45879
+ return { page, search, sortProp, sortColumn, sortDir, offset: (page - 1) * limit, requestPath };
45880
+ }
45881
+ function clampLimit(raw) {
45882
+ const limit = Number(raw ?? 10);
45883
+ return !Number.isFinite(limit) || limit <= 0 ? 10 : limit;
45884
+ }
45885
+ async function toCrud(request, options) {
45886
+ const model = options.model;
45887
+ if (!model || !model.tableName || !model.fields) {
45888
+ throw new Error("Crud.toCrud requires a model (an ORM class)");
45889
+ }
45890
+ const sql = options.sql;
45891
+ const title = String(options.title ?? "CRUD");
45892
+ const prefix = String(options.prefix ?? "/api");
45893
+ const limit = clampLimit(options.limit);
45894
+ const tableName = String(model.tableName);
45895
+ const pkProp = primaryKeyProp(model);
45896
+ const columns = Object.keys(model.fields);
45897
+ await registerBackend(model, { prefix, public: options.public });
45898
+ const req2 = parseCrudRequest(request, model, pkProp, limit);
45899
+ const fetchOpts = {
45900
+ search: req2.search,
45901
+ sort: req2.sortColumn,
45902
+ sortDir: req2.sortDir,
45903
+ limit,
45904
+ offset: req2.offset
45905
+ };
45906
+ const [records, total] = sql ? await fetchSqlData(sql, fetchOpts) : await fetchModelData(model, fetchOpts);
45907
+ const totalPages = total > 0 ? Math.ceil(total / limit) : 1;
45908
+ const apiPath = `${prefix}/${tableName}`;
45909
+ const editableColumns = columns.filter((c) => c !== pkProp);
45910
+ const tableHtml = renderCrudTemplate(
45911
+ "table",
45912
+ tableData({
45913
+ columns,
45914
+ records,
45915
+ pk: pkProp,
45916
+ tableName,
45917
+ editable: false,
45918
+ sortable: true,
45919
+ inlineScript: false,
45920
+ requestPath: req2.requestPath,
45921
+ search: req2.search,
45922
+ sortCol: req2.sortProp,
45923
+ sortDir: req2.sortDir,
45924
+ page: req2.page,
45925
+ limit,
45926
+ tableId: null,
45927
+ model
45928
+ })
45929
+ );
45930
+ return renderCrudTemplate("page", {
45931
+ title: h(title),
45932
+ search: h(req2.search),
45933
+ request_path: h(req2.requestPath),
45934
+ info_count: records.length,
45935
+ info_total: total,
45936
+ info_page: req2.page,
45937
+ info_total_pages: totalPages,
45938
+ table_html: tableHtml,
45939
+ modals_html: renderModals(editableColumns, pkProp),
45940
+ show_pagination: totalPages > 1,
45941
+ controls: pageControls(req2.page, totalPages, req2.requestPath, req2.search, req2.sortProp, req2.sortDir, limit),
45942
+ config_json: jsConfig({
45943
+ apiPath,
45944
+ pk: pkProp,
45945
+ columns,
45946
+ editable: editableColumns,
45947
+ model,
45948
+ limit,
45949
+ search: req2.search,
45950
+ sortCol: req2.sortProp,
45951
+ sortDir: req2.sortDir,
45952
+ page: req2.page
45953
+ })
45954
+ });
45955
+ }
45956
+ var registeredTables, Crud, CRUD;
45957
+ var init_crud = __esm({
45958
+ "src/crud.ts"() {
45959
+ "use strict";
45960
+ init_router();
45961
+ init_autoCrud();
45962
+ init_database();
45963
+ init_crudHelpers();
45964
+ init_crudTable();
45965
+ init_crudRender();
45966
+ init_crudRender();
45967
+ init_crudHelpers();
45968
+ registeredTables = /* @__PURE__ */ new Set();
45969
+ Crud = {
45970
+ toCrud,
45971
+ registerBackend,
45972
+ generateTable,
45973
+ generateForm: generateForm2,
45974
+ stripOrderAndLimit,
45975
+ _resetCrudRegistrations
45976
+ };
45977
+ CRUD = Crud;
45978
+ }
45979
+ });
45980
+
44964
45981
  // src/queryBuilder.ts
44965
45982
  var QueryBuilder;
44966
45983
  var init_queryBuilder = __esm({
@@ -47313,7 +48330,7 @@ var init_seeder = __esm({
47313
48330
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
47314
48331
  import { randomBytes as randomBytes11 } from "node:crypto";
47315
48332
  import { mkdirSync as mkdirSync20 } from "node:fs";
47316
- import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
48333
+ import { dirname as dirname16, isAbsolute as isAbsolute6, join as join32 } from "node:path";
47317
48334
  function iso(d) {
47318
48335
  return d.toISOString();
47319
48336
  }
@@ -47584,7 +48601,7 @@ function resolveStorePath(dbPath) {
47584
48601
  let path10 = dbPath;
47585
48602
  if (!isAbsolute6(path10)) {
47586
48603
  path10 = join32(process.cwd(), path10);
47587
- mkdirSync20(dirname14(path10), { recursive: true });
48604
+ mkdirSync20(dirname16(path10), { recursive: true });
47588
48605
  }
47589
48606
  return path10;
47590
48607
  }
@@ -49334,7 +50351,9 @@ __export(index_exports, {
49334
50351
  AutoCrud: () => AutoCrud,
49335
50352
  BaseModel: () => BaseModel,
49336
50353
  CONNECT_TIMEOUT_TOLERANCE_MS: () => CONNECT_TIMEOUT_TOLERANCE_MS,
50354
+ CRUD: () => CRUD,
49337
50355
  CachedDatabaseAdapter: () => CachedDatabaseAdapter,
50356
+ Crud: () => Crud,
49338
50357
  Cursor: () => Cursor,
49339
50358
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
49340
50359
  DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS,
@@ -49381,6 +50400,7 @@ __export(index_exports, {
49381
50400
  SqliteCollection: () => SqliteCollection,
49382
50401
  SqliteDatabase: () => SqliteDatabase,
49383
50402
  UnknownFieldError: () => UnknownFieldError,
50403
+ _resetCrudRegistrations: () => _resetCrudRegistrations,
49384
50404
  adapterColumns: () => adapterColumns,
49385
50405
  adapterCommit: () => adapterCommit,
49386
50406
  adapterCreateTable: () => adapterCreateTable,
@@ -49413,6 +50433,8 @@ __export(index_exports, {
49413
50433
  ensureMigrationTable: () => ensureMigrationTable,
49414
50434
  extractLastInsertId: () => extractLastInsertId,
49415
50435
  generateCrudRoutes: () => generateCrudRoutes,
50436
+ generateForm: () => generateForm2,
50437
+ generateTable: () => generateTable,
49416
50438
  getAdapter: () => getAdapter,
49417
50439
  getAppliedMigrations: () => getAppliedMigrations,
49418
50440
  getCollection: () => getCollection,
@@ -49432,10 +50454,13 @@ __export(index_exports, {
49432
50454
  realtime: () => realtime,
49433
50455
  recordMigration: () => recordMigration,
49434
50456
  redactCredentials: () => redactCredentials,
50457
+ registerBackend: () => registerBackend,
49435
50458
  removeMigrationRecord: () => removeMigrationRecord,
49436
50459
  resetDefaultStore: () => resetDefaultStore,
49437
50460
  resetRequestCaches: () => resetRequestCaches2,
49438
50461
  resolveDbPool: () => resolveDbPool,
50462
+ resolveField: () => resolveField,
50463
+ resolveFieldColumn: () => resolveFieldColumn,
49439
50464
  resolveFirebirdCharset: () => resolveFirebirdCharset,
49440
50465
  resolveGraphConnectTimeout: () => resolveGraphConnectTimeout,
49441
50466
  resolveSqlitePath: () => resolveSqlitePath,
@@ -49453,8 +50478,10 @@ __export(index_exports, {
49453
50478
  splitStatements: () => splitStatements,
49454
50479
  status: () => status,
49455
50480
  storageKey: () => storageKey,
50481
+ stripOrderAndLimit: () => stripOrderAndLimit,
49456
50482
  stripTrailingSemicolons: () => stripTrailingSemicolons,
49457
50483
  syncModels: () => syncModels,
50484
+ toCrud: () => toCrud,
49458
50485
  validate: () => validate,
49459
50486
  withConnectTimeout: () => withConnectTimeout,
49460
50487
  wrapWithCache: () => wrapWithCache
@@ -49470,6 +50497,7 @@ var init_index = __esm({
49470
50497
  init_model();
49471
50498
  init_migration();
49472
50499
  init_autoCrud();
50500
+ init_crud();
49473
50501
  init_query();
49474
50502
  init_validation();
49475
50503
  init_baseModel();
@@ -49501,7 +50529,9 @@ export {
49501
50529
  AutoCrud,
49502
50530
  BaseModel,
49503
50531
  CONNECT_TIMEOUT_TOLERANCE_MS,
50532
+ CRUD,
49504
50533
  CachedDatabaseAdapter,
50534
+ Crud,
49505
50535
  Cursor,
49506
50536
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
49507
50537
  DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS,
@@ -49548,6 +50578,7 @@ export {
49548
50578
  SqliteCollection,
49549
50579
  SqliteDatabase,
49550
50580
  UnknownFieldError,
50581
+ _resetCrudRegistrations,
49551
50582
  adapterColumns,
49552
50583
  adapterCommit,
49553
50584
  adapterCreateTable,
@@ -49580,6 +50611,8 @@ export {
49580
50611
  ensureMigrationTable,
49581
50612
  extractLastInsertId,
49582
50613
  generateCrudRoutes,
50614
+ generateForm2 as generateForm,
50615
+ generateTable,
49583
50616
  getAdapter,
49584
50617
  getAppliedMigrations,
49585
50618
  getCollection,
@@ -49599,10 +50632,13 @@ export {
49599
50632
  realtime,
49600
50633
  recordMigration,
49601
50634
  redactCredentials,
50635
+ registerBackend,
49602
50636
  removeMigrationRecord,
49603
50637
  resetDefaultStore,
49604
50638
  resetRequestCaches2 as resetRequestCaches,
49605
50639
  resolveDbPool,
50640
+ resolveField,
50641
+ resolveFieldColumn,
49606
50642
  resolveFirebirdCharset,
49607
50643
  resolveGraphConnectTimeout,
49608
50644
  resolveSqlitePath,
@@ -49620,8 +50656,10 @@ export {
49620
50656
  splitStatements,
49621
50657
  status,
49622
50658
  storageKey,
50659
+ stripOrderAndLimit,
49623
50660
  stripTrailingSemicolons,
49624
50661
  syncModels,
50662
+ toCrud,
49625
50663
  validate,
49626
50664
  withConnectTimeout,
49627
50665
  wrapWithCache