tina4-nodejs 3.13.95 → 3.13.96

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.
@@ -8,64 +8,6 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/types.ts
12
- var FetchResult;
13
- var init_types = __esm({
14
- "src/types.ts"() {
15
- "use strict";
16
- FetchResult = class {
17
- records;
18
- count;
19
- sql;
20
- constructor(records, sql = "") {
21
- this.records = records;
22
- this.count = records.length;
23
- this.sql = sql;
24
- }
25
- /** Paginate the in-memory result set. */
26
- toPaginate(page = 1, perPage = 20) {
27
- const total = this.count;
28
- const totalPages = Math.max(1, Math.ceil(total / perPage));
29
- const offset = (page - 1) * perPage;
30
- const data = this.records.slice(offset, offset + perPage);
31
- return {
32
- data,
33
- page,
34
- perPage,
35
- total,
36
- totalPages,
37
- hasNext: page < totalPages,
38
- hasPrev: page > 1
39
- };
40
- }
41
- /** Return the first record or null. */
42
- first() {
43
- return this.records[0] ?? null;
44
- }
45
- /** Return the last record or null. */
46
- last() {
47
- return this.records[this.records.length - 1] ?? null;
48
- }
49
- /** Check if result is empty. */
50
- isEmpty() {
51
- return this.records.length === 0;
52
- }
53
- /** Convert to plain array. */
54
- toArray() {
55
- return [...this.records];
56
- }
57
- /** Convert to JSON string. */
58
- toJSON() {
59
- return JSON.stringify(this.records);
60
- }
61
- /** Iterate over records. */
62
- [Symbol.iterator]() {
63
- return this.records[Symbol.iterator]();
64
- }
65
- };
66
- }
67
- });
68
-
69
11
  // src/databaseResult.ts
70
12
  var DatabaseResult;
71
13
  var init_databaseResult = __esm({
@@ -129,75 +71,52 @@ var init_databaseResult = __esm({
129
71
  toArray() {
130
72
  return this.records;
131
73
  }
132
- /** Pagination envelope — accepts either (page, perPage) or (offset, limit) style.
74
+ /**
75
+ * Describe the page this result IS — the canonical pagination envelope.
133
76
  *
134
- * When called with two arguments both >= 0 and the first >= the second
135
- * (i.e. offset-style), pass `{ offset, limit }` as the first argument.
136
- * The simplest way is to always use the default (page, perPage) form and
137
- * let the autoCRUD layer supply offset/limit from the query string.
77
+ * Takes NO arguments and derives every field from the query that produced this
78
+ * result (ADR-0043). Passing ANY argument RAISES: a DatabaseResult holds no
79
+ * connection, so an argument could only re-slice the rows already in memory and
80
+ * then report total_pages for pages it can never reach. To read page N, FETCH
81
+ * page N (limit + offset) and call this with no arguments.
138
82
  *
139
- * Returns a superset of keys for backwards-compatibility across all clients.
140
- */
141
- /**
142
- * Describe the page this result actually IS. Takes no arguments.
83
+ * The envelope is EXACTLY seven snake_case keys, identical across all four
84
+ * frameworks: `records, total, page, per_page, total_pages, limit, offset`.
143
85
  *
144
- * MEASURED 2026-08-05 on a real 250-row table read with limit=20 offset=40
145
- * (page 3 of 13): this reported page 1 of 2 and returned 10 of the 20 rows.
146
- * It ignored the query entirely - defaulting page to 1 and perPage to 10 -
147
- * then re-sliced the rows it was handed, which were already just that page.
148
- * So a caller who paginated correctly at the SQL level had the answer
149
- * silently re-paginated underneath them, with a page number that was simply
150
- * wrong.
86
+ * per_page = the query's limit
87
+ * page = floor(offset / limit) + 1
88
+ * total = the TRUE total for the filter Database.fetch (and
89
+ * QueryBuilder.get) run a COUNT probe whenever a limit was
90
+ * applied NEVER the number of rows returned
91
+ * total_pages = ceil(total / per_page)
92
+ * records = the rows the query returned, VERBATIM (never re-sliced)
93
+ * limit = the SQL limit actually applied
94
+ * offset = the SQL offset actually applied
151
95
  *
152
- * WITH page/perPage it slices this result in memory, the behaviour GitHub
153
- * issue #106 asked for. Valid ONLY when the result holds the WHOLE set
154
- * (records.length >= count). A PARTIAL result cannot be sliced by page number
155
- * without lying: MEASURED on 100,000 rows read under the default cap of 100,
156
- * pages 1-5 of 20 were right and every page from 6 onward came back EMPTY
157
- * while totalPages reported 5,000.
96
+ * The JSON payload is snake_case even though the method name is camelCase — a
97
+ * JSON key is data, not a language surface (ADR-0043). The old duplicate and
98
+ * camelCase keys (`data`, `count`, `perPage`, `totalPages`, `has_next`,
99
+ * `has_prev`) are removed: Node emitted 13 keys, the worst offender of the four.
158
100
  *
159
- * `total` is `count`, and `count` is now the TRUE total for the filter in
160
- * all four frameworks - Database.fetch runs a COUNT probe whenever it applied
161
- * a limit. It used to be ROWS RETURNED here and in Ruby while Python and PHP
162
- * probed, so one query answered 20 in two frameworks and 250 in the other
163
- * two.
101
+ * @throws {TypeError} if called with any argument.
164
102
  */
165
- toPaginate(page, perPage) {
166
- if ((page !== void 0 || perPage !== void 0) && this.records.length < this.count) {
103
+ toPaginate() {
104
+ if (arguments.length > 0) {
167
105
  throw new TypeError(
168
- `toPaginate(page, perPage) slices the rows this result holds, but this result holds only ${this.records.length} of ${this.count} rows - it is a PARTIAL result, so any page past the rows it holds comes back empty while totalPages claims it exists. MEASURED on 100,000 rows read under the default cap of 100: pages 1-5 of 20 were right and pages 6 onward returned NOTHING. Fetch the page you want instead: fetch(sql, params, perPage, (page - 1) * perPage), then call toPaginate() with no arguments.`
106
+ "toPaginate() takes no arguments and derives the page from the query that ran (ADR-0043). A DatabaseResult holds no connection, so an argument could only re-slice the rows already in memory and report total_pages for pages it can never reach. To read a page, FETCH it: db.fetch(sql, params, perPage, (page - 1) * perPage), then call toPaginate() with no arguments."
169
107
  );
170
108
  }
171
- let resolvedPerPage;
172
- let resolvedPage;
173
- let offset;
174
- let rows;
175
- if (page === void 0 && perPage === void 0) {
176
- resolvedPerPage = this.limit > 0 ? this.limit : this.records.length;
177
- resolvedPage = resolvedPerPage > 0 ? Math.floor(this.offset / resolvedPerPage) + 1 : 1;
178
- offset = this.offset;
179
- rows = this.records;
180
- } else {
181
- resolvedPage = page ?? 1;
182
- resolvedPerPage = perPage ?? (this.limit > 0 ? this.limit : 10);
183
- offset = (resolvedPage - 1) * resolvedPerPage;
184
- rows = this.records.slice(offset, offset + resolvedPerPage);
185
- }
186
- const totalPages = resolvedPerPage > 0 ? Math.max(1, Math.ceil(this.count / resolvedPerPage)) : 1;
109
+ const perPage = this.limit > 0 ? this.limit : this.records.length;
110
+ const page = perPage > 0 ? Math.floor(this.offset / perPage) + 1 : 1;
111
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this.count / perPage)) : 1;
187
112
  return {
188
- records: rows,
189
- data: rows,
190
- count: this.count,
113
+ records: this.records,
191
114
  total: this.count,
192
- limit: resolvedPerPage,
193
- offset,
194
- page: resolvedPage,
195
- per_page: resolvedPerPage,
196
- perPage: resolvedPerPage,
197
- totalPages,
115
+ page,
116
+ per_page: perPage,
198
117
  total_pages: totalPages,
199
- has_next: resolvedPage < totalPages,
200
- has_prev: resolvedPage > 1
118
+ limit: perPage,
119
+ offset: this.offset
201
120
  };
202
121
  }
203
122
  /** Iterable — for (const row of result) */
@@ -3336,7 +3255,7 @@ ${s}\r
3336
3255
  connect() {
3337
3256
  if (this.connected) return Promise.resolve();
3338
3257
  if (this.connecting) return this.connecting;
3339
- this.connecting = new Promise((resolve21, reject) => {
3258
+ this.connecting = new Promise((resolve20, reject) => {
3340
3259
  const sock = net.createConnection({ host: this.host, port: this.port });
3341
3260
  sock.setNoDelay(true);
3342
3261
  const onError = (err) => {
@@ -3373,7 +3292,7 @@ ${s}\r
3373
3292
  sock.on("error", (e) => {
3374
3293
  this.brokenError = e;
3375
3294
  });
3376
- resolve21();
3295
+ resolve20();
3377
3296
  } catch (e) {
3378
3297
  onError(e);
3379
3298
  }
@@ -3436,12 +3355,12 @@ ${s}\r
3436
3355
  }
3437
3356
  /** Send one command and await its reply (assumes socket is up). */
3438
3357
  raw(args) {
3439
- return new Promise((resolve21, reject) => {
3358
+ return new Promise((resolve20, reject) => {
3440
3359
  if (!this.sock || this.sock.destroyed) {
3441
3360
  reject(this.brokenError ?? new Error("redis socket not connected"));
3442
3361
  return;
3443
3362
  }
3444
- this.waiters.push({ resolve: resolve21, reject });
3363
+ this.waiters.push({ resolve: resolve20, reject });
3445
3364
  this.sock.write(_RespClient.encode(args));
3446
3365
  });
3447
3366
  }
@@ -3783,7 +3702,7 @@ ${s}\r
3783
3702
  connect() {
3784
3703
  if (this.connected) return Promise.resolve();
3785
3704
  if (this.connecting) return this.connecting;
3786
- this.connecting = new Promise((resolve21, reject) => {
3705
+ this.connecting = new Promise((resolve20, reject) => {
3787
3706
  const sock = net.createConnection({ host: this.host, port: this.port });
3788
3707
  sock.setNoDelay(true);
3789
3708
  sock.once("error", (err) => {
@@ -3804,7 +3723,7 @@ ${s}\r
3804
3723
  p.resolve(this.buffer.toString("utf-8"));
3805
3724
  }
3806
3725
  });
3807
- resolve21();
3726
+ resolve20();
3808
3727
  });
3809
3728
  });
3810
3729
  return this.connecting;
@@ -3837,13 +3756,13 @@ ${s}\r
3837
3756
  async send(payload, terminator) {
3838
3757
  await this.connect();
3839
3758
  if (!this.sock || this.sock.destroyed) return "";
3840
- return new Promise((resolve21) => {
3759
+ return new Promise((resolve20) => {
3841
3760
  this.buffer = Buffer.alloc(0);
3842
- this.pending = { terminator, resolve: resolve21 };
3761
+ this.pending = { terminator, resolve: resolve20 };
3843
3762
  const timer = setTimeout(() => {
3844
- if (this.pending && this.pending.resolve === resolve21) {
3763
+ if (this.pending && this.pending.resolve === resolve20) {
3845
3764
  this.pending = null;
3846
- resolve21(this.buffer.toString("utf-8"));
3765
+ resolve20(this.buffer.toString("utf-8"));
3847
3766
  }
3848
3767
  }, 4e3);
3849
3768
  if (timer.unref) timer.unref();
@@ -5369,16 +5288,27 @@ async function parseBody(req2) {
5369
5288
  }
5370
5289
  const contentType = req2.headers["content-type"] ?? "";
5371
5290
  const chunks = [];
5372
- await new Promise((resolve21, reject) => {
5373
- req2.on("data", (chunk) => chunks.push(chunk));
5374
- req2.on("end", resolve21);
5291
+ await new Promise((resolve20, reject) => {
5292
+ let received = 0;
5293
+ let refused = false;
5294
+ req2.on("data", (chunk) => {
5295
+ if (refused) return;
5296
+ received += chunk.length;
5297
+ if (received > TINA4_MAX_UPLOAD_SIZE) {
5298
+ refused = true;
5299
+ chunks.length = 0;
5300
+ reject(new PayloadTooLargeError(received, TINA4_MAX_UPLOAD_SIZE));
5301
+ return;
5302
+ }
5303
+ chunks.push(chunk);
5304
+ });
5305
+ req2.on("end", () => {
5306
+ if (!refused) resolve20();
5307
+ });
5375
5308
  req2.on("error", reject);
5376
5309
  });
5377
5310
  const raw = Buffer.concat(chunks);
5378
5311
  if (raw.length === 0) return;
5379
- if (raw.length > TINA4_MAX_UPLOAD_SIZE) {
5380
- throw new PayloadTooLargeError(raw.length, TINA4_MAX_UPLOAD_SIZE);
5381
- }
5382
5312
  if (contentType.includes("multipart/form-data")) {
5383
5313
  const boundary = extractBoundary(contentType);
5384
5314
  if (boundary) {
@@ -13201,14 +13131,14 @@ data: ${channel.buffer.shift()}
13201
13131
  `;
13202
13132
  continue;
13203
13133
  }
13204
- const gotMessage = await new Promise((resolve21) => {
13134
+ const gotMessage = await new Promise((resolve20) => {
13205
13135
  const timer = setTimeout(() => {
13206
13136
  channel.wake = null;
13207
- resolve21(false);
13137
+ resolve20(false);
13208
13138
  }, keepaliveMs);
13209
13139
  channel.wake = () => {
13210
13140
  clearTimeout(timer);
13211
- resolve21(true);
13141
+ resolve20(true);
13212
13142
  };
13213
13143
  });
13214
13144
  if (!gotMessage) yield `: keep-alive
@@ -15723,7 +15653,7 @@ var init_websocket = __esm({
15723
15653
  * Start the WebSocket server.
15724
15654
  */
15725
15655
  async start() {
15726
- return new Promise((resolve21, reject) => {
15656
+ return new Promise((resolve20, reject) => {
15727
15657
  this.server = createServer((req2, res) => {
15728
15658
  res.writeHead(426, { "Content-Type": "text/plain" });
15729
15659
  res.end("Upgrade Required");
@@ -15733,7 +15663,7 @@ var init_websocket = __esm({
15733
15663
  });
15734
15664
  this.server.listen(this.port, () => {
15735
15665
  this.startIdleReaper();
15736
- resolve21();
15666
+ resolve20();
15737
15667
  });
15738
15668
  this.server.on("error", (err) => {
15739
15669
  this.emit("error", err);
@@ -16297,7 +16227,7 @@ var init_websocket = __esm({
16297
16227
  client.trackerId = this.onAdd(socket.remoteAddress ?? "unknown", "/__dev_reload");
16298
16228
  }
16299
16229
  this.clients.add(client);
16300
- const cleanup2 = () => {
16230
+ const cleanup = () => {
16301
16231
  if (!this.clients.has(client)) return;
16302
16232
  this.clients.delete(client);
16303
16233
  if (client.trackerId && this.onRemove) this.onRemove(client.trackerId);
@@ -16320,13 +16250,13 @@ var init_websocket = __esm({
16320
16250
  socket.end();
16321
16251
  } catch {
16322
16252
  }
16323
- cleanup2();
16253
+ cleanup();
16324
16254
  return;
16325
16255
  }
16326
16256
  }
16327
16257
  });
16328
- socket.on("close", cleanup2);
16329
- socket.on("error", cleanup2);
16258
+ socket.on("close", cleanup);
16259
+ socket.on("error", cleanup);
16330
16260
  return true;
16331
16261
  }
16332
16262
  /**
@@ -17844,7 +17774,7 @@ var init_queue = __esm({
17844
17774
  const jobs = this.popBatch(resolvedBatchSize);
17845
17775
  if (jobs.length === 0) {
17846
17776
  if (resolvedPollInterval <= 0) break;
17847
- await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
17777
+ await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
17848
17778
  continue;
17849
17779
  }
17850
17780
  yield jobs;
@@ -17854,7 +17784,7 @@ var init_queue = __esm({
17854
17784
  const raw = this.pop();
17855
17785
  if (raw === null) {
17856
17786
  if (resolvedPollInterval <= 0) break;
17857
- await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
17787
+ await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
17858
17788
  continue;
17859
17789
  }
17860
17790
  yield createJob(raw, this);
@@ -22374,23 +22304,23 @@ var init_devAdmin = __esm({
22374
22304
  });
22375
22305
  };
22376
22306
  handleDevAdminJs = async (_req, res) => {
22377
- const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
22378
- const { dirname: dirname14, join: join30, resolve: resolve21 } = await import("node:path");
22307
+ const { readFileSync: readFileSync26, existsSync: existsSync26 } = await import("node:fs");
22308
+ const { dirname: dirname13, join: join29, resolve: resolve20 } = await import("node:path");
22379
22309
  const { fileURLToPath: fileURLToPath7 } = await import("node:url");
22380
- const dir = dirname14(fileURLToPath7(import.meta.url));
22310
+ const dir = dirname13(fileURLToPath7(import.meta.url));
22381
22311
  const candidates = [
22382
- join30(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
22312
+ join29(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
22383
22313
  // src/../public/js/
22384
- join30(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
22314
+ join29(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
22385
22315
  // deeper nesting
22386
- resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
22387
- resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
22316
+ resolve20(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
22317
+ resolve20(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
22388
22318
  // project public/
22389
22319
  ];
22390
22320
  for (const jsPath of candidates) {
22391
- if (existsSync27(jsPath)) {
22321
+ if (existsSync26(jsPath)) {
22392
22322
  try {
22393
- const content = readFileSync27(jsPath, "utf-8");
22323
+ const content = readFileSync26(jsPath, "utf-8");
22394
22324
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
22395
22325
  res.raw.end(content);
22396
22326
  return;
@@ -22851,8 +22781,12 @@ function sanitizeSecurity(reqs, schemes) {
22851
22781
  function generate(routes, models = []) {
22852
22782
  const info = {
22853
22783
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
22854
- version: process.env.TINA4_SWAGGER_VERSION ?? "0.0.1",
22855
- description: process.env.TINA4_SWAGGER_DESCRIPTION ?? "Auto-generated API documentation"
22784
+ // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
22785
+ // 0.0.1). description defaults to the empty string, not a canned sentence.
22786
+ // Both are the settled cross-framework defaults (parity with the Python
22787
+ // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
22788
+ version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
22789
+ description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
22856
22790
  };
22857
22791
  const contactEmail = (process.env.TINA4_SWAGGER_CONTACT_EMAIL ?? "").trim();
22858
22792
  const contactName = (process.env.TINA4_SWAGGER_CONTACT_TEAM ?? "").trim();
@@ -22885,9 +22819,11 @@ function generate(routes, models = []) {
22885
22819
  const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
22886
22820
  const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
22887
22821
  const refSchemas = /* @__PURE__ */ new Set();
22822
+ const tableToSchema = /* @__PURE__ */ new Map();
22888
22823
  for (const model of models) {
22889
- const schema = modelToSchema(model);
22890
- spec.components.schemas[model.tableName] = schema;
22824
+ const schemaKey = schemaNameForModel(model);
22825
+ tableToSchema.set(model.tableName, schemaKey);
22826
+ spec.components.schemas[schemaKey] = modelToSchema(model);
22891
22827
  }
22892
22828
  const usedTags = [];
22893
22829
  const seenIds = /* @__PURE__ */ new Set();
@@ -22914,11 +22850,11 @@ function generate(routes, models = []) {
22914
22850
  if (route.meta?.deprecated) operation.deprecated = true;
22915
22851
  const pathParams = extractPathParams(route.pattern);
22916
22852
  if (pathParams.length > 0) {
22917
- operation.parameters = pathParams.map((name) => ({
22853
+ operation.parameters = pathParams.map(({ name, schema }) => ({
22918
22854
  name,
22919
22855
  in: "path",
22920
22856
  required: true,
22921
- schema: { type: "string" }
22857
+ schema
22922
22858
  }));
22923
22859
  }
22924
22860
  if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
@@ -22944,19 +22880,20 @@ function generate(routes, models = []) {
22944
22880
  };
22945
22881
  } else if (method === "post" || method === "put") {
22946
22882
  const modelName = inferModelFromPath(route.pattern);
22947
- if (modelName && models.some((m) => m.tableName === modelName)) {
22948
- const media = {
22949
- schema: { $ref: `#/components/schemas/${modelName}` }
22950
- };
22883
+ const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
22884
+ if (schemaKey) {
22885
+ const sref = `#/components/schemas/${schemaKey}`;
22886
+ const media = { schema: { $ref: sref } };
22951
22887
  if (route.meta?.example !== void 0) media.example = route.meta.example;
22952
22888
  operation.requestBody = {
22953
22889
  required: true,
22954
22890
  content: { "application/json": media }
22955
22891
  };
22956
- operation.responses = {
22957
- ...method === "post" ? { "201": { description: "Created", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } } : { "200": { description: "Updated", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } },
22958
- "422": { description: "Validation failed" }
22959
- };
22892
+ if (route.meta?.responses === void 0) {
22893
+ operation.responses = {
22894
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
22895
+ };
22896
+ }
22960
22897
  } else if (route.meta?.example !== void 0) {
22961
22898
  operation.requestBody = {
22962
22899
  content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
@@ -23043,6 +22980,21 @@ function resolveServers() {
23043
22980
  const dev = (process.env.SWAGGER_DEV_URL ?? "").trim();
23044
22981
  return dev.length > 0 ? [{ url: dev }] : [{ url: "/" }];
23045
22982
  }
22983
+ function schemaNameForModel(model) {
22984
+ const explicit = model.className?.trim();
22985
+ if (explicit) return explicit;
22986
+ return deriveClassName(model.tableName);
22987
+ }
22988
+ function deriveClassName(tableName) {
22989
+ return singularize(tableName).split(/[_\s-]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || tableName;
22990
+ }
22991
+ function singularize(word) {
22992
+ if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
22993
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) return word.slice(0, -2);
22994
+ if (/ss$/i.test(word)) return word;
22995
+ if (/s$/i.test(word) && word.length > 1) return word.slice(0, -1);
22996
+ return word;
22997
+ }
23046
22998
  function modelToSchema(model) {
23047
22999
  const properties = {};
23048
23000
  const required = [];
@@ -23116,15 +23068,35 @@ function inferSchema(value) {
23116
23068
  if (typeof value === "number") return { type: Number.isInteger(value) ? "integer" : "number" };
23117
23069
  return { type: "string" };
23118
23070
  }
23071
+ function segmentParam(segment) {
23072
+ if (segment.startsWith("{") && segment.endsWith("}")) {
23073
+ const inner = segment.slice(1, -1);
23074
+ if (inner.startsWith("...")) return { name: inner.slice(3), type: "string" };
23075
+ const colon = inner.indexOf(":");
23076
+ if (colon >= 0) return { name: inner.slice(0, colon), type: inner.slice(colon + 1) };
23077
+ return { name: inner, type: "string" };
23078
+ }
23079
+ if (segment.startsWith("[") && segment.endsWith("]")) {
23080
+ const inner = segment.slice(1, -1);
23081
+ return { name: inner.startsWith("...") ? inner.slice(3) : inner, type: "string" };
23082
+ }
23083
+ if (segment.startsWith(":") && segment.length > 1) {
23084
+ return { name: segment.slice(1), type: "string" };
23085
+ }
23086
+ return null;
23087
+ }
23119
23088
  function patternToOpenAPI(pattern) {
23120
- return pattern.replace(/\[\.\.\.(\w+)\]/g, "{$1}").replace(/\[(\w+)\]/g, "{$1}");
23089
+ return pattern.split("/").map((segment) => {
23090
+ const p = segmentParam(segment);
23091
+ return p ? `{${p.name}}` : segment;
23092
+ }).join("/");
23121
23093
  }
23122
23094
  function extractPathParams(pattern) {
23123
23095
  const params = [];
23124
- const regex = /\[(?:\.\.\.)?(\w+)\]/g;
23125
- let match;
23126
- while ((match = regex.exec(pattern)) !== null) {
23127
- params.push(match[1]);
23096
+ for (const segment of pattern.split("/")) {
23097
+ const p = segmentParam(segment);
23098
+ if (!p) continue;
23099
+ params.push({ name: p.name, schema: { ...PARAM_TYPE_SCHEMA[p.type] ?? { type: "string" } } });
23128
23100
  }
23129
23101
  return params;
23130
23102
  }
@@ -23146,8 +23118,12 @@ function inferModelFromPath(pattern) {
23146
23118
  if (rest.length === 1 && /^[[{]\.{0,3}\w+[\]}]$/.test(rest[0])) return candidate;
23147
23119
  return null;
23148
23120
  }
23121
+ function operationIdBase(method, openApiPath) {
23122
+ const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
23123
+ return clean ? `${method}_${clean}` : method;
23124
+ }
23149
23125
  function uniqueOperationId(method, openApiPath, seen) {
23150
- const base = (method + openApiPath.replace(/[/{}]/g, "_")).replace(/_+/g, "_").replace(/_$/, "");
23126
+ const base = operationIdBase(method, openApiPath);
23151
23127
  let oid = base;
23152
23128
  let n = 2;
23153
23129
  while (seen.has(oid)) {
@@ -23157,13 +23133,25 @@ function uniqueOperationId(method, openApiPath, seen) {
23157
23133
  seen.add(oid);
23158
23134
  return oid;
23159
23135
  }
23160
- var WRITE_METHODS, registeredSchemes, registeredSchemas;
23136
+ var WRITE_METHODS, registeredSchemes, registeredSchemas, PARAM_TYPE_SCHEMA;
23161
23137
  var init_generator = __esm({
23162
23138
  "../swagger/src/generator.ts"() {
23163
23139
  "use strict";
23164
23140
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
23165
23141
  registeredSchemes = {};
23166
23142
  registeredSchemas = {};
23143
+ PARAM_TYPE_SCHEMA = {
23144
+ int: { type: "integer" },
23145
+ integer: { type: "integer" },
23146
+ float: { type: "number" },
23147
+ number: { type: "number" },
23148
+ uuid: { type: "string", format: "uuid" },
23149
+ slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
23150
+ alpha: { type: "string", pattern: "^[A-Za-z]+$" },
23151
+ alnum: { type: "string", pattern: "^[A-Za-z0-9]+$" },
23152
+ path: { type: "string" },
23153
+ string: { type: "string" }
23154
+ };
23167
23155
  }
23168
23156
  });
23169
23157
 
@@ -23483,10 +23471,29 @@ function openBrowser(url) {
23483
23471
  }, 2e3);
23484
23472
  }
23485
23473
  function resolvePortAndHost(config) {
23486
- const port = config?.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? 7148;
23474
+ const tina4Port = process.env.TINA4_PORT;
23475
+ const legacyPort = process.env.PORT;
23476
+ let port;
23477
+ if (config?.port !== void 0) {
23478
+ port = config.port;
23479
+ } else if (tina4Port && /^\d+$/.test(tina4Port)) {
23480
+ port = parseInt(tina4Port, 10);
23481
+ } else if (legacyPort && /^\d+$/.test(legacyPort)) {
23482
+ port = parseInt(legacyPort, 10);
23483
+ warnDeprecatedPort(port);
23484
+ } else {
23485
+ port = 7148;
23486
+ }
23487
23487
  const host = config?.host ?? process.env.TINA4_HOST ?? process.env.HOST ?? "0.0.0.0";
23488
23488
  return { port, host };
23489
23489
  }
23490
+ function warnDeprecatedPort(port) {
23491
+ if (portDeprecationWarned) return;
23492
+ portDeprecationWarned = true;
23493
+ Log.warning(
23494
+ `PORT is deprecated and will be removed in 3.14 - use TINA4_PORT instead (binding port ${port} from PORT)`
23495
+ );
23496
+ }
23490
23497
  function isBannerSuppressed() {
23491
23498
  return isTruthy(process.env.TINA4_SUPPRESS);
23492
23499
  }
@@ -23758,6 +23765,29 @@ function deployGallery(name) {
23758
23765
  </body>
23759
23766
  </html>`;
23760
23767
  }
23768
+ function startLoopWatchdog() {
23769
+ const raw = (process.env.TINA4_LOOP_LAG_WARN_MS ?? "").trim();
23770
+ const threshold = /^\d+$/.test(raw) ? parseInt(raw, 10) : 250;
23771
+ if (threshold <= 0) {
23772
+ return { stop: () => {
23773
+ } };
23774
+ }
23775
+ let last = Date.now();
23776
+ let warned = 0;
23777
+ const timer = setInterval(() => {
23778
+ const now = Date.now();
23779
+ const lag = now - last - LOOP_WATCHDOG_TICK_MS;
23780
+ last = now;
23781
+ if (lag < threshold) return;
23782
+ warned++;
23783
+ if (warned > 5 && warned % 20 !== 0) return;
23784
+ Log.warning(
23785
+ `Event loop blocked for ${lag}ms. Node serves every request on one loop, so a handler doing CPU-bound work or synchronous I/O stalls all the others for that long. Move the work to Tina4's queue, or await it. Set TINA4_LOOP_LAG_WARN_MS to change the ${threshold}ms threshold, or 0 to silence.`
23786
+ );
23787
+ }, LOOP_WATCHDOG_TICK_MS);
23788
+ timer.unref();
23789
+ return { stop: () => clearInterval(timer) };
23790
+ }
23761
23791
  async function start(config) {
23762
23792
  const isManaged = process.argv.includes("--managed");
23763
23793
  if (!isManaged && process.env.TINA4_OVERRIDE_CLIENT !== "true") {
@@ -24007,7 +24037,9 @@ async function startServer(config) {
24007
24037
  const resolved = resolvePortAndHost(config);
24008
24038
  const host = resolved.host;
24009
24039
  let port = resolved.port;
24010
- port = findAvailablePort(port);
24040
+ if (!cluster.isWorker) {
24041
+ port = findAvailablePort(port);
24042
+ }
24011
24043
  const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
24012
24044
  if (cluster.isPrimary && isProduction) {
24013
24045
  const numCPUs = os2.cpus().length;
@@ -24223,7 +24255,20 @@ ${reset2}
24223
24255
  await sessionAutoStart(rawReq, rawRes, req2);
24224
24256
  await middleware.run(req2, res);
24225
24257
  if (res.raw.writableEnded) return;
24226
- await req2.parseBody();
24258
+ try {
24259
+ await req2.parseBody();
24260
+ } catch (err) {
24261
+ const status2 = err?.statusCode;
24262
+ if (typeof status2 === "number" && status2 >= 400 && status2 < 500) {
24263
+ if (!rawRes.writableEnded) {
24264
+ rawRes.statusCode = status2;
24265
+ rawRes.setHeader("content-type", "application/json");
24266
+ rawRes.end(JSON.stringify({ error: err.message }));
24267
+ }
24268
+ return;
24269
+ }
24270
+ throw err;
24271
+ }
24227
24272
  const pathname = req2.path;
24228
24273
  const reqStartTime = DevAdmin.isEnabled() ? Date.now() : 0;
24229
24274
  const matchedPattern = { value: "" };
@@ -24422,8 +24467,10 @@ ${reset2}
24422
24467
  };
24423
24468
  process.on("SIGTERM", onSigterm);
24424
24469
  process.on("SIGINT", onSigint);
24470
+ const loopWatchdog = startLoopWatchdog();
24425
24471
  resolvePromise({
24426
24472
  close: () => {
24473
+ loopWatchdog.stop();
24427
24474
  process.off("SIGTERM", onSigterm);
24428
24475
  process.off("SIGINT", onSigint);
24429
24476
  stopAllBackgroundTasks();
@@ -24438,7 +24485,7 @@ ${reset2}
24438
24485
  });
24439
24486
  });
24440
24487
  }
24441
- var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swaggerAssetsEnabled, DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TINA4_VERSION2, frondCache, _LEGACY_ENV_VARS, TEMPLATE_PAGES_DIR, HTTP_REASON_PHRASES, templateCache, _dispatchFn, _serverHandle, FALLBACK_STAGES;
24488
+ var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swaggerAssetsEnabled, DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TINA4_VERSION2, frondCache, _LEGACY_ENV_VARS, portDeprecationWarned, TEMPLATE_PAGES_DIR, HTTP_REASON_PHRASES, templateCache, _dispatchFn, _serverHandle, LOOP_WATCHDOG_TICK_MS, FALLBACK_STAGES;
24442
24489
  var init_server = __esm({
24443
24490
  "../core/src/server.ts"() {
24444
24491
  "use strict";
@@ -24491,6 +24538,7 @@ var init_server = __esm({
24491
24538
  SWAGGER_VERSION: "TINA4_SWAGGER_VERSION",
24492
24539
  ORM_PLURAL_TABLE_NAMES: "TINA4_ORM_PLURAL_TABLE_NAMES"
24493
24540
  };
24541
+ portDeprecationWarned = false;
24494
24542
  TEMPLATE_PAGES_DIR = "pages";
24495
24543
  HTTP_REASON_PHRASES = {
24496
24544
  100: "Continue",
@@ -24527,6 +24575,7 @@ var init_server = __esm({
24527
24575
  templateCache = null;
24528
24576
  _dispatchFn = null;
24529
24577
  _serverHandle = null;
24578
+ LOOP_WATCHDOG_TICK_MS = 100;
24530
24579
  FALLBACK_STAGES = [
24531
24580
  serveTemplateFallback,
24532
24581
  serveLandingPage,
@@ -25082,433 +25131,6 @@ var init_fakeData = __esm({
25082
25131
  }
25083
25132
  });
25084
25133
 
25085
- // ../core/src/scss.ts
25086
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, existsSync as existsSync23, mkdirSync as mkdirSync14, readdirSync as readdirSync16 } from "node:fs";
25087
- import { join as join23, resolve as resolve16, dirname as dirname10 } from "node:path";
25088
- function compileString(scss, importPaths, variables) {
25089
- const imported = /* @__PURE__ */ new Set();
25090
- scss = resolveImports(scss, importPaths, imported);
25091
- scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
25092
- scss = extractVariables(scss, variables);
25093
- const mixins = {};
25094
- scss = extractMixins(scss, mixins);
25095
- scss = resolveIncludes(scss, mixins);
25096
- scss = resolveInterpolation(scss, variables);
25097
- scss = substituteVariables(scss, variables);
25098
- scss = evalMath(scss);
25099
- scss = resolveColorFunctions(scss);
25100
- const css = flattenNesting(scss);
25101
- return cleanup(css);
25102
- }
25103
- function resolveImports(content, paths, imported) {
25104
- return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name) => {
25105
- name = name.trim();
25106
- const candidates = [];
25107
- for (const base of paths) {
25108
- candidates.push(
25109
- join23(base, `${name}.scss`),
25110
- join23(base, `_${name}.scss`),
25111
- join23(base, name)
25112
- );
25113
- }
25114
- for (const candidate of candidates) {
25115
- if (existsSync23(candidate) && !imported.has(candidate)) {
25116
- imported.add(candidate);
25117
- const fileContent = readFileSync21(candidate, "utf-8");
25118
- return resolveImports(fileContent, [dirname10(candidate), ...paths], imported);
25119
- }
25120
- }
25121
- return `/* IMPORT NOT FOUND: ${name} */`;
25122
- });
25123
- }
25124
- function stripVariableFlags(value) {
25125
- let declaresDefault = false;
25126
- for (; ; ) {
25127
- const match = VARIABLE_FLAG.exec(value);
25128
- if (match === null) return [value.trim(), declaresDefault];
25129
- if (match[1] === "default") declaresDefault = true;
25130
- value = value.slice(0, match.index);
25131
- }
25132
- }
25133
- function extractVariables(scss, variables) {
25134
- return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name, value) => {
25135
- const [stripped, declaresDefault] = stripVariableFlags(value.trim());
25136
- if (declaresDefault && (variables[name] ?? "null") !== "null") {
25137
- return "";
25138
- }
25139
- let resolved = stripped;
25140
- for (const [vName, vVal] of Object.entries(variables)) {
25141
- resolved = resolved.replaceAll(`$${vName}`, vVal);
25142
- }
25143
- variables[name] = resolved;
25144
- return "";
25145
- });
25146
- }
25147
- function substituteVariables(scss, variables) {
25148
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
25149
- for (const name of sorted) {
25150
- scss = scss.replaceAll(`$${name}`, variables[name]);
25151
- }
25152
- return scss;
25153
- }
25154
- function resolveInterpolation(scss, variables) {
25155
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
25156
- return scss.replace(/#\{([^{}]*)\}/g, (_m, inner) => {
25157
- let resolved = inner.trim();
25158
- for (const name of sorted) {
25159
- resolved = resolved.replaceAll(`$${name}`, variables[name]);
25160
- }
25161
- return resolved;
25162
- });
25163
- }
25164
- function extractMixins(scss, mixins) {
25165
- const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
25166
- let match;
25167
- const locations = [];
25168
- while ((match = pattern.exec(scss)) !== null) {
25169
- const name = match[1];
25170
- const paramsStr = match[2] ?? "";
25171
- const params = paramsStr.split(",").map((p) => p.trim().replace(/^\$/, "")).filter(Boolean);
25172
- const bodyStart = match.index + match[0].length;
25173
- const body = findBlock(scss, bodyStart);
25174
- if (body !== null) {
25175
- mixins[name] = { params, body };
25176
- locations.push({
25177
- start: match.index,
25178
- end: bodyStart + body.length + 1,
25179
- name
25180
- });
25181
- }
25182
- }
25183
- let result = scss;
25184
- for (const loc of locations.reverse()) {
25185
- result = result.slice(0, loc.start) + result.slice(loc.end);
25186
- }
25187
- return result;
25188
- }
25189
- function resolveIncludes(scss, mixins) {
25190
- return scss.replace(
25191
- /@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
25192
- (_m, name, argsStr) => {
25193
- if (!(name in mixins)) {
25194
- return `/* MIXIN NOT FOUND: ${name} */`;
25195
- }
25196
- const mixin = mixins[name];
25197
- const args = argsStr ? argsStr.split(",").map((a) => a.trim()).filter(Boolean) : [];
25198
- let body = mixin.body;
25199
- for (let i = 0; i < mixin.params.length; i++) {
25200
- const paramName = mixin.params[i].split(":")[0].trim();
25201
- const defaultVal = mixin.params[i].includes(":") ? mixin.params[i].split(":").slice(1).join(":").trim() : "";
25202
- const value = i < args.length ? args[i] : defaultVal;
25203
- body = body.replaceAll(`$${paramName}`, value);
25204
- }
25205
- return body;
25206
- }
25207
- );
25208
- }
25209
- function evalMath(scss) {
25210
- const placeholders = [];
25211
- const masked = scss.replace(/calc\([^()]*\)/g, (m) => {
25212
- placeholders.push(m);
25213
- return `\0CALC${placeholders.length - 1}\0`;
25214
- });
25215
- const folded = masked.replace(
25216
- /([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
25217
- (full, n1, u1, op, n2, u2) => {
25218
- const num1 = parseFloat(n1);
25219
- const num2 = parseFloat(n2);
25220
- if (Number.isNaN(num1) || Number.isNaN(num2)) return full;
25221
- const unit1 = u1 || "";
25222
- const unit2 = u2 || "";
25223
- let unit;
25224
- if (unit1 === unit2) {
25225
- unit = unit1;
25226
- } else if ((op === "*" || op === "/") && unit1 === "") {
25227
- unit = unit2;
25228
- } else if ((op === "*" || op === "/") && unit2 === "") {
25229
- unit = unit1;
25230
- } else {
25231
- return full;
25232
- }
25233
- let result;
25234
- switch (op) {
25235
- case "+":
25236
- result = num1 + num2;
25237
- break;
25238
- case "-":
25239
- result = num1 - num2;
25240
- break;
25241
- case "*":
25242
- result = num1 * num2;
25243
- break;
25244
- case "/":
25245
- if (num2 === 0) return full;
25246
- result = num1 / num2;
25247
- break;
25248
- default:
25249
- return full;
25250
- }
25251
- if (result === Math.floor(result)) {
25252
- return `${Math.floor(result)}${unit}`;
25253
- }
25254
- return `${result.toFixed(2)}${unit}`;
25255
- }
25256
- );
25257
- return folded.replace(/\x00CALC(\d+)\x00/g, (_m, idx) => {
25258
- return placeholders[parseInt(idx, 10)];
25259
- });
25260
- }
25261
- function resolveColorFunctions(scss) {
25262
- scss = scss.replace(
25263
- /lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
25264
- (_m, color, amt) => adjustLightness(color.trim(), parseFloat(amt.trim().replace(/%$/, "")) / 100)
25265
- );
25266
- scss = scss.replace(
25267
- /darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
25268
- (_m, color, amt) => adjustLightness(color.trim(), -(parseFloat(amt.trim().replace(/%$/, "")) / 100))
25269
- );
25270
- scss = scss.replace(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/g, (whole, hex, alpha) => {
25271
- const rgb = hexToRgb(hex);
25272
- return rgb === null ? whole : `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.trim()})`;
25273
- });
25274
- scss = scss.replace(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/g, (whole, hex) => {
25275
- const rgb = hexToRgb(hex);
25276
- return rgb === null ? whole : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
25277
- });
25278
- scss = scss.replace(
25279
- /mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/g,
25280
- (whole, h1, h2, weight) => {
25281
- const c1 = hexToRgb(h1);
25282
- const c2 = hexToRgb(h2);
25283
- if (c1 === null || c2 === null) return whole;
25284
- const w = weight ? parseFloat(weight.replace(/%$/, "")) / 100 : 0.5;
25285
- const mixed = [0, 1, 2].map((i) => Math.round(c1[i] * w + c2[i] * (1 - w)));
25286
- return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
25287
- }
25288
- );
25289
- return scss;
25290
- }
25291
- function hexToRgb(color) {
25292
- let c = color.trim().replace(/^#/, "");
25293
- if (c.length === 3) {
25294
- c = c.split("").map((ch) => ch + ch).join("");
25295
- }
25296
- if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
25297
- return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
25298
- }
25299
- function adjustLightness(color, amount) {
25300
- const rgb = hexToRgb(color);
25301
- if (rgb === null) return color;
25302
- let [r, g, b] = rgb.map((v) => v / 255);
25303
- const max = Math.max(r, g, b);
25304
- const min = Math.min(r, g, b);
25305
- let l = (max + min) / 2;
25306
- const d = max - min;
25307
- let h = 0;
25308
- let s = 0;
25309
- if (d !== 0) {
25310
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
25311
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
25312
- else if (max === g) h = (b - r) / d + 2;
25313
- else h = (r - g) / d + 4;
25314
- h /= 6;
25315
- }
25316
- l = Math.max(0, Math.min(1, l + amount));
25317
- if (s === 0) {
25318
- r = g = b = l;
25319
- } else {
25320
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
25321
- const p = 2 * l - q;
25322
- r = hueToRgb(p, q, h + 1 / 3);
25323
- g = hueToRgb(p, q, h);
25324
- b = hueToRgb(p, q, h - 1 / 3);
25325
- }
25326
- const hex = (v) => Math.trunc(v * 255).toString(16).padStart(2, "0");
25327
- return `#${hex(r)}${hex(g)}${hex(b)}`;
25328
- }
25329
- function hueToRgb(p, q, t) {
25330
- if (t < 0) t += 1;
25331
- if (t > 1) t -= 1;
25332
- if (t < 1 / 6) return p + (q - p) * 6 * t;
25333
- if (t < 1 / 2) return q;
25334
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
25335
- return p;
25336
- }
25337
- function flattenNesting(scss) {
25338
- const output = [];
25339
- flattenBlock(scss, [], output);
25340
- return output.join("\n");
25341
- }
25342
- function flattenBlock(content, parentSelectors, output) {
25343
- let pos = 0;
25344
- const properties = [];
25345
- while (pos < content.length) {
25346
- while (pos < content.length && /[\s]/.test(content[pos])) {
25347
- pos++;
25348
- }
25349
- if (pos >= content.length) break;
25350
- if (content[pos] === "/" && content[pos + 1] === "*") {
25351
- const end = content.indexOf("*/", pos + 2);
25352
- if (end === -1) break;
25353
- output.push(content.slice(pos, end + 2));
25354
- pos = end + 2;
25355
- continue;
25356
- }
25357
- if (content.slice(pos, pos + 6) === "@media") {
25358
- const brace = content.indexOf("{", pos);
25359
- if (brace === -1) break;
25360
- const mediaQuery = content.slice(pos, brace).trim();
25361
- const body = findBlock(content, brace + 1);
25362
- if (body === null) break;
25363
- pos = brace + 1 + body.length + 1;
25364
- const innerOutput = [];
25365
- flattenBlock(body, parentSelectors, innerOutput);
25366
- if (innerOutput.length > 0) {
25367
- output.push(`${mediaQuery} {`);
25368
- for (const line of innerOutput) {
25369
- output.push(` ${line}`);
25370
- }
25371
- output.push("}");
25372
- }
25373
- continue;
25374
- }
25375
- const bracePos = content.indexOf("{", pos);
25376
- const semiPos = content.indexOf(";", pos);
25377
- if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
25378
- const prop = content.slice(pos, semiPos).trim();
25379
- if (prop && !prop.startsWith("@")) {
25380
- properties.push(prop);
25381
- }
25382
- pos = semiPos + 1;
25383
- continue;
25384
- }
25385
- if (bracePos !== -1) {
25386
- const selectorText = content.slice(pos, bracePos).trim();
25387
- const body = findBlock(content, bracePos + 1);
25388
- if (body === null) break;
25389
- pos = bracePos + 1 + body.length + 1;
25390
- if (!selectorText) continue;
25391
- const selectors = selectorText.split(",").map((s) => s.trim());
25392
- const newSelectors = [];
25393
- for (const sel of selectors) {
25394
- if (parentSelectors.length > 0) {
25395
- for (const parent of parentSelectors) {
25396
- if (sel.includes("&")) {
25397
- newSelectors.push(sel.replace(/&/g, parent));
25398
- } else {
25399
- newSelectors.push(`${parent} ${sel}`);
25400
- }
25401
- }
25402
- } else {
25403
- newSelectors.push(sel);
25404
- }
25405
- }
25406
- flattenBlock(body, newSelectors, output);
25407
- continue;
25408
- }
25409
- const remaining = content.slice(pos).trim();
25410
- if (remaining) {
25411
- properties.push(remaining);
25412
- }
25413
- break;
25414
- }
25415
- if (properties.length > 0 && parentSelectors.length > 0) {
25416
- const selectorStr = parentSelectors.join(", ");
25417
- output.push(`${selectorStr} {`);
25418
- for (const prop of properties) {
25419
- output.push(` ${prop};`);
25420
- }
25421
- output.push("}");
25422
- }
25423
- }
25424
- function findBlock(content, start2) {
25425
- let depth = 1;
25426
- let pos = start2;
25427
- while (pos < content.length && depth > 0) {
25428
- if (content[pos] === "{") depth++;
25429
- else if (content[pos] === "}") depth--;
25430
- if (depth > 0) pos++;
25431
- }
25432
- return depth === 0 ? content.slice(start2, pos) : null;
25433
- }
25434
- function cleanup(css) {
25435
- css = css.replace(/[^{}]+\{\s*\}/g, "");
25436
- css = css.replace(/\n{3,}/g, "\n\n");
25437
- css = css.split("\n").map((line) => line.trimEnd()).join("\n");
25438
- return css.trim() + "\n";
25439
- }
25440
- var ScssCompiler, VARIABLE_FLAG;
25441
- var init_scss = __esm({
25442
- "../core/src/scss.ts"() {
25443
- "use strict";
25444
- ScssCompiler = class {
25445
- _importPaths;
25446
- _variables;
25447
- constructor(config) {
25448
- this._importPaths = config?.importPaths ? [...config.importPaths] : [];
25449
- this._variables = config?.variables ? { ...config.variables } : {};
25450
- }
25451
- /** Compile an SCSS string to CSS. */
25452
- compile(source) {
25453
- return compileString(source, this._importPaths, { ...this._variables });
25454
- }
25455
- /** Compile an SCSS file to CSS. */
25456
- compileFile(filePath) {
25457
- const absPath = resolve16(filePath);
25458
- const content = readFileSync21(absPath, "utf-8");
25459
- const paths = [dirname10(absPath), ...this._importPaths];
25460
- return compileString(content, paths, { ...this._variables });
25461
- }
25462
- /** Add a directory to the import resolution path. */
25463
- addImportPath(path8) {
25464
- this._importPaths.push(resolve16(path8));
25465
- }
25466
- /** Set or override an SCSS variable. */
25467
- setVariable(name, value) {
25468
- const key = name.startsWith("$") ? name.slice(1) : name;
25469
- this._variables[key] = value;
25470
- }
25471
- /** Compile all .scss files in a directory into a single CSS output file. */
25472
- compileScss(scssDir = "src/scss", output = "src/public/css/default.css", minify = false) {
25473
- const absDir = resolve16(scssDir);
25474
- if (!existsSync23(absDir)) return "";
25475
- const files = readdirSync16(absDir).filter((f) => f.endsWith(".scss") && !f.startsWith("_")).sort().map((f) => join23(absDir, f));
25476
- if (files.length === 0) return "";
25477
- const paths = [absDir, ...this._importPaths];
25478
- const imported = /* @__PURE__ */ new Set();
25479
- let merged = "";
25480
- for (const file of files) {
25481
- const content = readFileSync21(file, "utf-8");
25482
- imported.add(file);
25483
- merged += resolveImports(content, paths, imported) + "\n";
25484
- }
25485
- let css = compileString(merged, paths, { ...this._variables });
25486
- if (minify) {
25487
- css = css.replace(/\/\*.*?\*\//gs, "");
25488
- css = css.replace(/\s+/g, " ");
25489
- css = css.replace(/\s*([{}:;,])\s*/g, "$1");
25490
- css = css.replace(/;}/g, "}");
25491
- css = css.trim();
25492
- }
25493
- const absOutput = resolve16(output);
25494
- const outDir = dirname10(absOutput);
25495
- if (!existsSync23(outDir)) mkdirSync14(outDir, { recursive: true });
25496
- let existing = null;
25497
- try {
25498
- existing = existsSync23(absOutput) ? readFileSync21(absOutput, "utf-8") : null;
25499
- } catch {
25500
- existing = null;
25501
- }
25502
- if (existing !== css) {
25503
- writeFileSync14(absOutput, css, "utf-8");
25504
- }
25505
- return css;
25506
- }
25507
- };
25508
- VARIABLE_FLAG = /\s*!(default|global)\s*$/;
25509
- }
25510
- });
25511
-
25512
25134
  // ../core/src/mqttMessage.ts
25513
25135
  var MqttMessage;
25514
25136
  var init_mqttMessage = __esm({
@@ -25582,7 +25204,7 @@ var init_mqttMessage = __esm({
25582
25204
  import net2 from "node:net";
25583
25205
  import tls from "node:tls";
25584
25206
  import { randomBytes as randomBytes5 } from "node:crypto";
25585
- import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
25207
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "node:fs";
25586
25208
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
25587
25209
  var init_mqtt = __esm({
25588
25210
  "../core/src/mqtt.ts"() {
@@ -25785,7 +25407,7 @@ var init_mqtt = __esm({
25785
25407
  */
25786
25408
  async connect() {
25787
25409
  this.closeSocket();
25788
- if (this.secure && this.tlsVerify && this.caFile && !existsSync24(this.caFile)) {
25410
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync23(this.caFile)) {
25789
25411
  throw new MqttError(
25790
25412
  `MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
25791
25413
  );
@@ -26022,7 +25644,7 @@ var init_mqtt = __esm({
26022
25644
  * a later client.
26023
25645
  */
26024
25646
  openSocket() {
26025
- return new Promise((resolve21, reject) => {
25647
+ return new Promise((resolve20, reject) => {
26026
25648
  let settled = false;
26027
25649
  const settle = (fn) => {
26028
25650
  if (settled) return;
@@ -26047,10 +25669,10 @@ var init_mqtt = __esm({
26047
25669
  servername: this.host,
26048
25670
  rejectUnauthorized: this.tlsVerify
26049
25671
  };
26050
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
26051
- sock = tls.connect(opts, () => settle(() => resolve21(sock)));
25672
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync21(this.caFile);
25673
+ sock = tls.connect(opts, () => settle(() => resolve20(sock)));
26052
25674
  } else {
26053
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
25675
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
26054
25676
  }
26055
25677
  sock.once("error", (err) => {
26056
25678
  settle(() => {
@@ -26089,13 +25711,13 @@ var init_mqtt = __esm({
26089
25711
  writePacket(header, body) {
26090
25712
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
26091
25713
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
26092
- return new Promise((resolve21, reject) => {
25714
+ return new Promise((resolve20, reject) => {
26093
25715
  this.socket.write(packet, (err) => {
26094
25716
  if (err) {
26095
25717
  reject(new MqttError(`MQTT write failed: ${err.message}`));
26096
25718
  } else {
26097
25719
  this.lastWriteAt = Date.now();
26098
- resolve21();
25720
+ resolve20();
26099
25721
  }
26100
25722
  });
26101
25723
  });
@@ -26128,7 +25750,7 @@ var init_mqtt = __esm({
26128
25750
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
26129
25751
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
26130
25752
  if (this.socketError !== null) return Promise.reject(this.socketError);
26131
- return new Promise((resolve21, reject) => {
25753
+ return new Promise((resolve20, reject) => {
26132
25754
  let timer = null;
26133
25755
  if (deadline !== null) {
26134
25756
  const remaining = deadline - Date.now();
@@ -26143,7 +25765,7 @@ var init_mqtt = __esm({
26143
25765
  }
26144
25766
  }, remaining);
26145
25767
  }
26146
- this.waiter = { need, resolve: resolve21, reject, timer };
25768
+ this.waiter = { need, resolve: resolve20, reject, timer };
26147
25769
  this.serviceWaiter();
26148
25770
  });
26149
25771
  }
@@ -26267,8 +25889,8 @@ var init_mqtt = __esm({
26267
25889
  });
26268
25890
 
26269
25891
  // ../core/src/service.ts
26270
- import { readdirSync as readdirSync17, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
26271
- import { join as join24, extname as extname7 } from "node:path";
25892
+ import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
25893
+ import { join as join23, extname as extname7 } from "node:path";
26272
25894
  import { pathToFileURL } from "node:url";
26273
25895
  function matchCronField(field, value) {
26274
25896
  if (field === "*") return true;
@@ -26434,14 +26056,14 @@ var init_service = __esm({
26434
26056
  const discovered = [];
26435
26057
  let entries;
26436
26058
  try {
26437
- entries = readdirSync17(dir);
26059
+ entries = readdirSync16(dir);
26438
26060
  } catch {
26439
26061
  return discovered;
26440
26062
  }
26441
26063
  for (const entry of entries) {
26442
26064
  const ext = extname7(entry);
26443
26065
  if (ext !== ".ts" && ext !== ".js") continue;
26444
- const fullPath = join24(dir, entry);
26066
+ const fullPath = join23(dir, entry);
26445
26067
  const stat = statSync16(fullPath);
26446
26068
  if (!stat.isFile()) continue;
26447
26069
  try {
@@ -26550,14 +26172,14 @@ var init_service = __esm({
26550
26172
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
26551
26173
  let entries;
26552
26174
  try {
26553
- entries = readdirSync17(dir);
26175
+ entries = readdirSync16(dir);
26554
26176
  } catch {
26555
26177
  return;
26556
26178
  }
26557
26179
  for (const entry of entries) {
26558
26180
  const ext = extname7(entry);
26559
26181
  if (ext !== ".ts" && ext !== ".js") continue;
26560
- const fullPath = join24(dir, entry);
26182
+ const fullPath = join23(dir, entry);
26561
26183
  if (watchedFiles.has(fullPath)) continue;
26562
26184
  watchedFiles.add(fullPath);
26563
26185
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -26601,7 +26223,7 @@ import https from "node:https";
26601
26223
  import { URL as URL2 } from "node:url";
26602
26224
  import { randomBytes as randomBytes6 } from "node:crypto";
26603
26225
  import { promises as fsp, createWriteStream } from "node:fs";
26604
- import { basename as basename5 } from "node:path";
26226
+ import { basename as basename4 } from "node:path";
26605
26227
  import { pipeline } from "node:stream/promises";
26606
26228
  function sameOrigin(urlA, urlB) {
26607
26229
  try {
@@ -26891,7 +26513,7 @@ var init_api = __esm({
26891
26513
  error: err instanceof Error ? err.message : String(err)
26892
26514
  };
26893
26515
  }
26894
- uploadName = filename || basename5(filePath);
26516
+ uploadName = filename || basename4(filePath);
26895
26517
  } else {
26896
26518
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
26897
26519
  }
@@ -27106,12 +26728,12 @@ var init_api = __esm({
27106
26728
  * authenticate to.
27107
26729
  */
27108
26730
  performRequest(method, url, headers, data, redirectsLeft) {
27109
- return new Promise((resolve21) => {
26731
+ return new Promise((resolve20) => {
27110
26732
  let parsed;
27111
26733
  try {
27112
26734
  parsed = new URL2(url);
27113
26735
  } catch (err) {
27114
- resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
26736
+ resolve20({ kind: "error", error: err instanceof Error ? err.message : String(err) });
27115
26737
  return;
27116
26738
  }
27117
26739
  const isHttps = parsed.protocol === "https:";
@@ -27136,7 +26758,7 @@ var init_api = __esm({
27136
26758
  try {
27137
26759
  nextUrl = new URL2(location, url).toString();
27138
26760
  } catch {
27139
- resolve21({ kind: "response", res });
26761
+ resolve20({ kind: "response", res });
27140
26762
  return;
27141
26763
  }
27142
26764
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -27154,17 +26776,17 @@ var init_api = __esm({
27154
26776
  deleteHeaderCaseInsensitive(nextHeaders, name);
27155
26777
  }
27156
26778
  }
27157
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
26779
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve20);
27158
26780
  return;
27159
26781
  }
27160
- resolve21({ kind: "response", res });
26782
+ resolve20({ kind: "response", res });
27161
26783
  });
27162
26784
  req2.on("timeout", () => {
27163
26785
  req2.destroy();
27164
- resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
26786
+ resolve20({ kind: "error", error: `Request timed out after ${this.timeout}s` });
27165
26787
  });
27166
26788
  req2.on("error", (err) => {
27167
- resolve21({ kind: "error", error: err.message });
26789
+ resolve20({ kind: "error", error: err.message });
27168
26790
  });
27169
26791
  if (data) {
27170
26792
  req2.write(data);
@@ -27174,7 +26796,7 @@ var init_api = __esm({
27174
26796
  }
27175
26797
  /** Buffer a response body, parse JSON if possible, and store cookies. */
27176
26798
  readResponse(res) {
27177
- return new Promise((resolve21) => {
26799
+ return new Promise((resolve20) => {
27178
26800
  const chunks = [];
27179
26801
  res.on("data", (chunk) => {
27180
26802
  chunks.push(chunk);
@@ -27189,7 +26811,7 @@ var init_api = __esm({
27189
26811
  } catch {
27190
26812
  parsed = raw;
27191
26813
  }
27192
- resolve21({
26814
+ resolve20({
27193
26815
  http_code: res.statusCode ?? null,
27194
26816
  body: parsed,
27195
26817
  headers: respHeaders,
@@ -27197,7 +26819,7 @@ var init_api = __esm({
27197
26819
  });
27198
26820
  });
27199
26821
  res.on("error", (err) => {
27200
- resolve21({ http_code: null, body: null, headers: {}, error: err.message });
26822
+ resolve20({ http_code: null, body: null, headers: {}, error: err.message });
27201
26823
  });
27202
26824
  });
27203
26825
  }
@@ -27250,14 +26872,14 @@ var init_api = __esm({
27250
26872
  // ../core/src/messenger.ts
27251
26873
  import net3 from "node:net";
27252
26874
  import tls2 from "node:tls";
27253
- import { readFileSync as readFileSync23 } from "node:fs";
27254
- import { basename as basename6 } from "node:path";
26875
+ import { readFileSync as readFileSync22 } from "node:fs";
26876
+ import { basename as basename5 } from "node:path";
27255
26877
  import { randomUUID as randomUUID7 } from "node:crypto";
27256
26878
  function tlsRejectUnauthorized() {
27257
26879
  return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
27258
26880
  }
27259
26881
  function readResponse(socket) {
27260
- return new Promise((resolve21, reject) => {
26882
+ return new Promise((resolve20, reject) => {
27261
26883
  let buffer = "";
27262
26884
  const onData = (chunk) => {
27263
26885
  buffer += chunk.toString("utf-8");
@@ -27269,7 +26891,7 @@ function readResponse(socket) {
27269
26891
  if (line.length >= 4 && line[3] === " ") {
27270
26892
  socket.removeListener("data", onData);
27271
26893
  socket.removeListener("error", onError);
27272
- resolve21({ code, text: buffer.trim() });
26894
+ resolve20({ code, text: buffer.trim() });
27273
26895
  return;
27274
26896
  }
27275
26897
  }
@@ -27283,10 +26905,10 @@ function readResponse(socket) {
27283
26905
  });
27284
26906
  }
27285
26907
  function sendCommand(socket, command) {
27286
- return new Promise((resolve21, reject) => {
26908
+ return new Promise((resolve20, reject) => {
27287
26909
  socket.write(command + "\r\n", "utf-8", (err) => {
27288
26910
  if (err) return reject(err);
27289
- readResponse(socket).then(resolve21, reject);
26911
+ readResponse(socket).then(resolve20, reject);
27290
26912
  });
27291
26913
  });
27292
26914
  }
@@ -27342,8 +26964,8 @@ function buildMimeMessage(options) {
27342
26964
  lines.push(options.body);
27343
26965
  }
27344
26966
  for (const filePath of options.attachments) {
27345
- const fileName = basename6(filePath);
27346
- const fileData = readFileSync23(filePath);
26967
+ const fileName = basename5(filePath);
26968
+ const fileData = readFileSync22(filePath);
27347
26969
  const base64Data = fileData.toString("base64");
27348
26970
  lines.push("");
27349
26971
  lines.push(`--${boundary}`);
@@ -27386,7 +27008,7 @@ function imapQuote(s) {
27386
27008
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
27387
27009
  }
27388
27010
  function imapReadLine(socket) {
27389
- return new Promise((resolve21, reject) => {
27011
+ return new Promise((resolve20, reject) => {
27390
27012
  let buffer = "";
27391
27013
  const onData = (chunk) => {
27392
27014
  buffer += chunk.toString("utf-8");
@@ -27394,7 +27016,7 @@ function imapReadLine(socket) {
27394
27016
  if (nlIndex !== -1) {
27395
27017
  socket.removeListener("data", onData);
27396
27018
  socket.removeListener("error", onError);
27397
- resolve21(buffer);
27019
+ resolve20(buffer);
27398
27020
  }
27399
27021
  };
27400
27022
  const onError = (err) => {
@@ -27406,7 +27028,7 @@ function imapReadLine(socket) {
27406
27028
  });
27407
27029
  }
27408
27030
  function imapCommand(socket, command) {
27409
- return new Promise((resolve21, reject) => {
27031
+ return new Promise((resolve20, reject) => {
27410
27032
  imapTagCounter++;
27411
27033
  const tag = `T${imapTagCounter}`;
27412
27034
  const fullCommand = `${tag} ${command}\r
@@ -27417,7 +27039,7 @@ function imapCommand(socket, command) {
27417
27039
  if (buffer.includes(`${tag} OK`)) {
27418
27040
  socket.removeListener("data", onData);
27419
27041
  socket.removeListener("error", onError);
27420
- resolve21(buffer);
27042
+ resolve20(buffer);
27421
27043
  return;
27422
27044
  }
27423
27045
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -27446,91 +27068,149 @@ function parseSearchResponse(response) {
27446
27068
  if (!match) return [];
27447
27069
  return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
27448
27070
  }
27449
- function parseHeaderResponse(uid, response) {
27450
- const headers = {};
27451
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
27452
- if (headerBlock) {
27453
- const lines = headerBlock[1].split(/\r\n/);
27454
- let currentKey = "";
27455
- for (const line of lines) {
27456
- if (/^\s/.test(line) && currentKey) {
27457
- headers[currentKey] += " " + line.trim();
27458
- } else {
27459
- const colonIdx = line.indexOf(":");
27460
- if (colonIdx > 0) {
27461
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
27462
- headers[currentKey] = line.substring(colonIdx + 1).trim();
27463
- }
27464
- }
27465
- }
27466
- }
27467
- const seen = /\\Seen/i.test(response);
27468
- return {
27469
- uid,
27470
- subject: headers["subject"] ?? "",
27471
- from: headers["from"] ?? "",
27472
- to: headers["to"] ?? "",
27473
- date: headers["date"] ?? "",
27474
- snippet: "",
27475
- seen
27476
- };
27071
+ function extractRawMessage(response) {
27072
+ const m = response.match(/\{(\d+)\}\r\n/);
27073
+ if (!m) return response;
27074
+ const start2 = (m.index ?? 0) + m[0].length;
27075
+ return response.slice(start2, start2 + parseInt(m[1], 10));
27477
27076
  }
27478
- function parseFullMessage(uid, response) {
27479
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
27480
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
27481
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
27482
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
27483
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
27077
+ function parseMimeHeaders(section) {
27484
27078
  const headers = {};
27485
- const headerLines = headerSection.split(/\r\n/);
27486
27079
  let currentKey = "";
27487
- for (const line of headerLines) {
27080
+ for (const line of section.split(/\r\n/)) {
27488
27081
  if (/^\s/.test(line) && currentKey) {
27489
27082
  headers[currentKey] += " " + line.trim();
27490
27083
  } else {
27491
- const colonIdx = line.indexOf(":");
27492
- if (colonIdx > 0) {
27493
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
27494
- headers[currentKey] = line.substring(colonIdx + 1).trim();
27084
+ const idx = line.indexOf(":");
27085
+ if (idx > 0) {
27086
+ currentKey = line.substring(0, idx).trim().toLowerCase();
27087
+ headers[currentKey] = line.substring(idx + 1).trim();
27088
+ }
27089
+ }
27090
+ }
27091
+ return headers;
27092
+ }
27093
+ function decodeTransfer(body, encoding) {
27094
+ const enc = encoding.toLowerCase().trim();
27095
+ if (enc === "base64") {
27096
+ try {
27097
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf-8");
27098
+ } catch {
27099
+ return body;
27100
+ }
27101
+ }
27102
+ if (enc === "quoted-printable") {
27103
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
27104
+ }
27105
+ return body;
27106
+ }
27107
+ function decodeAttachmentBytes(body, encoding) {
27108
+ const enc = encoding.toLowerCase().trim();
27109
+ if (enc === "base64") {
27110
+ return Buffer.from(body.replace(/\s+/g, ""), "base64");
27111
+ }
27112
+ const trimmed = body.replace(/\r\n$/, "");
27113
+ if (enc === "quoted-printable") {
27114
+ const collapsed = trimmed.replace(/=\r?\n/g, "");
27115
+ const bytes = [];
27116
+ for (let i = 0; i < collapsed.length; i++) {
27117
+ const hex = collapsed.substring(i + 1, i + 3);
27118
+ if (collapsed[i] === "=" && /^[0-9A-Fa-f]{2}$/.test(hex)) {
27119
+ bytes.push(parseInt(hex, 16));
27120
+ i += 2;
27121
+ } else {
27122
+ bytes.push(collapsed.charCodeAt(i) & 255);
27495
27123
  }
27496
27124
  }
27125
+ return Buffer.from(bytes);
27497
27126
  }
27127
+ return Buffer.from(trimmed, "utf-8");
27128
+ }
27129
+ function attachmentFilename(disposition, contentType) {
27130
+ const d = disposition.match(/filename="?([^";\r\n]+)"?/i);
27131
+ if (d) return d[1].trim();
27132
+ const c = contentType.match(/name="?([^";\r\n]+)"?/i);
27133
+ if (c) return c[1].trim();
27134
+ return "attachment";
27135
+ }
27136
+ function makeSnippet(bodyText, bodyHtml) {
27137
+ return (bodyText || bodyHtml || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
27138
+ }
27139
+ function toIsoDate(raw) {
27140
+ if (!raw) return "";
27141
+ const d = new Date(raw);
27142
+ return Number.isNaN(d.getTime()) ? raw : d.toISOString();
27143
+ }
27144
+ function parseMessage(response) {
27145
+ const raw = extractRawMessage(response);
27146
+ const headerEnd = raw.indexOf("\r\n\r\n");
27147
+ const headerSection = headerEnd >= 0 ? raw.substring(0, headerEnd) : raw;
27148
+ const bodySection = headerEnd >= 0 ? raw.substring(headerEnd + 4) : "";
27149
+ const headers = parseMimeHeaders(headerSection);
27498
27150
  const contentType = headers["content-type"] ?? "text/plain";
27499
27151
  let bodyText = "";
27500
27152
  let bodyHtml = "";
27153
+ const attachments = [];
27501
27154
  if (contentType.includes("multipart")) {
27502
27155
  const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
27503
27156
  if (boundaryMatch) {
27504
- const boundary = boundaryMatch[1];
27505
- const parts = bodySection.split("--" + boundary);
27506
- for (const part of parts) {
27507
- if (part.trim() === "" || part.trim() === "--") continue;
27508
- const partHeaderEnd = part.indexOf("\r\n\r\n");
27509
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
27510
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
27511
- if (partHeaders.includes("text/html")) {
27512
- bodyHtml = partBody;
27513
- } else if (partHeaders.includes("text/plain")) {
27514
- bodyText = partBody;
27157
+ const boundary = "--" + boundaryMatch[1];
27158
+ for (const part of bodySection.split(boundary)) {
27159
+ const trimmed = part.trim();
27160
+ if (trimmed === "" || trimmed === "--") continue;
27161
+ const pEnd = part.indexOf("\r\n\r\n");
27162
+ if (pEnd < 0) continue;
27163
+ const pHeaders = parseMimeHeaders(part.substring(0, pEnd));
27164
+ const pBody = part.substring(pEnd + 4);
27165
+ const cte = pHeaders["content-transfer-encoding"] ?? "";
27166
+ const pType = pHeaders["content-type"] ?? "text/plain";
27167
+ const disposition = pHeaders["content-disposition"] ?? "";
27168
+ if (/attachment/i.test(disposition)) {
27169
+ const content = decodeAttachmentBytes(pBody, cte);
27170
+ attachments.push({
27171
+ filename: attachmentFilename(disposition, pType),
27172
+ contentType: pType.split(";")[0].trim(),
27173
+ size: content.length,
27174
+ content
27175
+ });
27176
+ } else if (pType.includes("text/html")) {
27177
+ bodyHtml = decodeTransfer(pBody, cte).trim();
27178
+ } else if (pType.includes("text/plain")) {
27179
+ bodyText = decodeTransfer(pBody, cte).trim();
27515
27180
  }
27516
27181
  }
27517
27182
  }
27518
27183
  } else if (contentType.includes("text/html")) {
27519
- bodyHtml = bodySection;
27184
+ bodyHtml = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
27520
27185
  } else {
27521
- bodyText = bodySection;
27186
+ bodyText = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
27522
27187
  }
27523
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
27524
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
27188
+ return { headers, bodyText, bodyHtml, attachments };
27189
+ }
27190
+ function parseSummary(uid, response) {
27191
+ const { headers, bodyText, bodyHtml } = parseMessage(response);
27192
+ return {
27193
+ uid,
27194
+ subject: headers["subject"] ?? "",
27195
+ from: headers["from"] ?? "",
27196
+ to: headers["to"] ?? "",
27197
+ date: toIsoDate(headers["date"] ?? ""),
27198
+ snippet: makeSnippet(bodyText, bodyHtml),
27199
+ seen: /\\Seen/i.test(response)
27200
+ };
27201
+ }
27202
+ function parseFullMessage(uid, response) {
27203
+ const { headers, bodyText, bodyHtml, attachments } = parseMessage(response);
27525
27204
  return {
27526
27205
  uid,
27527
27206
  subject: headers["subject"] ?? "",
27528
27207
  from: headers["from"] ?? "",
27529
27208
  to: headers["to"] ?? "",
27530
27209
  cc: headers["cc"] ?? "",
27531
- date: headers["date"] ?? "",
27210
+ date: toIsoDate(headers["date"] ?? ""),
27532
27211
  bodyText,
27533
27212
  bodyHtml,
27213
+ attachments,
27534
27214
  headers
27535
27215
  };
27536
27216
  }
@@ -27646,89 +27326,89 @@ var init_messenger = __esm({
27646
27326
  }
27647
27327
  const messageId = `${randomUUID7()}@${this.host}`;
27648
27328
  if (allRecipients.length === 0) {
27649
- return { success: false, message: "No recipients specified" };
27329
+ return { success: false, message: "No recipients specified", id: null };
27650
27330
  }
27651
27331
  if (!this.fromAddress) {
27652
- return { success: false, message: "No from address configured" };
27332
+ return { success: false, message: "No from address configured", id: null };
27653
27333
  }
27654
27334
  try {
27655
27335
  let socket;
27656
27336
  if (this.port === 465) {
27657
27337
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
27658
- await new Promise((resolve21, reject) => {
27659
- socket.once("secureConnect", resolve21);
27338
+ await new Promise((resolve20, reject) => {
27339
+ socket.once("secureConnect", resolve20);
27660
27340
  socket.once("error", reject);
27661
27341
  });
27662
27342
  } else {
27663
27343
  socket = net3.createConnection({ host: this.host, port: this.port });
27664
- await new Promise((resolve21, reject) => {
27665
- socket.once("connect", resolve21);
27344
+ await new Promise((resolve20, reject) => {
27345
+ socket.once("connect", resolve20);
27666
27346
  socket.once("error", reject);
27667
27347
  });
27668
27348
  }
27669
27349
  const greeting = await readResponse(socket);
27670
27350
  if (greeting.code !== 220) {
27671
27351
  socket.destroy();
27672
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
27352
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}`, id: null };
27673
27353
  }
27674
27354
  const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
27675
27355
  if (ehlo.code !== 250) {
27676
27356
  socket.destroy();
27677
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
27357
+ return { success: false, message: `EHLO failed: ${ehlo.text}`, id: null };
27678
27358
  }
27679
27359
  if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
27680
27360
  const starttls = await sendCommand(socket, "STARTTLS");
27681
27361
  if (starttls.code !== 220) {
27682
27362
  socket.destroy();
27683
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
27363
+ return { success: false, message: `STARTTLS failed: ${starttls.text}`, id: null };
27684
27364
  }
27685
27365
  const plainSocket = socket;
27686
27366
  socket = tls2.connect(
27687
27367
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
27688
27368
  );
27689
- await new Promise((resolve21, reject) => {
27690
- socket.once("secureConnect", resolve21);
27369
+ await new Promise((resolve20, reject) => {
27370
+ socket.once("secureConnect", resolve20);
27691
27371
  socket.once("error", reject);
27692
27372
  });
27693
27373
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
27694
27374
  if (ehlo2.code !== 250) {
27695
27375
  socket.destroy();
27696
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
27376
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}`, id: null };
27697
27377
  }
27698
27378
  }
27699
27379
  if (this.username && this.password) {
27700
27380
  const auth = await sendCommand(socket, "AUTH LOGIN");
27701
27381
  if (auth.code !== 334) {
27702
27382
  socket.destroy();
27703
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
27383
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}`, id: null };
27704
27384
  }
27705
27385
  const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
27706
27386
  if (userResp.code !== 334) {
27707
27387
  socket.destroy();
27708
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
27388
+ return { success: false, message: `AUTH username failed: ${userResp.text}`, id: null };
27709
27389
  }
27710
27390
  const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
27711
27391
  if (passResp.code !== 235) {
27712
27392
  socket.destroy();
27713
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
27393
+ return { success: false, message: `AUTH password failed: ${passResp.text}`, id: null };
27714
27394
  }
27715
27395
  }
27716
27396
  const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
27717
27397
  if (mailFrom.code !== 250) {
27718
27398
  socket.destroy();
27719
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
27399
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}`, id: null };
27720
27400
  }
27721
27401
  for (const recipient of allRecipients) {
27722
27402
  const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
27723
27403
  if (rcpt.code !== 250 && rcpt.code !== 251) {
27724
27404
  socket.destroy();
27725
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
27405
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}`, id: null };
27726
27406
  }
27727
27407
  }
27728
27408
  const dataCmd = await sendCommand(socket, "DATA");
27729
27409
  if (dataCmd.code !== 354) {
27730
27410
  socket.destroy();
27731
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
27411
+ return { success: false, message: `DATA failed: ${dataCmd.text}`, id: null };
27732
27412
  }
27733
27413
  const mimeMessage = buildMimeMessage({
27734
27414
  from: this.fromAddress,
@@ -27747,15 +27427,30 @@ var init_messenger = __esm({
27747
27427
  const endData = await sendCommand(socket, mimeMessage + "\r\n.");
27748
27428
  if (endData.code !== 250) {
27749
27429
  socket.destroy();
27750
- return { success: false, message: `Message delivery failed: ${endData.text}` };
27430
+ return { success: false, message: `Message delivery failed: ${endData.text}`, id: null };
27751
27431
  }
27752
27432
  await sendCommand(socket, "QUIT");
27753
27433
  socket.destroy();
27754
27434
  return { success: true, message: "Email sent successfully", id: messageId };
27755
27435
  } catch (err) {
27756
27436
  const errMsg = err instanceof Error ? err.message : String(err);
27757
- return { success: false, message: `SMTP error: ${errMsg}` };
27437
+ return { success: false, message: `SMTP error: ${errMsg}`, id: null };
27438
+ }
27439
+ }
27440
+ /**
27441
+ * Render a Frond template STRING and send it as an HTML email (G7, parity with
27442
+ * Python's send_template). Extra send() options (cc, bcc, replyTo, attachments,
27443
+ * headers) pass through. If the Frond package cannot be loaded the raw template
27444
+ * is sent verbatim (matches Python's ImportError fallback) rather than failing.
27445
+ */
27446
+ async sendTemplate(to, subject, template, data = {}, cc, bcc, replyTo, attachments, headers) {
27447
+ let body = template;
27448
+ try {
27449
+ const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
27450
+ body = new Frond2().renderString(template, data);
27451
+ } catch {
27758
27452
  }
27453
+ return this.send(to, subject, body, true, void 0, cc, bcc, replyTo, attachments, headers);
27759
27454
  }
27760
27455
  /**
27761
27456
  * Test the SMTP connection without sending an email.
@@ -27765,14 +27460,14 @@ var init_messenger = __esm({
27765
27460
  let socket;
27766
27461
  if (this.port === 465) {
27767
27462
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
27768
- await new Promise((resolve21, reject) => {
27769
- socket.once("secureConnect", resolve21);
27463
+ await new Promise((resolve20, reject) => {
27464
+ socket.once("secureConnect", resolve20);
27770
27465
  socket.once("error", reject);
27771
27466
  });
27772
27467
  } else {
27773
27468
  socket = net3.createConnection({ host: this.host, port: this.port });
27774
- await new Promise((resolve21, reject) => {
27775
- socket.once("connect", resolve21);
27469
+ await new Promise((resolve20, reject) => {
27470
+ socket.once("connect", resolve20);
27776
27471
  socket.once("error", reject);
27777
27472
  });
27778
27473
  }
@@ -27807,14 +27502,14 @@ var init_messenger = __esm({
27807
27502
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
27808
27503
  if (useTls) {
27809
27504
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
27810
- await new Promise((resolve21, reject) => {
27811
- socket.once("secureConnect", resolve21);
27505
+ await new Promise((resolve20, reject) => {
27506
+ socket.once("secureConnect", resolve20);
27812
27507
  socket.once("error", reject);
27813
27508
  });
27814
27509
  } else {
27815
27510
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
27816
- await new Promise((resolve21, reject) => {
27817
- socket.once("connect", resolve21);
27511
+ await new Promise((resolve20, reject) => {
27512
+ socket.once("connect", resolve20);
27818
27513
  socket.once("error", reject);
27819
27514
  });
27820
27515
  }
@@ -27851,7 +27546,7 @@ var init_messenger = __esm({
27851
27546
  }
27852
27547
  try {
27853
27548
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27854
- const searchResp = await imapCommand(socket, "SEARCH ALL");
27549
+ const searchResp = await imapCommand(socket, "UID SEARCH ALL");
27855
27550
  const uids = parseSearchResponse(searchResp);
27856
27551
  if (uids.length === 0) return [];
27857
27552
  uids.reverse();
@@ -27859,8 +27554,8 @@ var init_messenger = __esm({
27859
27554
  if (selected.length === 0) return [];
27860
27555
  const messages = [];
27861
27556
  for (const uid of selected) {
27862
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
27863
- messages.push(parseHeaderResponse(uid, fetchResp));
27557
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
27558
+ messages.push(parseSummary(uid, fetchResp));
27864
27559
  }
27865
27560
  return messages;
27866
27561
  } catch (err) {
@@ -27870,7 +27565,7 @@ var init_messenger = __esm({
27870
27565
  }
27871
27566
  }
27872
27567
  /**
27873
- * Read a single message by sequence number or UID.
27568
+ * Read a single message by its IMAP UID.
27874
27569
  */
27875
27570
  async read(uid, folder = "INBOX") {
27876
27571
  let socket;
@@ -27881,11 +27576,11 @@ var init_messenger = __esm({
27881
27576
  }
27882
27577
  try {
27883
27578
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27884
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
27579
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY[])`);
27885
27580
  if (!/\{\d+\}/.test(fetchResp)) {
27886
27581
  return null;
27887
27582
  }
27888
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
27583
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
27889
27584
  return parseFullMessage(uid, fetchResp);
27890
27585
  } catch (err) {
27891
27586
  throw imapFail("read", err);
@@ -27912,14 +27607,14 @@ var init_messenger = __esm({
27912
27607
  }
27913
27608
  try {
27914
27609
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27915
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
27610
+ const searchResp = await imapCommand(socket, `UID SEARCH ${query}`);
27916
27611
  const uids = parseSearchResponse(searchResp);
27917
27612
  if (uids.length === 0) return [];
27918
27613
  uids.reverse();
27919
27614
  const messages = [];
27920
27615
  for (const uid of uids.slice(0, limit)) {
27921
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
27922
- messages.push(parseHeaderResponse(uid, fetchResp));
27616
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
27617
+ messages.push(parseSummary(uid, fetchResp));
27923
27618
  }
27924
27619
  return messages;
27925
27620
  } catch (err) {
@@ -27929,26 +27624,46 @@ var init_messenger = __esm({
27929
27624
  }
27930
27625
  }
27931
27626
  /**
27932
- * Delete a message by UID.
27627
+ * Delete a message by UID (mark \Deleted, then EXPUNGE).
27628
+ *
27629
+ * `delete` is the one cross-framework name (python/php/ruby/node all spell it
27630
+ * `delete`). `deleteMessage` remains as a DEPRECATED alias for one release.
27933
27631
  */
27934
- async deleteMessage(uid, folder = "INBOX") {
27632
+ async delete(uid, folder = "INBOX") {
27935
27633
  const socket = await this.imapConnect();
27936
27634
  try {
27937
27635
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27938
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
27636
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Deleted)`);
27939
27637
  await imapCommand(socket, "EXPUNGE");
27940
27638
  } finally {
27941
27639
  await this.imapDisconnect(socket);
27942
27640
  }
27943
27641
  }
27642
+ /** @deprecated Use {@link delete} — kept as an alias for one release (G7). */
27643
+ async deleteMessage(uid, folder = "INBOX") {
27644
+ return this.delete(uid, folder);
27645
+ }
27944
27646
  /**
27945
- * Mark a message as read.
27647
+ * Mark a message as read (+FLAGS \Seen).
27946
27648
  */
27947
27649
  async markRead(uid, folder = "INBOX") {
27948
27650
  const socket = await this.imapConnect();
27949
27651
  try {
27950
27652
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27951
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
27653
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
27654
+ } finally {
27655
+ await this.imapDisconnect(socket);
27656
+ }
27657
+ }
27658
+ /**
27659
+ * Mark a message as unread (-FLAGS \Seen) — the inverse of markRead (G7,
27660
+ * parity with Python's mark_unread).
27661
+ */
27662
+ async markUnread(uid, folder = "INBOX") {
27663
+ const socket = await this.imapConnect();
27664
+ try {
27665
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27666
+ await imapCommand(socket, `UID STORE ${uid} -FLAGS (\\Seen)`);
27952
27667
  } finally {
27953
27668
  await this.imapDisconnect(socket);
27954
27669
  }
@@ -27965,7 +27680,7 @@ var init_messenger = __esm({
27965
27680
  }
27966
27681
  try {
27967
27682
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27968
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
27683
+ const searchResp = await imapCommand(socket, "UID SEARCH UNSEEN");
27969
27684
  return parseSearchResponse(searchResp).length;
27970
27685
  } catch (err) {
27971
27686
  throw imapFail("unread", err);
@@ -28704,17 +28419,17 @@ var init_htmlElement = __esm({
28704
28419
  });
28705
28420
 
28706
28421
  // ../core/src/ai.ts
28707
- import { existsSync as existsSync25, mkdirSync as mkdirSync15, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
28422
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, writeFileSync as writeFileSync14, readFileSync as readFileSync23 } from "node:fs";
28708
28423
  import { homedir } from "node:os";
28709
- import { join as join25, resolve as resolve17, relative as relative10, dirname as dirname11 } from "node:path";
28424
+ import { join as join24, resolve as resolve16, relative as relative10, dirname as dirname10 } from "node:path";
28710
28425
  import { fileURLToPath as fileURLToPath6 } from "node:url";
28711
28426
  import { execSync, execFileSync as execFileSync3 } from "node:child_process";
28712
28427
  import { createInterface } from "node:readline";
28713
28428
  function readVersion() {
28714
28429
  try {
28715
- const thisDir = dirname11(fileURLToPath6(import.meta.url));
28716
- const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
28717
- const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
28430
+ const thisDir = dirname10(fileURLToPath6(import.meta.url));
28431
+ const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
28432
+ const pkg = JSON.parse(readFileSync23(rootPkg, "utf-8"));
28718
28433
  return pkg.version ?? "0.0.0";
28719
28434
  } catch {
28720
28435
  return "0.0.0";
@@ -28763,8 +28478,8 @@ function downloadSkillsSync(jobs) {
28763
28478
  function installSkills(root = ".", targets) {
28764
28479
  const ref = skillsRef();
28765
28480
  const dests = targets ?? [
28766
- join25(resolve17(root), ".claude", "skills"),
28767
- join25(homedir(), ".claude", "skills")
28481
+ join24(resolve16(root), ".claude", "skills"),
28482
+ join24(homedir(), ".claude", "skills")
28768
28483
  ];
28769
28484
  const jobs = [];
28770
28485
  const index = /* @__PURE__ */ new Map();
@@ -28782,9 +28497,9 @@ function installSkills(root = ".", targets) {
28782
28497
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
28783
28498
  skillMdUrl[skill] = `${base}/SKILL.md`;
28784
28499
  for (const dest of dests) {
28785
- add(`${base}/SKILL.md`, join25(dest, skill, "SKILL.md"));
28500
+ add(`${base}/SKILL.md`, join24(dest, skill, "SKILL.md"));
28786
28501
  for (const r of spec.references) {
28787
- add(`${base}/references/${r}`, join25(dest, skill, "references", r));
28502
+ add(`${base}/references/${r}`, join24(dest, skill, "references", r));
28788
28503
  }
28789
28504
  }
28790
28505
  }
@@ -28796,10 +28511,10 @@ function installSkills(root = ".", targets) {
28796
28511
  return installed;
28797
28512
  }
28798
28513
  function isInstalled(root, tool) {
28799
- return existsSync25(join25(resolve17(root), tool.contextFile));
28514
+ return existsSync24(join24(resolve16(root), tool.contextFile));
28800
28515
  }
28801
28516
  function showMenu(root = ".") {
28802
- const r = resolve17(root);
28517
+ const r = resolve16(root);
28803
28518
  console.log("\n Tina4 AI Context Installer\n");
28804
28519
  for (let i = 0; i < AI_TOOLS.length; i++) {
28805
28520
  const tool = AI_TOOLS[i];
@@ -28817,16 +28532,16 @@ function showMenu(root = ".") {
28817
28532
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
28818
28533
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
28819
28534
  console.log();
28820
- return new Promise((resolve21) => {
28535
+ return new Promise((resolve20) => {
28821
28536
  const rl = createInterface({ input: process.stdin, output: process.stdout });
28822
28537
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
28823
28538
  rl.close();
28824
- resolve21(answer.trim());
28539
+ resolve20(answer.trim());
28825
28540
  });
28826
28541
  });
28827
28542
  }
28828
28543
  function installSelected(root, selection) {
28829
- const rootPath = resolve17(root);
28544
+ const rootPath = resolve16(root);
28830
28545
  const created = [];
28831
28546
  let indices;
28832
28547
  let doInstallTina4Ai = false;
@@ -28919,33 +28634,33 @@ function looksLikeOldFrameworkInstall(existing) {
28919
28634
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
28920
28635
  const block = skillBlock(contextFile);
28921
28636
  const [start2, end] = markersFor(contextFile);
28922
- if (!existsSync25(contextPath)) {
28923
- writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28637
+ if (!existsSync24(contextPath)) {
28638
+ writeFileSync14(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28924
28639
  return "Installed";
28925
28640
  }
28926
- const existing = readFileSync24(contextPath, "utf-8");
28641
+ const existing = readFileSync23(contextPath, "utf-8");
28927
28642
  if (hasMarkers(existing, start2, end)) {
28928
- writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
28643
+ writeFileSync14(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
28929
28644
  return "Refreshed skill block in";
28930
28645
  }
28931
28646
  if (looksLikeOldFrameworkInstall(existing)) {
28932
28647
  const head = existing.replace(/^\s+/, "");
28933
28648
  const preamble = existing.slice(0, existing.length - head.length);
28934
28649
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
28935
- writeFileSync15(contextPath, newContent, "utf-8");
28650
+ writeFileSync14(contextPath, newContent, "utf-8");
28936
28651
  return "Migrated (replaced old framework dump in)";
28937
28652
  }
28938
- writeFileSync15(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28653
+ writeFileSync14(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28939
28654
  return "Appended skill block to";
28940
28655
  }
28941
28656
  function installForTool(root, tool, context) {
28942
28657
  const created = [];
28943
- const contextPath = join25(root, tool.contextFile);
28658
+ const contextPath = join24(root, tool.contextFile);
28944
28659
  if (tool.configDir) {
28945
- mkdirSync15(join25(root, tool.configDir), { recursive: true });
28660
+ mkdirSync14(join24(root, tool.configDir), { recursive: true });
28946
28661
  }
28947
- const parentDir = dirname11(contextPath);
28948
- mkdirSync15(parentDir, { recursive: true });
28662
+ const parentDir = dirname10(contextPath);
28663
+ mkdirSync14(parentDir, { recursive: true });
28949
28664
  const action = writeOrMerge(contextPath, tool.contextFile, context);
28950
28665
  const rel = relative10(root, contextPath);
28951
28666
  created.push(rel);
@@ -28980,7 +28695,7 @@ function installTina4Ai() {
28980
28695
  function installClaudeSkills(root) {
28981
28696
  const created = [];
28982
28697
  for (const skill of installSkills(root)) {
28983
- created.push(join25(".claude", "skills", skill));
28698
+ created.push(join24(".claude", "skills", skill));
28984
28699
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
28985
28700
  }
28986
28701
  return created;
@@ -29321,11 +29036,11 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
29321
29036
  }
29322
29037
  function generateClaudeCodeContext() {
29323
29038
  try {
29324
- const thisDir = dirname11(fileURLToPath6(import.meta.url));
29325
- const repoRoot = resolve17(thisDir, "..", "..", "..");
29326
- const claudeMdPath = join25(repoRoot, "CLAUDE.md");
29327
- if (existsSync25(claudeMdPath)) {
29328
- return readFileSync24(claudeMdPath, "utf-8");
29039
+ const thisDir = dirname10(fileURLToPath6(import.meta.url));
29040
+ const repoRoot = resolve16(thisDir, "..", "..", "..");
29041
+ const claudeMdPath = join24(repoRoot, "CLAUDE.md");
29042
+ if (existsSync24(claudeMdPath)) {
29043
+ return readFileSync23(claudeMdPath, "utf-8");
29329
29044
  }
29330
29045
  } catch {
29331
29046
  }
@@ -31317,7 +31032,6 @@ __export(src_exports2, {
31317
31032
  RouteRef: () => RouteRef,
31318
31033
  Router: () => Router,
31319
31034
  SafeString: () => SafeString2,
31320
- ScssCompiler: () => ScssCompiler,
31321
31035
  SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
31322
31036
  ServiceRunner: () => ServiceRunner,
31323
31037
  Session: () => Session,
@@ -31514,7 +31228,6 @@ var init_src2 = __esm({
31514
31228
  init_session();
31515
31229
  init_i18n();
31516
31230
  init_fakeData();
31517
- init_scss();
31518
31231
  init_queue();
31519
31232
  init_job();
31520
31233
  init_mqtt();
@@ -32073,8 +31786,8 @@ __export(sqlite_exports, {
32073
31786
  SQLiteAdapter: () => SQLiteAdapter
32074
31787
  });
32075
31788
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32076
- import { mkdirSync as mkdirSync16 } from "node:fs";
32077
- import { dirname as dirname12, isAbsolute as isAbsolute4, join as join26, resolve as resolve18 } from "node:path";
31789
+ import { mkdirSync as mkdirSync15 } from "node:fs";
31790
+ import { dirname as dirname11, isAbsolute as isAbsolute4, join as join25, resolve as resolve17 } from "node:path";
32078
31791
  function isIdentifier(str) {
32079
31792
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
32080
31793
  }
@@ -32107,13 +31820,13 @@ function resolveSqlitePath(dbPath) {
32107
31820
  if (dbPath === ":memory:") return dbPath;
32108
31821
  let path8 = dbPath;
32109
31822
  if (!isAbsolute4(path8)) {
32110
- path8 = join26(process.cwd(), path8);
32111
- mkdirSync16(dirname12(path8), { recursive: true });
31823
+ path8 = join25(process.cwd(), path8);
31824
+ mkdirSync15(dirname11(path8), { recursive: true });
32112
31825
  } else {
32113
- const cwd = resolve18(process.cwd());
32114
- const abs = resolve18(path8);
31826
+ const cwd = resolve17(process.cwd());
31827
+ const abs = resolve17(path8);
32115
31828
  if (abs.startsWith(cwd + "/") || abs === cwd) {
32116
- mkdirSync16(dirname12(abs), { recursive: true });
31829
+ mkdirSync15(dirname11(abs), { recursive: true });
32117
31830
  }
32118
31831
  }
32119
31832
  return path8;
@@ -32461,7 +32174,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
32461
32174
  const elapsedMs = () => performance.now() - startedAt;
32462
32175
  if (budgetMs === null) return attempt();
32463
32176
  const started = attempt();
32464
- return new Promise((resolve21, reject) => {
32177
+ return new Promise((resolve20, reject) => {
32465
32178
  let expired = false;
32466
32179
  const timer = setTimeout(() => {
32467
32180
  expired = true;
@@ -32471,7 +32184,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
32471
32184
  (arrived) => {
32472
32185
  clearTimeout(timer);
32473
32186
  if (expired) abandon?.(arrived);
32474
- else resolve21(arrived);
32187
+ else resolve20(arrived);
32475
32188
  },
32476
32189
  (failure) => {
32477
32190
  clearTimeout(timer);
@@ -33028,10 +32741,10 @@ var init_mysql = __esm({
33028
32741
  ...timeoutOption
33029
32742
  });
33030
32743
  }
33031
- return new Promise((resolve21, reject) => {
32744
+ return new Promise((resolve20, reject) => {
33032
32745
  this.connection.connect((err) => {
33033
32746
  if (err) reject(err);
33034
- else resolve21();
32747
+ else resolve20();
33035
32748
  });
33036
32749
  });
33037
32750
  },
@@ -33053,10 +32766,10 @@ var init_mysql = __esm({
33053
32766
  }
33054
32767
  }
33055
32768
  queryPromise(sql, params) {
33056
- return new Promise((resolve21, reject) => {
32769
+ return new Promise((resolve20, reject) => {
33057
32770
  this.connection.query(sql, params ?? [], (err, results) => {
33058
32771
  if (err) reject(err);
33059
- else resolve21(results);
32772
+ else resolve20(results);
33060
32773
  });
33061
32774
  });
33062
32775
  }
@@ -33426,11 +33139,11 @@ var init_mssql = __esm({
33426
33139
  };
33427
33140
  }
33428
33141
  await withConnectTimeout(
33429
- () => new Promise((resolve21, reject) => {
33142
+ () => new Promise((resolve20, reject) => {
33430
33143
  this.connection = new Connection(tediousConfig);
33431
33144
  this.connection.on("connect", (err) => {
33432
33145
  if (err) reject(err);
33433
- else resolve21();
33146
+ else resolve20();
33434
33147
  });
33435
33148
  this.connection.connect();
33436
33149
  }),
@@ -33475,11 +33188,11 @@ var init_mssql = __esm({
33475
33188
  const tediousModule = requireTedious();
33476
33189
  const Request = tediousModule.Request;
33477
33190
  const TYPES = tediousModule.TYPES;
33478
- return new Promise((resolve21, reject) => {
33191
+ return new Promise((resolve20, reject) => {
33479
33192
  const rows = [];
33480
33193
  const request = new Request(sql, (err, rowCount) => {
33481
33194
  if (err) reject(err);
33482
- else resolve21({ rows, rowCount });
33195
+ else resolve20({ rows, rowCount });
33483
33196
  });
33484
33197
  if (params) {
33485
33198
  params.forEach((p, i) => {
@@ -33682,8 +33395,8 @@ var init_mssql = __esm({
33682
33395
  throw new Error("Use startTransactionAsync() for MSSQL.");
33683
33396
  }
33684
33397
  async startTransactionAsync() {
33685
- await new Promise((resolve21, reject) => {
33686
- this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
33398
+ await new Promise((resolve20, reject) => {
33399
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve20());
33687
33400
  });
33688
33401
  this._inTransaction = true;
33689
33402
  }
@@ -33691,8 +33404,8 @@ var init_mssql = __esm({
33691
33404
  throw new Error("Use commitAsync() for MSSQL.");
33692
33405
  }
33693
33406
  async commitAsync() {
33694
- await new Promise((resolve21, reject) => {
33695
- this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
33407
+ await new Promise((resolve20, reject) => {
33408
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve20());
33696
33409
  });
33697
33410
  this._inTransaction = false;
33698
33411
  }
@@ -33700,8 +33413,8 @@ var init_mssql = __esm({
33700
33413
  throw new Error("Use rollbackAsync() for MSSQL.");
33701
33414
  }
33702
33415
  async rollbackAsync() {
33703
- await new Promise((resolve21, reject) => {
33704
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
33416
+ await new Promise((resolve20, reject) => {
33417
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve20());
33705
33418
  });
33706
33419
  this._inTransaction = false;
33707
33420
  }
@@ -33971,8 +33684,8 @@ var init_firebird = __esm({
33971
33684
  fbConfig.database = normalizeFirebirdDbIdentifier(fbConfig.database);
33972
33685
  }
33973
33686
  this.db = await withConnectTimeout(
33974
- () => new Promise((resolve21, reject) => {
33975
- fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve21(db));
33687
+ () => new Promise((resolve20, reject) => {
33688
+ fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve20(db));
33976
33689
  }),
33977
33690
  connectTimeoutMillis(),
33978
33691
  fbConfig.host,
@@ -34039,20 +33752,20 @@ var init_firebird = __esm({
34039
33752
  return this.transaction ?? this.db;
34040
33753
  }
34041
33754
  queryPromise(sql, params) {
34042
- return new Promise((resolve21, reject) => {
33755
+ return new Promise((resolve20, reject) => {
34043
33756
  const translated = this.translateSql(sql);
34044
33757
  this.statementHandle().query(translated, params ?? [], (err, result) => {
34045
33758
  if (err) reject(err);
34046
- else resolve21(result ?? []);
33759
+ else resolve20(result ?? []);
34047
33760
  });
34048
33761
  });
34049
33762
  }
34050
33763
  executePromise(sql, params) {
34051
- return new Promise((resolve21, reject) => {
33764
+ return new Promise((resolve20, reject) => {
34052
33765
  const translated = this.translateSql(sql);
34053
33766
  this.statementHandle().execute(translated, params ?? [], (err) => {
34054
33767
  if (err) reject(err);
34055
- else resolve21();
33768
+ else resolve20();
34056
33769
  });
34057
33770
  });
34058
33771
  }
@@ -34168,12 +33881,12 @@ var init_firebird = __esm({
34168
33881
  }
34169
33882
  async startTransactionAsync() {
34170
33883
  this.ensureConnected();
34171
- await new Promise((resolve21, reject) => {
33884
+ await new Promise((resolve20, reject) => {
34172
33885
  this.db.transaction(0, (err, transaction) => {
34173
33886
  if (err) reject(err);
34174
33887
  else {
34175
33888
  this.transaction = transaction;
34176
- resolve21();
33889
+ resolve20();
34177
33890
  }
34178
33891
  });
34179
33892
  });
@@ -34183,12 +33896,12 @@ var init_firebird = __esm({
34183
33896
  }
34184
33897
  async commitAsync() {
34185
33898
  if (!this.transaction) throw new Error("No active transaction to commit.");
34186
- await new Promise((resolve21, reject) => {
33899
+ await new Promise((resolve20, reject) => {
34187
33900
  this.transaction.commit((err) => {
34188
33901
  if (err) reject(err);
34189
33902
  else {
34190
33903
  this.transaction = null;
34191
- resolve21();
33904
+ resolve20();
34192
33905
  }
34193
33906
  });
34194
33907
  });
@@ -34198,12 +33911,12 @@ var init_firebird = __esm({
34198
33911
  }
34199
33912
  async rollbackAsync() {
34200
33913
  if (!this.transaction) throw new Error("No active transaction to rollback.");
34201
- await new Promise((resolve21, reject) => {
33914
+ await new Promise((resolve20, reject) => {
34202
33915
  this.transaction.rollback((err) => {
34203
33916
  if (err) reject(err);
34204
33917
  else {
34205
33918
  this.transaction = null;
34206
- resolve21();
33919
+ resolve20();
34207
33920
  }
34208
33921
  });
34209
33922
  });
@@ -35264,6 +34977,7 @@ __export(database_exports, {
35264
34977
  getNamedAdapter: () => getNamedAdapter,
35265
34978
  initDatabase: () => initDatabase,
35266
34979
  parseDatabaseUrl: () => parseDatabaseUrl,
34980
+ probeTotal: () => probeTotal,
35267
34981
  resetRequestCaches: () => resetRequestCaches2,
35268
34982
  resolveDbPool: () => resolveDbPool,
35269
34983
  setAdapter: () => setAdapter,
@@ -35317,6 +35031,29 @@ async function adapterCreateTable(adapter, name, columns) {
35317
35031
  if (adapter.createTableAsync) await adapter.createTableAsync(name, columns);
35318
35032
  else adapter.createTable(name, columns);
35319
35033
  }
35034
+ async function probeTotal(adapter, sql, params, limit) {
35035
+ if (limit === void 0 || limit <= 0) return void 0;
35036
+ try {
35037
+ const alias = adapter.countSubqueryAlias;
35038
+ const suffix = alias ? ` AS ${alias}` : "";
35039
+ const rows = await adapterFetch(
35040
+ adapter,
35041
+ `SELECT COUNT(*) AS tina4_total FROM (${sql}
35042
+ )${suffix}`,
35043
+ params,
35044
+ void 0,
35045
+ void 0,
35046
+ true
35047
+ );
35048
+ const row = Array.isArray(rows) ? rows[0] : void 0;
35049
+ if (!row) return void 0;
35050
+ const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
35051
+ const n = Number(value);
35052
+ return Number.isFinite(n) ? n : void 0;
35053
+ } catch {
35054
+ return void 0;
35055
+ }
35056
+ }
35320
35057
  function extractLastInsertId(result) {
35321
35058
  if (result && typeof result === "object") {
35322
35059
  const r = result;
@@ -35775,65 +35512,13 @@ var init_database = __esm({
35775
35512
  try {
35776
35513
  const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
35777
35514
  this.lastError = null;
35778
- const total = await this.countProbe(adapter, sql, params, limit);
35515
+ const total = await probeTotal(adapter, sql, params, limit);
35779
35516
  return new DatabaseResult(rows, void 0, total, limit, offset, adapter, sql);
35780
35517
  } catch (e) {
35781
35518
  this.lastError = e?.message ?? String(e);
35782
35519
  throw e;
35783
35520
  }
35784
35521
  }
35785
- /**
35786
- * The true row count for `sql`, ignoring the pagination we appended.
35787
- *
35788
- * `count` is the TRUE TOTAL for the filter, not the number of rows this page
35789
- * returned. Node and Ruby used to populate it with `records.length` while
35790
- * Python and PHP populated it from a probe, so `db.fetch(sql).count` answered
35791
- * 20 here and 250 there for one query against one table, and every paginated
35792
- * response built on it under-reported. MEASURED 2026-08-05 on a 250-row table
35793
- * read with limit=20: Node reported total 20 over 2 pages against Python's
35794
- * 250 over 13.
35795
- *
35796
- * Only probed when a limit was actually applied. With no limit the rows
35797
- * returned ARE the whole answer for this SQL, so `records.length` is already
35798
- * the true total and a second round-trip would buy nothing — which is also
35799
- * what keeps `fetchAll()` at one query.
35800
- *
35801
- * BEST EFFORT, and it can never mask a real failure: it runs AFTER the main
35802
- * query (which has already thrown on bad SQL) and returns undefined on any
35803
- * error. `undefined` — not 0 — is the miss value, so DatabaseResult falls
35804
- * back to records.length, a true lower bound. Reporting 0 next to 100 real
35805
- * records would be the same "states a wrong number authoritatively" defect
35806
- * this change exists to remove.
35807
- *
35808
- * The closing paren goes on its OWN LINE: appended inline, a trailing
35809
- * `-- comment` in the caller's SQL comments it out and the probe dies with
35810
- * "incomplete input". Postgres, MySQL and MSSQL additionally require a name
35811
- * for the derived table; SQLite and Firebird do not, and Firebird rejects
35812
- * `AS` there — so the alias comes from the adapter, not an assumption.
35813
- */
35814
- async countProbe(adapter, sql, params, limit) {
35815
- if (limit === void 0 || limit <= 0) return void 0;
35816
- try {
35817
- const alias = adapter.countSubqueryAlias;
35818
- const suffix = alias ? ` AS ${alias}` : "";
35819
- const rows = await adapterFetch(
35820
- adapter,
35821
- `SELECT COUNT(*) AS tina4_total FROM (${sql}
35822
- )${suffix}`,
35823
- params,
35824
- void 0,
35825
- void 0,
35826
- true
35827
- );
35828
- const row = Array.isArray(rows) ? rows[0] : void 0;
35829
- if (!row) return void 0;
35830
- const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
35831
- const n = Number(value);
35832
- return Number.isFinite(n) ? n : void 0;
35833
- } catch {
35834
- return void 0;
35835
- }
35836
- }
35837
35522
  /**
35838
35523
  * Fetch a single row or null.
35839
35524
  *
@@ -36519,18 +36204,18 @@ var init_database = __esm({
36519
36204
  });
36520
36205
 
36521
36206
  // src/model.ts
36522
- import { readdirSync as readdirSync18, statSync as statSync17 } from "node:fs";
36523
- import { join as join27, extname as extname8 } from "node:path";
36207
+ import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
36208
+ import { join as join26, extname as extname8 } from "node:path";
36524
36209
  async function discoverModels(modelsDir) {
36525
36210
  const models = [];
36526
36211
  let files;
36527
36212
  try {
36528
- files = readdirSync18(modelsDir);
36213
+ files = readdirSync17(modelsDir);
36529
36214
  } catch {
36530
36215
  return models;
36531
36216
  }
36532
36217
  for (const file of files) {
36533
- const filePath = join27(modelsDir, file);
36218
+ const filePath = join26(modelsDir, file);
36534
36219
  const stat = statSync17(filePath);
36535
36220
  if (!stat.isFile()) continue;
36536
36221
  const ext = extname8(file);
@@ -36545,6 +36230,10 @@ async function discoverModels(modelsDir) {
36545
36230
  }
36546
36231
  const definition = {
36547
36232
  tableName: ModelClass.tableName,
36233
+ // The class name is the type name a generated OpenAPI client wants
36234
+ // (`Item`, not `items`). Carry it so Swagger keys components.schemas by
36235
+ // it. A model exported as `default` keeps its declared class name here.
36236
+ className: typeof ModelClass.name === "string" && ModelClass.name ? ModelClass.name : void 0,
36548
36237
  fields: ModelClass.fields,
36549
36238
  fieldMapping: ModelClass.fieldMapping,
36550
36239
  softDelete: ModelClass.softDelete ?? false,
@@ -36568,8 +36257,8 @@ var init_model = __esm({
36568
36257
  });
36569
36258
 
36570
36259
  // src/migration.ts
36571
- import { existsSync as existsSync26, readdirSync as readdirSync19, readFileSync as readFileSync25, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16 } from "node:fs";
36572
- import { join as join28, resolve as resolve19 } from "node:path";
36260
+ import { existsSync as existsSync25, readdirSync as readdirSync18, readFileSync as readFileSync24, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15 } from "node:fs";
36261
+ import { join as join27, resolve as resolve18 } from "node:path";
36573
36262
  function unwrapAdapter(db) {
36574
36263
  let cur = db;
36575
36264
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -36877,16 +36566,16 @@ async function rollback(migrationsDir, delimiter2) {
36877
36566
  }
36878
36567
  return rolledBack2;
36879
36568
  }
36880
- const dir = resolve19(migrationsDir ?? "migrations");
36569
+ const dir = resolve18(migrationsDir ?? "migrations");
36881
36570
  const delim = delimiter2 ?? ";";
36882
36571
  const db = getAdapter();
36883
36572
  const migrations = await getLastBatchMigrations();
36884
36573
  const rolledBack = [];
36885
36574
  for (const migration of migrations) {
36886
36575
  const downFile = `${migration.migration_name}.down.sql`;
36887
- const downPath = join28(dir, downFile);
36888
- if (existsSync26(downPath)) {
36889
- const sqlContent = readFileSync25(downPath, "utf-8").trim();
36576
+ const downPath = join27(dir, downFile);
36577
+ if (existsSync25(downPath)) {
36578
+ const sqlContent = readFileSync24(downPath, "utf-8").trim();
36890
36579
  if (sqlContent) {
36891
36580
  const statements = splitStatements(sqlContent, delim);
36892
36581
  try {
@@ -37046,15 +36735,15 @@ function warnUnprefixedMigrations(files) {
37046
36735
  }
37047
36736
  async function migrate(adapter, options) {
37048
36737
  const db = adapter ?? getAdapter();
37049
- const dir = resolve19(options?.migrationsDir ?? "migrations");
36738
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
37050
36739
  const delimiter2 = options?.delimiter ?? ";";
37051
36740
  const result = { applied: [], skipped: [], failed: [] };
37052
- if (!existsSync26(dir)) {
36741
+ if (!existsSync25(dir)) {
37053
36742
  return result;
37054
36743
  }
37055
36744
  await ensureMigrationTableOn(db);
37056
36745
  const files = sortMigrationFiles(
37057
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
36746
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37058
36747
  );
37059
36748
  if (files.length === 0) return result;
37060
36749
  warnUnprefixedMigrations(files);
@@ -37085,7 +36774,7 @@ async function migrate(adapter, options) {
37085
36774
  result.skipped.push(file);
37086
36775
  continue;
37087
36776
  }
37088
- const sqlContent = readFileSync25(join28(dir, file), "utf-8").trim();
36777
+ const sqlContent = readFileSync24(join27(dir, file), "utf-8").trim();
37089
36778
  if (!sqlContent) {
37090
36779
  result.skipped.push(file);
37091
36780
  continue;
@@ -37120,21 +36809,21 @@ async function migrate(adapter, options) {
37120
36809
  }
37121
36810
  async function status(adapter, options) {
37122
36811
  const db = adapter ?? getAdapter();
37123
- const dir = resolve19(options?.migrationsDir ?? "migrations");
36812
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
37124
36813
  const result = { completed: [], pending: [] };
37125
- if (!existsSync26(dir)) {
36814
+ if (!existsSync25(dir)) {
37126
36815
  return result;
37127
36816
  }
37128
36817
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
37129
36818
  const files2 = sortMigrationFiles(
37130
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
36819
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37131
36820
  );
37132
36821
  result.pending = files2;
37133
36822
  return result;
37134
36823
  }
37135
36824
  await ensureMigrationTableOn(db);
37136
36825
  const files = sortMigrationFiles(
37137
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
36826
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37138
36827
  );
37139
36828
  const appliedNames = /* @__PURE__ */ new Set();
37140
36829
  try {
@@ -37159,12 +36848,18 @@ async function status(adapter, options) {
37159
36848
  return result;
37160
36849
  }
37161
36850
  async function createMigration(description, options) {
37162
- if (options?.kind === "class") {
36851
+ const kind = (options?.kind ?? "sql").trim().toLowerCase();
36852
+ if (!["sql", "code", "class"].includes(kind)) {
36853
+ throw new Error(
36854
+ `Unknown migration kind "${kind}". Use "sql" (default) or "code" (alias: "class"). An unrecognised kind used to produce a .sql file silently, which is why this now throws.`
36855
+ );
36856
+ }
36857
+ if (kind === "code" || kind === "class") {
37163
36858
  return createClassMigration(description, options);
37164
36859
  }
37165
- const dir = resolve19(options?.migrationsDir ?? "migrations");
37166
- if (!existsSync26(dir)) {
37167
- mkdirSync17(dir, { recursive: true });
36860
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
36861
+ if (!existsSync25(dir)) {
36862
+ mkdirSync16(dir, { recursive: true });
37168
36863
  }
37169
36864
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
37170
36865
  const now = /* @__PURE__ */ new Date();
@@ -37178,8 +36873,8 @@ async function createMigration(description, options) {
37178
36873
  ].join("");
37179
36874
  const upFileName = `${timestamp}_${safeName}.sql`;
37180
36875
  const downFileName = `${timestamp}_${safeName}.down.sql`;
37181
- const upPath = join28(dir, upFileName);
37182
- const downPath = join28(dir, downFileName);
36876
+ const upPath = join27(dir, upFileName);
36877
+ const downPath = join27(dir, downFileName);
37183
36878
  const upTemplate = `-- Migration: ${description}
37184
36879
  -- Created: ${now.toISOString()}
37185
36880
 
@@ -37188,14 +36883,14 @@ async function createMigration(description, options) {
37188
36883
  -- Created: ${now.toISOString()}
37189
36884
 
37190
36885
  `;
37191
- writeFileSync16(upPath, upTemplate, "utf-8");
37192
- writeFileSync16(downPath, downTemplate, "utf-8");
36886
+ writeFileSync15(upPath, upTemplate, "utf-8");
36887
+ writeFileSync15(downPath, downTemplate, "utf-8");
37193
36888
  return { upPath, downPath };
37194
36889
  }
37195
36890
  async function createClassMigration(description, options) {
37196
- const dir = resolve19(options?.migrationsDir ?? "migrations");
37197
- if (!existsSync26(dir)) {
37198
- mkdirSync17(dir, { recursive: true });
36891
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
36892
+ if (!existsSync25(dir)) {
36893
+ mkdirSync16(dir, { recursive: true });
37199
36894
  }
37200
36895
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
37201
36896
  const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
@@ -37209,7 +36904,7 @@ async function createClassMigration(description, options) {
37209
36904
  String(now.getSeconds()).padStart(2, "0")
37210
36905
  ].join("");
37211
36906
  const fileName = `${timestamp}_${safeName}.ts`;
37212
- const filePath = join28(dir, fileName);
36907
+ const filePath = join27(dir, fileName);
37213
36908
  const content = `// Migration: ${description}
37214
36909
  // Created: ${now.toISOString()}
37215
36910
 
@@ -37225,7 +36920,7 @@ export class ${className} {
37225
36920
  }
37226
36921
  }
37227
36922
  `;
37228
- writeFileSync16(filePath, content, "utf-8");
36923
+ writeFileSync15(filePath, content, "utf-8");
37229
36924
  return filePath;
37230
36925
  }
37231
36926
  var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
@@ -37286,15 +36981,13 @@ var init_migration = __esm({
37286
36981
  * Scaffold a new migration file.
37287
36982
  *
37288
36983
  * kind="sql" — creates {timestamp}_{description}.sql + .down.sql (default)
37289
- * kind="class" — creates {timestamp}_{description}.ts with a TypeScript class template
36984
+ * kind="code" — creates {timestamp}_{description}.ts with a TypeScript class
36985
+ * template. "class" is accepted as a legacy alias.
37290
36986
  *
37291
36987
  * Returns the path to the created up file (or class file).
37292
36988
  */
37293
36989
  async create(description, kind = "sql") {
37294
- if (kind === "class") {
37295
- return createClassMigration(description, { migrationsDir: this.dir });
37296
- }
37297
- return createMigration(description, { migrationsDir: this.dir });
36990
+ return createMigration(description, { migrationsDir: this.dir, kind });
37298
36991
  }
37299
36992
  /** Return list of completed (applied) migration filenames. */
37300
36993
  async getApplied() {
@@ -37308,10 +37001,10 @@ var init_migration = __esm({
37308
37001
  }
37309
37002
  /** Return sorted list of all migration files on disk (excludes .down.sql). */
37310
37003
  getFiles() {
37311
- const dir = resolve19(this.dir);
37312
- if (!existsSync26(dir)) return [];
37004
+ const dir = resolve18(this.dir);
37005
+ if (!existsSync25(dir)) return [];
37313
37006
  return sortMigrationFiles(
37314
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37007
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37315
37008
  );
37316
37009
  }
37317
37010
  };
@@ -37537,15 +37230,8 @@ function generateCrudRoutes(models, options = {}) {
37537
37230
  const total = Number(countRow[0]?.total ?? 0);
37538
37231
  const limit = qp.limit ?? 100;
37539
37232
  const page = qp.page ?? 1;
37540
- res.json({
37541
- data: rows,
37542
- meta: {
37543
- total,
37544
- page,
37545
- limit,
37546
- totalPages: Math.ceil(total / limit)
37547
- }
37548
- });
37233
+ const offset = (page - 1) * limit;
37234
+ res.json(new DatabaseResult(rows, void 0, total, limit, offset).toPaginate());
37549
37235
  }
37550
37236
  });
37551
37237
  routes.push({
@@ -37699,6 +37385,7 @@ var init_autoCrud = __esm({
37699
37385
  "src/autoCrud.ts"() {
37700
37386
  "use strict";
37701
37387
  init_database();
37388
+ init_databaseResult();
37702
37389
  init_query();
37703
37390
  init_validation();
37704
37391
  AutoCrud = class _AutoCrud {
@@ -37953,17 +37640,19 @@ var init_queryBuilder = __esm({
37953
37640
  this.ensureDb();
37954
37641
  const sql = this.toSql();
37955
37642
  const allParams = [...this.params, ...this.havingParams];
37643
+ const queryParams = allParams.length > 0 ? allParams : void 0;
37956
37644
  const rows = await adapterFetch(
37957
37645
  this.db,
37958
37646
  sql,
37959
- allParams.length > 0 ? allParams : void 0,
37647
+ queryParams,
37960
37648
  this.limitVal,
37961
37649
  this.offsetVal
37962
37650
  );
37651
+ const total = await probeTotal(this.db, sql, queryParams, this.limitVal);
37963
37652
  return new DatabaseResult(
37964
37653
  rows,
37965
37654
  void 0,
37966
- void 0,
37655
+ total,
37967
37656
  this.limitVal,
37968
37657
  this.offsetVal,
37969
37658
  this.db,
@@ -39828,8 +39517,8 @@ var init_seeder = __esm({
39828
39517
  // src/docstore.ts
39829
39518
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
39830
39519
  import { randomBytes as randomBytes7 } from "node:crypto";
39831
- import { mkdirSync as mkdirSync18 } from "node:fs";
39832
- import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29 } from "node:path";
39520
+ import { mkdirSync as mkdirSync17 } from "node:fs";
39521
+ import { dirname as dirname12, isAbsolute as isAbsolute5, join as join28 } from "node:path";
39833
39522
  function iso(d) {
39834
39523
  return d.toISOString();
39835
39524
  }
@@ -40094,8 +39783,8 @@ function resolveStorePath(dbPath) {
40094
39783
  if (dbPath === ":memory:") return dbPath;
40095
39784
  let path8 = dbPath;
40096
39785
  if (!isAbsolute5(path8)) {
40097
- path8 = join29(process.cwd(), path8);
40098
- mkdirSync18(dirname13(path8), { recursive: true });
39786
+ path8 = join28(process.cwd(), path8);
39787
+ mkdirSync17(dirname12(path8), { recursive: true });
40099
39788
  }
40100
39789
  return path8;
40101
39790
  }
@@ -40609,8 +40298,8 @@ var init_attachment = __esm({
40609
40298
 
40610
40299
  // src/realtime/storage.ts
40611
40300
  import { randomBytes as randomBytes8 } from "node:crypto";
40612
- import { mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync7, statSync as statSync18 } from "node:fs";
40613
- import { resolve as resolve20, sep as sep3 } from "node:path";
40301
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, statSync as statSync18 } from "node:fs";
40302
+ import { resolve as resolve19, sep as sep3 } from "node:path";
40614
40303
  import { createRequire as createRequire8 } from "node:module";
40615
40304
  function storageKey(filename = "") {
40616
40305
  let ext = "";
@@ -40646,23 +40335,23 @@ var init_storage = __esm({
40646
40335
  LocalStorage = class {
40647
40336
  directory;
40648
40337
  constructor(directory) {
40649
- this.directory = resolve20(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
40650
- mkdirSync19(this.directory, { recursive: true });
40338
+ this.directory = resolve19(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
40339
+ mkdirSync18(this.directory, { recursive: true });
40651
40340
  }
40652
40341
  // Resolve inside the root and reject any traversal attempt.
40653
40342
  pathFor(key) {
40654
- const target = resolve20(this.directory, key);
40343
+ const target = resolve19(this.directory, key);
40655
40344
  if (target !== this.directory && !target.startsWith(this.directory + sep3)) {
40656
40345
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
40657
40346
  }
40658
40347
  return target;
40659
40348
  }
40660
40349
  put(key, data) {
40661
- writeFileSync17(this.pathFor(key), data);
40350
+ writeFileSync16(this.pathFor(key), data);
40662
40351
  }
40663
40352
  get(key) {
40664
40353
  try {
40665
- return readFileSync26(this.pathFor(key));
40354
+ return readFileSync25(this.pathFor(key));
40666
40355
  } catch {
40667
40356
  return null;
40668
40357
  }
@@ -41072,7 +40761,6 @@ __export(index_exports, {
41072
40761
  DatabaseUrl: () => DatabaseUrl,
41073
40762
  DocStoreDriverMissing: () => DocStoreDriverMissing,
41074
40763
  FakeData: () => FakeData2,
41075
- FetchResult: () => FetchResult,
41076
40764
  FirebirdAdapter: () => FirebirdAdapter,
41077
40765
  InvalidId: () => InvalidId,
41078
40766
  LocalStorage: () => LocalStorage,
@@ -41171,7 +40859,6 @@ __export(index_exports, {
41171
40859
  });
41172
40860
  var init_index = __esm({
41173
40861
  "src/index.ts"() {
41174
- init_types();
41175
40862
  init_databaseResult();
41176
40863
  init_database();
41177
40864
  init_database();
@@ -41212,7 +40899,6 @@ export {
41212
40899
  DatabaseUrl,
41213
40900
  DocStoreDriverMissing,
41214
40901
  FakeData2 as FakeData,
41215
- FetchResult,
41216
40902
  FirebirdAdapter,
41217
40903
  InvalidId,
41218
40904
  LocalStorage,