tina4-nodejs 3.13.95 → 3.13.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CLAUDE.md +3 -4
  2. package/package.json +2 -1
  3. package/packages/cli/dist/bin.js +708 -1012
  4. package/packages/core/dist/index.js +588 -893
  5. package/packages/core/public/css/tina4.min.css +1 -1
  6. package/packages/core/src/index.ts +1 -3
  7. package/packages/core/src/messenger.ts +288 -96
  8. package/packages/core/src/queueBackends/kafkaBackend.ts +23 -2
  9. package/packages/core/src/queueBackends/rabbitmqBackend.ts +29 -17
  10. package/packages/core/src/request.ts +28 -7
  11. package/packages/core/src/server.ts +135 -7
  12. package/packages/core/src/session.ts +8 -1
  13. package/packages/orm/dist/index.js +639 -944
  14. package/packages/orm/src/autoCrud.ts +12 -10
  15. package/packages/orm/src/database.ts +62 -58
  16. package/packages/orm/src/databaseResult.ts +44 -73
  17. package/packages/orm/src/index.ts +0 -3
  18. package/packages/orm/src/migration.ts +26 -8
  19. package/packages/orm/src/model.ts +4 -0
  20. package/packages/orm/src/queryBuilder.ts +12 -5
  21. package/packages/orm/src/types.ts +7 -74
  22. package/packages/swagger/dist/index.js +78 -20
  23. package/packages/swagger/src/generator.ts +172 -29
  24. package/types/core/src/index.d.ts +1 -3
  25. package/types/core/src/messenger.d.ts +45 -4
  26. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -0
  27. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +2 -1
  28. package/types/core/src/server.d.ts +0 -4
  29. package/types/core/src/session.d.ts +7 -0
  30. package/types/orm/src/database.d.ts +34 -30
  31. package/types/orm/src/databaseResult.d.ts +26 -36
  32. package/types/orm/src/index.d.ts +1 -2
  33. package/types/orm/src/migration.d.ts +4 -3
  34. package/types/orm/src/types.d.ts +7 -34
  35. package/packages/core/src/scss.ts +0 -623
  36. package/types/core/src/scss.d.ts +0 -19
@@ -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) {
@@ -7327,10 +7257,17 @@ var init_session = __esm({
7327
7257
  *
7328
7258
  * session.flash("message", "Saved!") // set
7329
7259
  * session.flash("message") // get + auto-remove → "Saved!"
7260
+ * session.flash("message", null) // get + auto-remove (null is a GET sentinel)
7261
+ *
7262
+ * `null` — NOT just `undefined` — is the GET sentinel, so `flash(key, null)`
7263
+ * READS and clears rather than STORING null. This matches the Python master
7264
+ * (`if value is not None`), PHP (`if ($value !== null)`) and Ruby
7265
+ * (`if value.nil?`): passing the language's "no value" literal means GET. A
7266
+ * caller wanting to persist an explicit null should store it with `set()`.
7330
7267
  */
7331
7268
  flash(key, value) {
7332
7269
  const flashKey = `${FLASH_PREFIX}${key}`;
7333
- if (value !== void 0) {
7270
+ if (value !== void 0 && value !== null) {
7334
7271
  this.set(flashKey, value);
7335
7272
  return void 0;
7336
7273
  }
@@ -13201,14 +13138,14 @@ data: ${channel.buffer.shift()}
13201
13138
  `;
13202
13139
  continue;
13203
13140
  }
13204
- const gotMessage = await new Promise((resolve21) => {
13141
+ const gotMessage = await new Promise((resolve20) => {
13205
13142
  const timer = setTimeout(() => {
13206
13143
  channel.wake = null;
13207
- resolve21(false);
13144
+ resolve20(false);
13208
13145
  }, keepaliveMs);
13209
13146
  channel.wake = () => {
13210
13147
  clearTimeout(timer);
13211
- resolve21(true);
13148
+ resolve20(true);
13212
13149
  };
13213
13150
  });
13214
13151
  if (!gotMessage) yield `: keep-alive
@@ -15723,7 +15660,7 @@ var init_websocket = __esm({
15723
15660
  * Start the WebSocket server.
15724
15661
  */
15725
15662
  async start() {
15726
- return new Promise((resolve21, reject) => {
15663
+ return new Promise((resolve20, reject) => {
15727
15664
  this.server = createServer((req2, res) => {
15728
15665
  res.writeHead(426, { "Content-Type": "text/plain" });
15729
15666
  res.end("Upgrade Required");
@@ -15733,7 +15670,7 @@ var init_websocket = __esm({
15733
15670
  });
15734
15671
  this.server.listen(this.port, () => {
15735
15672
  this.startIdleReaper();
15736
- resolve21();
15673
+ resolve20();
15737
15674
  });
15738
15675
  this.server.on("error", (err) => {
15739
15676
  this.emit("error", err);
@@ -16297,7 +16234,7 @@ var init_websocket = __esm({
16297
16234
  client.trackerId = this.onAdd(socket.remoteAddress ?? "unknown", "/__dev_reload");
16298
16235
  }
16299
16236
  this.clients.add(client);
16300
- const cleanup2 = () => {
16237
+ const cleanup = () => {
16301
16238
  if (!this.clients.has(client)) return;
16302
16239
  this.clients.delete(client);
16303
16240
  if (client.trackerId && this.onRemove) this.onRemove(client.trackerId);
@@ -16320,13 +16257,13 @@ var init_websocket = __esm({
16320
16257
  socket.end();
16321
16258
  } catch {
16322
16259
  }
16323
- cleanup2();
16260
+ cleanup();
16324
16261
  return;
16325
16262
  }
16326
16263
  }
16327
16264
  });
16328
- socket.on("close", cleanup2);
16329
- socket.on("error", cleanup2);
16265
+ socket.on("close", cleanup);
16266
+ socket.on("error", cleanup);
16330
16267
  return true;
16331
16268
  }
16332
16269
  /**
@@ -17844,7 +17781,7 @@ var init_queue = __esm({
17844
17781
  const jobs = this.popBatch(resolvedBatchSize);
17845
17782
  if (jobs.length === 0) {
17846
17783
  if (resolvedPollInterval <= 0) break;
17847
- await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
17784
+ await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
17848
17785
  continue;
17849
17786
  }
17850
17787
  yield jobs;
@@ -17854,7 +17791,7 @@ var init_queue = __esm({
17854
17791
  const raw = this.pop();
17855
17792
  if (raw === null) {
17856
17793
  if (resolvedPollInterval <= 0) break;
17857
- await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
17794
+ await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
17858
17795
  continue;
17859
17796
  }
17860
17797
  yield createJob(raw, this);
@@ -22374,23 +22311,23 @@ var init_devAdmin = __esm({
22374
22311
  });
22375
22312
  };
22376
22313
  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");
22314
+ const { readFileSync: readFileSync26, existsSync: existsSync26 } = await import("node:fs");
22315
+ const { dirname: dirname13, join: join29, resolve: resolve20 } = await import("node:path");
22379
22316
  const { fileURLToPath: fileURLToPath7 } = await import("node:url");
22380
- const dir = dirname14(fileURLToPath7(import.meta.url));
22317
+ const dir = dirname13(fileURLToPath7(import.meta.url));
22381
22318
  const candidates = [
22382
- join30(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
22319
+ join29(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
22383
22320
  // src/../public/js/
22384
- join30(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
22321
+ join29(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
22385
22322
  // 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")
22323
+ resolve20(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
22324
+ resolve20(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
22388
22325
  // project public/
22389
22326
  ];
22390
22327
  for (const jsPath of candidates) {
22391
- if (existsSync27(jsPath)) {
22328
+ if (existsSync26(jsPath)) {
22392
22329
  try {
22393
- const content = readFileSync27(jsPath, "utf-8");
22330
+ const content = readFileSync26(jsPath, "utf-8");
22394
22331
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
22395
22332
  res.raw.end(content);
22396
22333
  return;
@@ -22851,8 +22788,12 @@ function sanitizeSecurity(reqs, schemes) {
22851
22788
  function generate(routes, models = []) {
22852
22789
  const info = {
22853
22790
  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"
22791
+ // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
22792
+ // 0.0.1). description defaults to the empty string, not a canned sentence.
22793
+ // Both are the settled cross-framework defaults (parity with the Python
22794
+ // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
22795
+ version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
22796
+ description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
22856
22797
  };
22857
22798
  const contactEmail = (process.env.TINA4_SWAGGER_CONTACT_EMAIL ?? "").trim();
22858
22799
  const contactName = (process.env.TINA4_SWAGGER_CONTACT_TEAM ?? "").trim();
@@ -22885,9 +22826,11 @@ function generate(routes, models = []) {
22885
22826
  const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
22886
22827
  const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
22887
22828
  const refSchemas = /* @__PURE__ */ new Set();
22829
+ const tableToSchema = /* @__PURE__ */ new Map();
22888
22830
  for (const model of models) {
22889
- const schema = modelToSchema(model);
22890
- spec.components.schemas[model.tableName] = schema;
22831
+ const schemaKey = schemaNameForModel(model);
22832
+ tableToSchema.set(model.tableName, schemaKey);
22833
+ spec.components.schemas[schemaKey] = modelToSchema(model);
22891
22834
  }
22892
22835
  const usedTags = [];
22893
22836
  const seenIds = /* @__PURE__ */ new Set();
@@ -22914,11 +22857,11 @@ function generate(routes, models = []) {
22914
22857
  if (route.meta?.deprecated) operation.deprecated = true;
22915
22858
  const pathParams = extractPathParams(route.pattern);
22916
22859
  if (pathParams.length > 0) {
22917
- operation.parameters = pathParams.map((name) => ({
22860
+ operation.parameters = pathParams.map(({ name, schema }) => ({
22918
22861
  name,
22919
22862
  in: "path",
22920
22863
  required: true,
22921
- schema: { type: "string" }
22864
+ schema
22922
22865
  }));
22923
22866
  }
22924
22867
  if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
@@ -22944,19 +22887,20 @@ function generate(routes, models = []) {
22944
22887
  };
22945
22888
  } else if (method === "post" || method === "put") {
22946
22889
  const modelName = inferModelFromPath(route.pattern);
22947
- if (modelName && models.some((m) => m.tableName === modelName)) {
22948
- const media = {
22949
- schema: { $ref: `#/components/schemas/${modelName}` }
22950
- };
22890
+ const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
22891
+ if (schemaKey) {
22892
+ const sref = `#/components/schemas/${schemaKey}`;
22893
+ const media = { schema: { $ref: sref } };
22951
22894
  if (route.meta?.example !== void 0) media.example = route.meta.example;
22952
22895
  operation.requestBody = {
22953
22896
  required: true,
22954
22897
  content: { "application/json": media }
22955
22898
  };
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
- };
22899
+ if (route.meta?.responses === void 0) {
22900
+ operation.responses = {
22901
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
22902
+ };
22903
+ }
22960
22904
  } else if (route.meta?.example !== void 0) {
22961
22905
  operation.requestBody = {
22962
22906
  content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
@@ -23043,6 +22987,21 @@ function resolveServers() {
23043
22987
  const dev = (process.env.SWAGGER_DEV_URL ?? "").trim();
23044
22988
  return dev.length > 0 ? [{ url: dev }] : [{ url: "/" }];
23045
22989
  }
22990
+ function schemaNameForModel(model) {
22991
+ const explicit = model.className?.trim();
22992
+ if (explicit) return explicit;
22993
+ return deriveClassName(model.tableName);
22994
+ }
22995
+ function deriveClassName(tableName) {
22996
+ return singularize(tableName).split(/[_\s-]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || tableName;
22997
+ }
22998
+ function singularize(word) {
22999
+ if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
23000
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) return word.slice(0, -2);
23001
+ if (/ss$/i.test(word)) return word;
23002
+ if (/s$/i.test(word) && word.length > 1) return word.slice(0, -1);
23003
+ return word;
23004
+ }
23046
23005
  function modelToSchema(model) {
23047
23006
  const properties = {};
23048
23007
  const required = [];
@@ -23116,15 +23075,35 @@ function inferSchema(value) {
23116
23075
  if (typeof value === "number") return { type: Number.isInteger(value) ? "integer" : "number" };
23117
23076
  return { type: "string" };
23118
23077
  }
23078
+ function segmentParam(segment) {
23079
+ if (segment.startsWith("{") && segment.endsWith("}")) {
23080
+ const inner = segment.slice(1, -1);
23081
+ if (inner.startsWith("...")) return { name: inner.slice(3), type: "string" };
23082
+ const colon = inner.indexOf(":");
23083
+ if (colon >= 0) return { name: inner.slice(0, colon), type: inner.slice(colon + 1) };
23084
+ return { name: inner, type: "string" };
23085
+ }
23086
+ if (segment.startsWith("[") && segment.endsWith("]")) {
23087
+ const inner = segment.slice(1, -1);
23088
+ return { name: inner.startsWith("...") ? inner.slice(3) : inner, type: "string" };
23089
+ }
23090
+ if (segment.startsWith(":") && segment.length > 1) {
23091
+ return { name: segment.slice(1), type: "string" };
23092
+ }
23093
+ return null;
23094
+ }
23119
23095
  function patternToOpenAPI(pattern) {
23120
- return pattern.replace(/\[\.\.\.(\w+)\]/g, "{$1}").replace(/\[(\w+)\]/g, "{$1}");
23096
+ return pattern.split("/").map((segment) => {
23097
+ const p = segmentParam(segment);
23098
+ return p ? `{${p.name}}` : segment;
23099
+ }).join("/");
23121
23100
  }
23122
23101
  function extractPathParams(pattern) {
23123
23102
  const params = [];
23124
- const regex = /\[(?:\.\.\.)?(\w+)\]/g;
23125
- let match;
23126
- while ((match = regex.exec(pattern)) !== null) {
23127
- params.push(match[1]);
23103
+ for (const segment of pattern.split("/")) {
23104
+ const p = segmentParam(segment);
23105
+ if (!p) continue;
23106
+ params.push({ name: p.name, schema: { ...PARAM_TYPE_SCHEMA[p.type] ?? { type: "string" } } });
23128
23107
  }
23129
23108
  return params;
23130
23109
  }
@@ -23146,8 +23125,12 @@ function inferModelFromPath(pattern) {
23146
23125
  if (rest.length === 1 && /^[[{]\.{0,3}\w+[\]}]$/.test(rest[0])) return candidate;
23147
23126
  return null;
23148
23127
  }
23128
+ function operationIdBase(method, openApiPath) {
23129
+ const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
23130
+ return clean ? `${method}_${clean}` : method;
23131
+ }
23149
23132
  function uniqueOperationId(method, openApiPath, seen) {
23150
- const base = (method + openApiPath.replace(/[/{}]/g, "_")).replace(/_+/g, "_").replace(/_$/, "");
23133
+ const base = operationIdBase(method, openApiPath);
23151
23134
  let oid = base;
23152
23135
  let n = 2;
23153
23136
  while (seen.has(oid)) {
@@ -23157,13 +23140,25 @@ function uniqueOperationId(method, openApiPath, seen) {
23157
23140
  seen.add(oid);
23158
23141
  return oid;
23159
23142
  }
23160
- var WRITE_METHODS, registeredSchemes, registeredSchemas;
23143
+ var WRITE_METHODS, registeredSchemes, registeredSchemas, PARAM_TYPE_SCHEMA;
23161
23144
  var init_generator = __esm({
23162
23145
  "../swagger/src/generator.ts"() {
23163
23146
  "use strict";
23164
23147
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
23165
23148
  registeredSchemes = {};
23166
23149
  registeredSchemas = {};
23150
+ PARAM_TYPE_SCHEMA = {
23151
+ int: { type: "integer" },
23152
+ integer: { type: "integer" },
23153
+ float: { type: "number" },
23154
+ number: { type: "number" },
23155
+ uuid: { type: "string", format: "uuid" },
23156
+ slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
23157
+ alpha: { type: "string", pattern: "^[A-Za-z]+$" },
23158
+ alnum: { type: "string", pattern: "^[A-Za-z0-9]+$" },
23159
+ path: { type: "string" },
23160
+ string: { type: "string" }
23161
+ };
23167
23162
  }
23168
23163
  });
23169
23164
 
@@ -23483,10 +23478,29 @@ function openBrowser(url) {
23483
23478
  }, 2e3);
23484
23479
  }
23485
23480
  function resolvePortAndHost(config) {
23486
- const port = config?.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? 7148;
23481
+ const tina4Port = process.env.TINA4_PORT;
23482
+ const legacyPort = process.env.PORT;
23483
+ let port;
23484
+ if (config?.port !== void 0) {
23485
+ port = config.port;
23486
+ } else if (tina4Port && /^\d+$/.test(tina4Port)) {
23487
+ port = parseInt(tina4Port, 10);
23488
+ } else if (legacyPort && /^\d+$/.test(legacyPort)) {
23489
+ port = parseInt(legacyPort, 10);
23490
+ warnDeprecatedPort(port);
23491
+ } else {
23492
+ port = 7148;
23493
+ }
23487
23494
  const host = config?.host ?? process.env.TINA4_HOST ?? process.env.HOST ?? "0.0.0.0";
23488
23495
  return { port, host };
23489
23496
  }
23497
+ function warnDeprecatedPort(port) {
23498
+ if (portDeprecationWarned) return;
23499
+ portDeprecationWarned = true;
23500
+ Log.warning(
23501
+ `PORT is deprecated and will be removed in 3.14 - use TINA4_PORT instead (binding port ${port} from PORT)`
23502
+ );
23503
+ }
23490
23504
  function isBannerSuppressed() {
23491
23505
  return isTruthy(process.env.TINA4_SUPPRESS);
23492
23506
  }
@@ -23758,6 +23772,29 @@ function deployGallery(name) {
23758
23772
  </body>
23759
23773
  </html>`;
23760
23774
  }
23775
+ function startLoopWatchdog() {
23776
+ const raw = (process.env.TINA4_LOOP_LAG_WARN_MS ?? "").trim();
23777
+ const threshold = /^\d+$/.test(raw) ? parseInt(raw, 10) : 250;
23778
+ if (threshold <= 0) {
23779
+ return { stop: () => {
23780
+ } };
23781
+ }
23782
+ let last = Date.now();
23783
+ let warned = 0;
23784
+ const timer = setInterval(() => {
23785
+ const now = Date.now();
23786
+ const lag = now - last - LOOP_WATCHDOG_TICK_MS;
23787
+ last = now;
23788
+ if (lag < threshold) return;
23789
+ warned++;
23790
+ if (warned > 5 && warned % 20 !== 0) return;
23791
+ Log.warning(
23792
+ `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.`
23793
+ );
23794
+ }, LOOP_WATCHDOG_TICK_MS);
23795
+ timer.unref();
23796
+ return { stop: () => clearInterval(timer) };
23797
+ }
23761
23798
  async function start(config) {
23762
23799
  const isManaged = process.argv.includes("--managed");
23763
23800
  if (!isManaged && process.env.TINA4_OVERRIDE_CLIENT !== "true") {
@@ -24007,7 +24044,9 @@ async function startServer(config) {
24007
24044
  const resolved = resolvePortAndHost(config);
24008
24045
  const host = resolved.host;
24009
24046
  let port = resolved.port;
24010
- port = findAvailablePort(port);
24047
+ if (!cluster.isWorker) {
24048
+ port = findAvailablePort(port);
24049
+ }
24011
24050
  const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
24012
24051
  if (cluster.isPrimary && isProduction) {
24013
24052
  const numCPUs = os2.cpus().length;
@@ -24223,7 +24262,20 @@ ${reset2}
24223
24262
  await sessionAutoStart(rawReq, rawRes, req2);
24224
24263
  await middleware.run(req2, res);
24225
24264
  if (res.raw.writableEnded) return;
24226
- await req2.parseBody();
24265
+ try {
24266
+ await req2.parseBody();
24267
+ } catch (err) {
24268
+ const status2 = err?.statusCode;
24269
+ if (typeof status2 === "number" && status2 >= 400 && status2 < 500) {
24270
+ if (!rawRes.writableEnded) {
24271
+ rawRes.statusCode = status2;
24272
+ rawRes.setHeader("content-type", "application/json");
24273
+ rawRes.end(JSON.stringify({ error: err.message }));
24274
+ }
24275
+ return;
24276
+ }
24277
+ throw err;
24278
+ }
24227
24279
  const pathname = req2.path;
24228
24280
  const reqStartTime = DevAdmin.isEnabled() ? Date.now() : 0;
24229
24281
  const matchedPattern = { value: "" };
@@ -24422,8 +24474,10 @@ ${reset2}
24422
24474
  };
24423
24475
  process.on("SIGTERM", onSigterm);
24424
24476
  process.on("SIGINT", onSigint);
24477
+ const loopWatchdog = startLoopWatchdog();
24425
24478
  resolvePromise({
24426
24479
  close: () => {
24480
+ loopWatchdog.stop();
24427
24481
  process.off("SIGTERM", onSigterm);
24428
24482
  process.off("SIGINT", onSigint);
24429
24483
  stopAllBackgroundTasks();
@@ -24438,7 +24492,7 @@ ${reset2}
24438
24492
  });
24439
24493
  });
24440
24494
  }
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;
24495
+ 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
24496
  var init_server = __esm({
24443
24497
  "../core/src/server.ts"() {
24444
24498
  "use strict";
@@ -24491,6 +24545,7 @@ var init_server = __esm({
24491
24545
  SWAGGER_VERSION: "TINA4_SWAGGER_VERSION",
24492
24546
  ORM_PLURAL_TABLE_NAMES: "TINA4_ORM_PLURAL_TABLE_NAMES"
24493
24547
  };
24548
+ portDeprecationWarned = false;
24494
24549
  TEMPLATE_PAGES_DIR = "pages";
24495
24550
  HTTP_REASON_PHRASES = {
24496
24551
  100: "Continue",
@@ -24527,6 +24582,7 @@ var init_server = __esm({
24527
24582
  templateCache = null;
24528
24583
  _dispatchFn = null;
24529
24584
  _serverHandle = null;
24585
+ LOOP_WATCHDOG_TICK_MS = 100;
24530
24586
  FALLBACK_STAGES = [
24531
24587
  serveTemplateFallback,
24532
24588
  serveLandingPage,
@@ -25082,433 +25138,6 @@ var init_fakeData = __esm({
25082
25138
  }
25083
25139
  });
25084
25140
 
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
25141
  // ../core/src/mqttMessage.ts
25513
25142
  var MqttMessage;
25514
25143
  var init_mqttMessage = __esm({
@@ -25582,7 +25211,7 @@ var init_mqttMessage = __esm({
25582
25211
  import net2 from "node:net";
25583
25212
  import tls from "node:tls";
25584
25213
  import { randomBytes as randomBytes5 } from "node:crypto";
25585
- import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
25214
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "node:fs";
25586
25215
  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
25216
  var init_mqtt = __esm({
25588
25217
  "../core/src/mqtt.ts"() {
@@ -25785,7 +25414,7 @@ var init_mqtt = __esm({
25785
25414
  */
25786
25415
  async connect() {
25787
25416
  this.closeSocket();
25788
- if (this.secure && this.tlsVerify && this.caFile && !existsSync24(this.caFile)) {
25417
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync23(this.caFile)) {
25789
25418
  throw new MqttError(
25790
25419
  `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
25420
  );
@@ -26022,7 +25651,7 @@ var init_mqtt = __esm({
26022
25651
  * a later client.
26023
25652
  */
26024
25653
  openSocket() {
26025
- return new Promise((resolve21, reject) => {
25654
+ return new Promise((resolve20, reject) => {
26026
25655
  let settled = false;
26027
25656
  const settle = (fn) => {
26028
25657
  if (settled) return;
@@ -26047,10 +25676,10 @@ var init_mqtt = __esm({
26047
25676
  servername: this.host,
26048
25677
  rejectUnauthorized: this.tlsVerify
26049
25678
  };
26050
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
26051
- sock = tls.connect(opts, () => settle(() => resolve21(sock)));
25679
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync21(this.caFile);
25680
+ sock = tls.connect(opts, () => settle(() => resolve20(sock)));
26052
25681
  } else {
26053
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
25682
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
26054
25683
  }
26055
25684
  sock.once("error", (err) => {
26056
25685
  settle(() => {
@@ -26089,13 +25718,13 @@ var init_mqtt = __esm({
26089
25718
  writePacket(header, body) {
26090
25719
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
26091
25720
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
26092
- return new Promise((resolve21, reject) => {
25721
+ return new Promise((resolve20, reject) => {
26093
25722
  this.socket.write(packet, (err) => {
26094
25723
  if (err) {
26095
25724
  reject(new MqttError(`MQTT write failed: ${err.message}`));
26096
25725
  } else {
26097
25726
  this.lastWriteAt = Date.now();
26098
- resolve21();
25727
+ resolve20();
26099
25728
  }
26100
25729
  });
26101
25730
  });
@@ -26128,7 +25757,7 @@ var init_mqtt = __esm({
26128
25757
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
26129
25758
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
26130
25759
  if (this.socketError !== null) return Promise.reject(this.socketError);
26131
- return new Promise((resolve21, reject) => {
25760
+ return new Promise((resolve20, reject) => {
26132
25761
  let timer = null;
26133
25762
  if (deadline !== null) {
26134
25763
  const remaining = deadline - Date.now();
@@ -26143,7 +25772,7 @@ var init_mqtt = __esm({
26143
25772
  }
26144
25773
  }, remaining);
26145
25774
  }
26146
- this.waiter = { need, resolve: resolve21, reject, timer };
25775
+ this.waiter = { need, resolve: resolve20, reject, timer };
26147
25776
  this.serviceWaiter();
26148
25777
  });
26149
25778
  }
@@ -26267,8 +25896,8 @@ var init_mqtt = __esm({
26267
25896
  });
26268
25897
 
26269
25898
  // ../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";
25899
+ import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
25900
+ import { join as join23, extname as extname7 } from "node:path";
26272
25901
  import { pathToFileURL } from "node:url";
26273
25902
  function matchCronField(field, value) {
26274
25903
  if (field === "*") return true;
@@ -26434,14 +26063,14 @@ var init_service = __esm({
26434
26063
  const discovered = [];
26435
26064
  let entries;
26436
26065
  try {
26437
- entries = readdirSync17(dir);
26066
+ entries = readdirSync16(dir);
26438
26067
  } catch {
26439
26068
  return discovered;
26440
26069
  }
26441
26070
  for (const entry of entries) {
26442
26071
  const ext = extname7(entry);
26443
26072
  if (ext !== ".ts" && ext !== ".js") continue;
26444
- const fullPath = join24(dir, entry);
26073
+ const fullPath = join23(dir, entry);
26445
26074
  const stat = statSync16(fullPath);
26446
26075
  if (!stat.isFile()) continue;
26447
26076
  try {
@@ -26550,14 +26179,14 @@ var init_service = __esm({
26550
26179
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
26551
26180
  let entries;
26552
26181
  try {
26553
- entries = readdirSync17(dir);
26182
+ entries = readdirSync16(dir);
26554
26183
  } catch {
26555
26184
  return;
26556
26185
  }
26557
26186
  for (const entry of entries) {
26558
26187
  const ext = extname7(entry);
26559
26188
  if (ext !== ".ts" && ext !== ".js") continue;
26560
- const fullPath = join24(dir, entry);
26189
+ const fullPath = join23(dir, entry);
26561
26190
  if (watchedFiles.has(fullPath)) continue;
26562
26191
  watchedFiles.add(fullPath);
26563
26192
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -26601,7 +26230,7 @@ import https from "node:https";
26601
26230
  import { URL as URL2 } from "node:url";
26602
26231
  import { randomBytes as randomBytes6 } from "node:crypto";
26603
26232
  import { promises as fsp, createWriteStream } from "node:fs";
26604
- import { basename as basename5 } from "node:path";
26233
+ import { basename as basename4 } from "node:path";
26605
26234
  import { pipeline } from "node:stream/promises";
26606
26235
  function sameOrigin(urlA, urlB) {
26607
26236
  try {
@@ -26891,7 +26520,7 @@ var init_api = __esm({
26891
26520
  error: err instanceof Error ? err.message : String(err)
26892
26521
  };
26893
26522
  }
26894
- uploadName = filename || basename5(filePath);
26523
+ uploadName = filename || basename4(filePath);
26895
26524
  } else {
26896
26525
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
26897
26526
  }
@@ -27106,12 +26735,12 @@ var init_api = __esm({
27106
26735
  * authenticate to.
27107
26736
  */
27108
26737
  performRequest(method, url, headers, data, redirectsLeft) {
27109
- return new Promise((resolve21) => {
26738
+ return new Promise((resolve20) => {
27110
26739
  let parsed;
27111
26740
  try {
27112
26741
  parsed = new URL2(url);
27113
26742
  } catch (err) {
27114
- resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
26743
+ resolve20({ kind: "error", error: err instanceof Error ? err.message : String(err) });
27115
26744
  return;
27116
26745
  }
27117
26746
  const isHttps = parsed.protocol === "https:";
@@ -27136,7 +26765,7 @@ var init_api = __esm({
27136
26765
  try {
27137
26766
  nextUrl = new URL2(location, url).toString();
27138
26767
  } catch {
27139
- resolve21({ kind: "response", res });
26768
+ resolve20({ kind: "response", res });
27140
26769
  return;
27141
26770
  }
27142
26771
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -27154,17 +26783,17 @@ var init_api = __esm({
27154
26783
  deleteHeaderCaseInsensitive(nextHeaders, name);
27155
26784
  }
27156
26785
  }
27157
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
26786
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve20);
27158
26787
  return;
27159
26788
  }
27160
- resolve21({ kind: "response", res });
26789
+ resolve20({ kind: "response", res });
27161
26790
  });
27162
26791
  req2.on("timeout", () => {
27163
26792
  req2.destroy();
27164
- resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
26793
+ resolve20({ kind: "error", error: `Request timed out after ${this.timeout}s` });
27165
26794
  });
27166
26795
  req2.on("error", (err) => {
27167
- resolve21({ kind: "error", error: err.message });
26796
+ resolve20({ kind: "error", error: err.message });
27168
26797
  });
27169
26798
  if (data) {
27170
26799
  req2.write(data);
@@ -27174,7 +26803,7 @@ var init_api = __esm({
27174
26803
  }
27175
26804
  /** Buffer a response body, parse JSON if possible, and store cookies. */
27176
26805
  readResponse(res) {
27177
- return new Promise((resolve21) => {
26806
+ return new Promise((resolve20) => {
27178
26807
  const chunks = [];
27179
26808
  res.on("data", (chunk) => {
27180
26809
  chunks.push(chunk);
@@ -27189,7 +26818,7 @@ var init_api = __esm({
27189
26818
  } catch {
27190
26819
  parsed = raw;
27191
26820
  }
27192
- resolve21({
26821
+ resolve20({
27193
26822
  http_code: res.statusCode ?? null,
27194
26823
  body: parsed,
27195
26824
  headers: respHeaders,
@@ -27197,7 +26826,7 @@ var init_api = __esm({
27197
26826
  });
27198
26827
  });
27199
26828
  res.on("error", (err) => {
27200
- resolve21({ http_code: null, body: null, headers: {}, error: err.message });
26829
+ resolve20({ http_code: null, body: null, headers: {}, error: err.message });
27201
26830
  });
27202
26831
  });
27203
26832
  }
@@ -27250,14 +26879,14 @@ var init_api = __esm({
27250
26879
  // ../core/src/messenger.ts
27251
26880
  import net3 from "node:net";
27252
26881
  import tls2 from "node:tls";
27253
- import { readFileSync as readFileSync23 } from "node:fs";
27254
- import { basename as basename6 } from "node:path";
26882
+ import { readFileSync as readFileSync22 } from "node:fs";
26883
+ import { basename as basename5 } from "node:path";
27255
26884
  import { randomUUID as randomUUID7 } from "node:crypto";
27256
26885
  function tlsRejectUnauthorized() {
27257
26886
  return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
27258
26887
  }
27259
26888
  function readResponse(socket) {
27260
- return new Promise((resolve21, reject) => {
26889
+ return new Promise((resolve20, reject) => {
27261
26890
  let buffer = "";
27262
26891
  const onData = (chunk) => {
27263
26892
  buffer += chunk.toString("utf-8");
@@ -27269,7 +26898,7 @@ function readResponse(socket) {
27269
26898
  if (line.length >= 4 && line[3] === " ") {
27270
26899
  socket.removeListener("data", onData);
27271
26900
  socket.removeListener("error", onError);
27272
- resolve21({ code, text: buffer.trim() });
26901
+ resolve20({ code, text: buffer.trim() });
27273
26902
  return;
27274
26903
  }
27275
26904
  }
@@ -27283,10 +26912,10 @@ function readResponse(socket) {
27283
26912
  });
27284
26913
  }
27285
26914
  function sendCommand(socket, command) {
27286
- return new Promise((resolve21, reject) => {
26915
+ return new Promise((resolve20, reject) => {
27287
26916
  socket.write(command + "\r\n", "utf-8", (err) => {
27288
26917
  if (err) return reject(err);
27289
- readResponse(socket).then(resolve21, reject);
26918
+ readResponse(socket).then(resolve20, reject);
27290
26919
  });
27291
26920
  });
27292
26921
  }
@@ -27342,8 +26971,8 @@ function buildMimeMessage(options) {
27342
26971
  lines.push(options.body);
27343
26972
  }
27344
26973
  for (const filePath of options.attachments) {
27345
- const fileName = basename6(filePath);
27346
- const fileData = readFileSync23(filePath);
26974
+ const fileName = basename5(filePath);
26975
+ const fileData = readFileSync22(filePath);
27347
26976
  const base64Data = fileData.toString("base64");
27348
26977
  lines.push("");
27349
26978
  lines.push(`--${boundary}`);
@@ -27386,7 +27015,7 @@ function imapQuote(s) {
27386
27015
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
27387
27016
  }
27388
27017
  function imapReadLine(socket) {
27389
- return new Promise((resolve21, reject) => {
27018
+ return new Promise((resolve20, reject) => {
27390
27019
  let buffer = "";
27391
27020
  const onData = (chunk) => {
27392
27021
  buffer += chunk.toString("utf-8");
@@ -27394,7 +27023,7 @@ function imapReadLine(socket) {
27394
27023
  if (nlIndex !== -1) {
27395
27024
  socket.removeListener("data", onData);
27396
27025
  socket.removeListener("error", onError);
27397
- resolve21(buffer);
27026
+ resolve20(buffer);
27398
27027
  }
27399
27028
  };
27400
27029
  const onError = (err) => {
@@ -27406,7 +27035,7 @@ function imapReadLine(socket) {
27406
27035
  });
27407
27036
  }
27408
27037
  function imapCommand(socket, command) {
27409
- return new Promise((resolve21, reject) => {
27038
+ return new Promise((resolve20, reject) => {
27410
27039
  imapTagCounter++;
27411
27040
  const tag = `T${imapTagCounter}`;
27412
27041
  const fullCommand = `${tag} ${command}\r
@@ -27417,7 +27046,7 @@ function imapCommand(socket, command) {
27417
27046
  if (buffer.includes(`${tag} OK`)) {
27418
27047
  socket.removeListener("data", onData);
27419
27048
  socket.removeListener("error", onError);
27420
- resolve21(buffer);
27049
+ resolve20(buffer);
27421
27050
  return;
27422
27051
  }
27423
27052
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -27446,91 +27075,149 @@ function parseSearchResponse(response) {
27446
27075
  if (!match) return [];
27447
27076
  return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
27448
27077
  }
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
- };
27078
+ function extractRawMessage(response) {
27079
+ const m = response.match(/\{(\d+)\}\r\n/);
27080
+ if (!m) return response;
27081
+ const start2 = (m.index ?? 0) + m[0].length;
27082
+ return response.slice(start2, start2 + parseInt(m[1], 10));
27477
27083
  }
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) : "";
27084
+ function parseMimeHeaders(section) {
27484
27085
  const headers = {};
27485
- const headerLines = headerSection.split(/\r\n/);
27486
27086
  let currentKey = "";
27487
- for (const line of headerLines) {
27087
+ for (const line of section.split(/\r\n/)) {
27488
27088
  if (/^\s/.test(line) && currentKey) {
27489
27089
  headers[currentKey] += " " + line.trim();
27490
27090
  } 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();
27091
+ const idx = line.indexOf(":");
27092
+ if (idx > 0) {
27093
+ currentKey = line.substring(0, idx).trim().toLowerCase();
27094
+ headers[currentKey] = line.substring(idx + 1).trim();
27095
+ }
27096
+ }
27097
+ }
27098
+ return headers;
27099
+ }
27100
+ function decodeTransfer(body, encoding) {
27101
+ const enc = encoding.toLowerCase().trim();
27102
+ if (enc === "base64") {
27103
+ try {
27104
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf-8");
27105
+ } catch {
27106
+ return body;
27107
+ }
27108
+ }
27109
+ if (enc === "quoted-printable") {
27110
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
27111
+ }
27112
+ return body;
27113
+ }
27114
+ function decodeAttachmentBytes(body, encoding) {
27115
+ const enc = encoding.toLowerCase().trim();
27116
+ if (enc === "base64") {
27117
+ return Buffer.from(body.replace(/\s+/g, ""), "base64");
27118
+ }
27119
+ const trimmed = body.replace(/\r\n$/, "");
27120
+ if (enc === "quoted-printable") {
27121
+ const collapsed = trimmed.replace(/=\r?\n/g, "");
27122
+ const bytes = [];
27123
+ for (let i = 0; i < collapsed.length; i++) {
27124
+ const hex = collapsed.substring(i + 1, i + 3);
27125
+ if (collapsed[i] === "=" && /^[0-9A-Fa-f]{2}$/.test(hex)) {
27126
+ bytes.push(parseInt(hex, 16));
27127
+ i += 2;
27128
+ } else {
27129
+ bytes.push(collapsed.charCodeAt(i) & 255);
27495
27130
  }
27496
27131
  }
27132
+ return Buffer.from(bytes);
27497
27133
  }
27134
+ return Buffer.from(trimmed, "utf-8");
27135
+ }
27136
+ function attachmentFilename(disposition, contentType) {
27137
+ const d = disposition.match(/filename="?([^";\r\n]+)"?/i);
27138
+ if (d) return d[1].trim();
27139
+ const c = contentType.match(/name="?([^";\r\n]+)"?/i);
27140
+ if (c) return c[1].trim();
27141
+ return "attachment";
27142
+ }
27143
+ function makeSnippet(bodyText, bodyHtml) {
27144
+ return (bodyText || bodyHtml || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
27145
+ }
27146
+ function toIsoDate(raw) {
27147
+ if (!raw) return "";
27148
+ const d = new Date(raw);
27149
+ return Number.isNaN(d.getTime()) ? raw : d.toISOString();
27150
+ }
27151
+ function parseMessage(response) {
27152
+ const raw = extractRawMessage(response);
27153
+ const headerEnd = raw.indexOf("\r\n\r\n");
27154
+ const headerSection = headerEnd >= 0 ? raw.substring(0, headerEnd) : raw;
27155
+ const bodySection = headerEnd >= 0 ? raw.substring(headerEnd + 4) : "";
27156
+ const headers = parseMimeHeaders(headerSection);
27498
27157
  const contentType = headers["content-type"] ?? "text/plain";
27499
27158
  let bodyText = "";
27500
27159
  let bodyHtml = "";
27160
+ const attachments = [];
27501
27161
  if (contentType.includes("multipart")) {
27502
27162
  const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
27503
27163
  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;
27164
+ const boundary = "--" + boundaryMatch[1];
27165
+ for (const part of bodySection.split(boundary)) {
27166
+ const trimmed = part.trim();
27167
+ if (trimmed === "" || trimmed === "--") continue;
27168
+ const pEnd = part.indexOf("\r\n\r\n");
27169
+ if (pEnd < 0) continue;
27170
+ const pHeaders = parseMimeHeaders(part.substring(0, pEnd));
27171
+ const pBody = part.substring(pEnd + 4);
27172
+ const cte = pHeaders["content-transfer-encoding"] ?? "";
27173
+ const pType = pHeaders["content-type"] ?? "text/plain";
27174
+ const disposition = pHeaders["content-disposition"] ?? "";
27175
+ if (/attachment/i.test(disposition)) {
27176
+ const content = decodeAttachmentBytes(pBody, cte);
27177
+ attachments.push({
27178
+ filename: attachmentFilename(disposition, pType),
27179
+ contentType: pType.split(";")[0].trim(),
27180
+ size: content.length,
27181
+ content
27182
+ });
27183
+ } else if (pType.includes("text/html")) {
27184
+ bodyHtml = decodeTransfer(pBody, cte).trim();
27185
+ } else if (pType.includes("text/plain")) {
27186
+ bodyText = decodeTransfer(pBody, cte).trim();
27515
27187
  }
27516
27188
  }
27517
27189
  }
27518
27190
  } else if (contentType.includes("text/html")) {
27519
- bodyHtml = bodySection;
27191
+ bodyHtml = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
27520
27192
  } else {
27521
- bodyText = bodySection;
27193
+ bodyText = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
27522
27194
  }
27523
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
27524
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
27195
+ return { headers, bodyText, bodyHtml, attachments };
27196
+ }
27197
+ function parseSummary(uid, response) {
27198
+ const { headers, bodyText, bodyHtml } = parseMessage(response);
27199
+ return {
27200
+ uid,
27201
+ subject: headers["subject"] ?? "",
27202
+ from: headers["from"] ?? "",
27203
+ to: headers["to"] ?? "",
27204
+ date: toIsoDate(headers["date"] ?? ""),
27205
+ snippet: makeSnippet(bodyText, bodyHtml),
27206
+ seen: /\\Seen/i.test(response)
27207
+ };
27208
+ }
27209
+ function parseFullMessage(uid, response) {
27210
+ const { headers, bodyText, bodyHtml, attachments } = parseMessage(response);
27525
27211
  return {
27526
27212
  uid,
27527
27213
  subject: headers["subject"] ?? "",
27528
27214
  from: headers["from"] ?? "",
27529
27215
  to: headers["to"] ?? "",
27530
27216
  cc: headers["cc"] ?? "",
27531
- date: headers["date"] ?? "",
27217
+ date: toIsoDate(headers["date"] ?? ""),
27532
27218
  bodyText,
27533
27219
  bodyHtml,
27220
+ attachments,
27534
27221
  headers
27535
27222
  };
27536
27223
  }
@@ -27646,89 +27333,89 @@ var init_messenger = __esm({
27646
27333
  }
27647
27334
  const messageId = `${randomUUID7()}@${this.host}`;
27648
27335
  if (allRecipients.length === 0) {
27649
- return { success: false, message: "No recipients specified" };
27336
+ return { success: false, message: "No recipients specified", id: null };
27650
27337
  }
27651
27338
  if (!this.fromAddress) {
27652
- return { success: false, message: "No from address configured" };
27339
+ return { success: false, message: "No from address configured", id: null };
27653
27340
  }
27654
27341
  try {
27655
27342
  let socket;
27656
27343
  if (this.port === 465) {
27657
27344
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
27658
- await new Promise((resolve21, reject) => {
27659
- socket.once("secureConnect", resolve21);
27345
+ await new Promise((resolve20, reject) => {
27346
+ socket.once("secureConnect", resolve20);
27660
27347
  socket.once("error", reject);
27661
27348
  });
27662
27349
  } else {
27663
27350
  socket = net3.createConnection({ host: this.host, port: this.port });
27664
- await new Promise((resolve21, reject) => {
27665
- socket.once("connect", resolve21);
27351
+ await new Promise((resolve20, reject) => {
27352
+ socket.once("connect", resolve20);
27666
27353
  socket.once("error", reject);
27667
27354
  });
27668
27355
  }
27669
27356
  const greeting = await readResponse(socket);
27670
27357
  if (greeting.code !== 220) {
27671
27358
  socket.destroy();
27672
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
27359
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}`, id: null };
27673
27360
  }
27674
27361
  const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
27675
27362
  if (ehlo.code !== 250) {
27676
27363
  socket.destroy();
27677
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
27364
+ return { success: false, message: `EHLO failed: ${ehlo.text}`, id: null };
27678
27365
  }
27679
27366
  if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
27680
27367
  const starttls = await sendCommand(socket, "STARTTLS");
27681
27368
  if (starttls.code !== 220) {
27682
27369
  socket.destroy();
27683
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
27370
+ return { success: false, message: `STARTTLS failed: ${starttls.text}`, id: null };
27684
27371
  }
27685
27372
  const plainSocket = socket;
27686
27373
  socket = tls2.connect(
27687
27374
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
27688
27375
  );
27689
- await new Promise((resolve21, reject) => {
27690
- socket.once("secureConnect", resolve21);
27376
+ await new Promise((resolve20, reject) => {
27377
+ socket.once("secureConnect", resolve20);
27691
27378
  socket.once("error", reject);
27692
27379
  });
27693
27380
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
27694
27381
  if (ehlo2.code !== 250) {
27695
27382
  socket.destroy();
27696
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
27383
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}`, id: null };
27697
27384
  }
27698
27385
  }
27699
27386
  if (this.username && this.password) {
27700
27387
  const auth = await sendCommand(socket, "AUTH LOGIN");
27701
27388
  if (auth.code !== 334) {
27702
27389
  socket.destroy();
27703
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
27390
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}`, id: null };
27704
27391
  }
27705
27392
  const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
27706
27393
  if (userResp.code !== 334) {
27707
27394
  socket.destroy();
27708
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
27395
+ return { success: false, message: `AUTH username failed: ${userResp.text}`, id: null };
27709
27396
  }
27710
27397
  const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
27711
27398
  if (passResp.code !== 235) {
27712
27399
  socket.destroy();
27713
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
27400
+ return { success: false, message: `AUTH password failed: ${passResp.text}`, id: null };
27714
27401
  }
27715
27402
  }
27716
27403
  const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
27717
27404
  if (mailFrom.code !== 250) {
27718
27405
  socket.destroy();
27719
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
27406
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}`, id: null };
27720
27407
  }
27721
27408
  for (const recipient of allRecipients) {
27722
27409
  const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
27723
27410
  if (rcpt.code !== 250 && rcpt.code !== 251) {
27724
27411
  socket.destroy();
27725
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
27412
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}`, id: null };
27726
27413
  }
27727
27414
  }
27728
27415
  const dataCmd = await sendCommand(socket, "DATA");
27729
27416
  if (dataCmd.code !== 354) {
27730
27417
  socket.destroy();
27731
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
27418
+ return { success: false, message: `DATA failed: ${dataCmd.text}`, id: null };
27732
27419
  }
27733
27420
  const mimeMessage = buildMimeMessage({
27734
27421
  from: this.fromAddress,
@@ -27747,16 +27434,31 @@ var init_messenger = __esm({
27747
27434
  const endData = await sendCommand(socket, mimeMessage + "\r\n.");
27748
27435
  if (endData.code !== 250) {
27749
27436
  socket.destroy();
27750
- return { success: false, message: `Message delivery failed: ${endData.text}` };
27437
+ return { success: false, message: `Message delivery failed: ${endData.text}`, id: null };
27751
27438
  }
27752
27439
  await sendCommand(socket, "QUIT");
27753
27440
  socket.destroy();
27754
27441
  return { success: true, message: "Email sent successfully", id: messageId };
27755
27442
  } catch (err) {
27756
27443
  const errMsg = err instanceof Error ? err.message : String(err);
27757
- return { success: false, message: `SMTP error: ${errMsg}` };
27444
+ return { success: false, message: `SMTP error: ${errMsg}`, id: null };
27758
27445
  }
27759
27446
  }
27447
+ /**
27448
+ * Render a Frond template STRING and send it as an HTML email (G7, parity with
27449
+ * Python's send_template). Extra send() options (cc, bcc, replyTo, attachments,
27450
+ * headers) pass through. If the Frond package cannot be loaded the raw template
27451
+ * is sent verbatim (matches Python's ImportError fallback) rather than failing.
27452
+ */
27453
+ async sendTemplate(to, subject, template, data = {}, cc, bcc, replyTo, attachments, headers) {
27454
+ let body = template;
27455
+ try {
27456
+ const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
27457
+ body = new Frond2().renderString(template, data);
27458
+ } catch {
27459
+ }
27460
+ return this.send(to, subject, body, true, void 0, cc, bcc, replyTo, attachments, headers);
27461
+ }
27760
27462
  /**
27761
27463
  * Test the SMTP connection without sending an email.
27762
27464
  */
@@ -27765,14 +27467,14 @@ var init_messenger = __esm({
27765
27467
  let socket;
27766
27468
  if (this.port === 465) {
27767
27469
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
27768
- await new Promise((resolve21, reject) => {
27769
- socket.once("secureConnect", resolve21);
27470
+ await new Promise((resolve20, reject) => {
27471
+ socket.once("secureConnect", resolve20);
27770
27472
  socket.once("error", reject);
27771
27473
  });
27772
27474
  } else {
27773
27475
  socket = net3.createConnection({ host: this.host, port: this.port });
27774
- await new Promise((resolve21, reject) => {
27775
- socket.once("connect", resolve21);
27476
+ await new Promise((resolve20, reject) => {
27477
+ socket.once("connect", resolve20);
27776
27478
  socket.once("error", reject);
27777
27479
  });
27778
27480
  }
@@ -27807,14 +27509,14 @@ var init_messenger = __esm({
27807
27509
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
27808
27510
  if (useTls) {
27809
27511
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
27810
- await new Promise((resolve21, reject) => {
27811
- socket.once("secureConnect", resolve21);
27512
+ await new Promise((resolve20, reject) => {
27513
+ socket.once("secureConnect", resolve20);
27812
27514
  socket.once("error", reject);
27813
27515
  });
27814
27516
  } else {
27815
27517
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
27816
- await new Promise((resolve21, reject) => {
27817
- socket.once("connect", resolve21);
27518
+ await new Promise((resolve20, reject) => {
27519
+ socket.once("connect", resolve20);
27818
27520
  socket.once("error", reject);
27819
27521
  });
27820
27522
  }
@@ -27851,7 +27553,7 @@ var init_messenger = __esm({
27851
27553
  }
27852
27554
  try {
27853
27555
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27854
- const searchResp = await imapCommand(socket, "SEARCH ALL");
27556
+ const searchResp = await imapCommand(socket, "UID SEARCH ALL");
27855
27557
  const uids = parseSearchResponse(searchResp);
27856
27558
  if (uids.length === 0) return [];
27857
27559
  uids.reverse();
@@ -27859,8 +27561,8 @@ var init_messenger = __esm({
27859
27561
  if (selected.length === 0) return [];
27860
27562
  const messages = [];
27861
27563
  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));
27564
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
27565
+ messages.push(parseSummary(uid, fetchResp));
27864
27566
  }
27865
27567
  return messages;
27866
27568
  } catch (err) {
@@ -27870,7 +27572,7 @@ var init_messenger = __esm({
27870
27572
  }
27871
27573
  }
27872
27574
  /**
27873
- * Read a single message by sequence number or UID.
27575
+ * Read a single message by its IMAP UID.
27874
27576
  */
27875
27577
  async read(uid, folder = "INBOX") {
27876
27578
  let socket;
@@ -27881,11 +27583,11 @@ var init_messenger = __esm({
27881
27583
  }
27882
27584
  try {
27883
27585
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27884
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
27586
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY[])`);
27885
27587
  if (!/\{\d+\}/.test(fetchResp)) {
27886
27588
  return null;
27887
27589
  }
27888
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
27590
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
27889
27591
  return parseFullMessage(uid, fetchResp);
27890
27592
  } catch (err) {
27891
27593
  throw imapFail("read", err);
@@ -27912,14 +27614,14 @@ var init_messenger = __esm({
27912
27614
  }
27913
27615
  try {
27914
27616
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27915
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
27617
+ const searchResp = await imapCommand(socket, `UID SEARCH ${query}`);
27916
27618
  const uids = parseSearchResponse(searchResp);
27917
27619
  if (uids.length === 0) return [];
27918
27620
  uids.reverse();
27919
27621
  const messages = [];
27920
27622
  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));
27623
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
27624
+ messages.push(parseSummary(uid, fetchResp));
27923
27625
  }
27924
27626
  return messages;
27925
27627
  } catch (err) {
@@ -27929,26 +27631,46 @@ var init_messenger = __esm({
27929
27631
  }
27930
27632
  }
27931
27633
  /**
27932
- * Delete a message by UID.
27634
+ * Delete a message by UID (mark \Deleted, then EXPUNGE).
27635
+ *
27636
+ * `delete` is the one cross-framework name (python/php/ruby/node all spell it
27637
+ * `delete`). `deleteMessage` remains as a DEPRECATED alias for one release.
27933
27638
  */
27934
- async deleteMessage(uid, folder = "INBOX") {
27639
+ async delete(uid, folder = "INBOX") {
27935
27640
  const socket = await this.imapConnect();
27936
27641
  try {
27937
27642
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27938
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
27643
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Deleted)`);
27939
27644
  await imapCommand(socket, "EXPUNGE");
27940
27645
  } finally {
27941
27646
  await this.imapDisconnect(socket);
27942
27647
  }
27943
27648
  }
27649
+ /** @deprecated Use {@link delete} — kept as an alias for one release (G7). */
27650
+ async deleteMessage(uid, folder = "INBOX") {
27651
+ return this.delete(uid, folder);
27652
+ }
27944
27653
  /**
27945
- * Mark a message as read.
27654
+ * Mark a message as read (+FLAGS \Seen).
27946
27655
  */
27947
27656
  async markRead(uid, folder = "INBOX") {
27948
27657
  const socket = await this.imapConnect();
27949
27658
  try {
27950
27659
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27951
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
27660
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
27661
+ } finally {
27662
+ await this.imapDisconnect(socket);
27663
+ }
27664
+ }
27665
+ /**
27666
+ * Mark a message as unread (-FLAGS \Seen) — the inverse of markRead (G7,
27667
+ * parity with Python's mark_unread).
27668
+ */
27669
+ async markUnread(uid, folder = "INBOX") {
27670
+ const socket = await this.imapConnect();
27671
+ try {
27672
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27673
+ await imapCommand(socket, `UID STORE ${uid} -FLAGS (\\Seen)`);
27952
27674
  } finally {
27953
27675
  await this.imapDisconnect(socket);
27954
27676
  }
@@ -27965,7 +27687,7 @@ var init_messenger = __esm({
27965
27687
  }
27966
27688
  try {
27967
27689
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
27968
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
27690
+ const searchResp = await imapCommand(socket, "UID SEARCH UNSEEN");
27969
27691
  return parseSearchResponse(searchResp).length;
27970
27692
  } catch (err) {
27971
27693
  throw imapFail("unread", err);
@@ -28704,17 +28426,17 @@ var init_htmlElement = __esm({
28704
28426
  });
28705
28427
 
28706
28428
  // ../core/src/ai.ts
28707
- import { existsSync as existsSync25, mkdirSync as mkdirSync15, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
28429
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, writeFileSync as writeFileSync14, readFileSync as readFileSync23 } from "node:fs";
28708
28430
  import { homedir } from "node:os";
28709
- import { join as join25, resolve as resolve17, relative as relative10, dirname as dirname11 } from "node:path";
28431
+ import { join as join24, resolve as resolve16, relative as relative10, dirname as dirname10 } from "node:path";
28710
28432
  import { fileURLToPath as fileURLToPath6 } from "node:url";
28711
28433
  import { execSync, execFileSync as execFileSync3 } from "node:child_process";
28712
28434
  import { createInterface } from "node:readline";
28713
28435
  function readVersion() {
28714
28436
  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"));
28437
+ const thisDir = dirname10(fileURLToPath6(import.meta.url));
28438
+ const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
28439
+ const pkg = JSON.parse(readFileSync23(rootPkg, "utf-8"));
28718
28440
  return pkg.version ?? "0.0.0";
28719
28441
  } catch {
28720
28442
  return "0.0.0";
@@ -28763,8 +28485,8 @@ function downloadSkillsSync(jobs) {
28763
28485
  function installSkills(root = ".", targets) {
28764
28486
  const ref = skillsRef();
28765
28487
  const dests = targets ?? [
28766
- join25(resolve17(root), ".claude", "skills"),
28767
- join25(homedir(), ".claude", "skills")
28488
+ join24(resolve16(root), ".claude", "skills"),
28489
+ join24(homedir(), ".claude", "skills")
28768
28490
  ];
28769
28491
  const jobs = [];
28770
28492
  const index = /* @__PURE__ */ new Map();
@@ -28782,9 +28504,9 @@ function installSkills(root = ".", targets) {
28782
28504
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
28783
28505
  skillMdUrl[skill] = `${base}/SKILL.md`;
28784
28506
  for (const dest of dests) {
28785
- add(`${base}/SKILL.md`, join25(dest, skill, "SKILL.md"));
28507
+ add(`${base}/SKILL.md`, join24(dest, skill, "SKILL.md"));
28786
28508
  for (const r of spec.references) {
28787
- add(`${base}/references/${r}`, join25(dest, skill, "references", r));
28509
+ add(`${base}/references/${r}`, join24(dest, skill, "references", r));
28788
28510
  }
28789
28511
  }
28790
28512
  }
@@ -28796,10 +28518,10 @@ function installSkills(root = ".", targets) {
28796
28518
  return installed;
28797
28519
  }
28798
28520
  function isInstalled(root, tool) {
28799
- return existsSync25(join25(resolve17(root), tool.contextFile));
28521
+ return existsSync24(join24(resolve16(root), tool.contextFile));
28800
28522
  }
28801
28523
  function showMenu(root = ".") {
28802
- const r = resolve17(root);
28524
+ const r = resolve16(root);
28803
28525
  console.log("\n Tina4 AI Context Installer\n");
28804
28526
  for (let i = 0; i < AI_TOOLS.length; i++) {
28805
28527
  const tool = AI_TOOLS[i];
@@ -28817,16 +28539,16 @@ function showMenu(root = ".") {
28817
28539
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
28818
28540
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
28819
28541
  console.log();
28820
- return new Promise((resolve21) => {
28542
+ return new Promise((resolve20) => {
28821
28543
  const rl = createInterface({ input: process.stdin, output: process.stdout });
28822
28544
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
28823
28545
  rl.close();
28824
- resolve21(answer.trim());
28546
+ resolve20(answer.trim());
28825
28547
  });
28826
28548
  });
28827
28549
  }
28828
28550
  function installSelected(root, selection) {
28829
- const rootPath = resolve17(root);
28551
+ const rootPath = resolve16(root);
28830
28552
  const created = [];
28831
28553
  let indices;
28832
28554
  let doInstallTina4Ai = false;
@@ -28919,33 +28641,33 @@ function looksLikeOldFrameworkInstall(existing) {
28919
28641
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
28920
28642
  const block = skillBlock(contextFile);
28921
28643
  const [start2, end] = markersFor(contextFile);
28922
- if (!existsSync25(contextPath)) {
28923
- writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28644
+ if (!existsSync24(contextPath)) {
28645
+ writeFileSync14(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28924
28646
  return "Installed";
28925
28647
  }
28926
- const existing = readFileSync24(contextPath, "utf-8");
28648
+ const existing = readFileSync23(contextPath, "utf-8");
28927
28649
  if (hasMarkers(existing, start2, end)) {
28928
- writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
28650
+ writeFileSync14(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
28929
28651
  return "Refreshed skill block in";
28930
28652
  }
28931
28653
  if (looksLikeOldFrameworkInstall(existing)) {
28932
28654
  const head = existing.replace(/^\s+/, "");
28933
28655
  const preamble = existing.slice(0, existing.length - head.length);
28934
28656
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
28935
- writeFileSync15(contextPath, newContent, "utf-8");
28657
+ writeFileSync14(contextPath, newContent, "utf-8");
28936
28658
  return "Migrated (replaced old framework dump in)";
28937
28659
  }
28938
- writeFileSync15(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28660
+ writeFileSync14(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
28939
28661
  return "Appended skill block to";
28940
28662
  }
28941
28663
  function installForTool(root, tool, context) {
28942
28664
  const created = [];
28943
- const contextPath = join25(root, tool.contextFile);
28665
+ const contextPath = join24(root, tool.contextFile);
28944
28666
  if (tool.configDir) {
28945
- mkdirSync15(join25(root, tool.configDir), { recursive: true });
28667
+ mkdirSync14(join24(root, tool.configDir), { recursive: true });
28946
28668
  }
28947
- const parentDir = dirname11(contextPath);
28948
- mkdirSync15(parentDir, { recursive: true });
28669
+ const parentDir = dirname10(contextPath);
28670
+ mkdirSync14(parentDir, { recursive: true });
28949
28671
  const action = writeOrMerge(contextPath, tool.contextFile, context);
28950
28672
  const rel = relative10(root, contextPath);
28951
28673
  created.push(rel);
@@ -28980,7 +28702,7 @@ function installTina4Ai() {
28980
28702
  function installClaudeSkills(root) {
28981
28703
  const created = [];
28982
28704
  for (const skill of installSkills(root)) {
28983
- created.push(join25(".claude", "skills", skill));
28705
+ created.push(join24(".claude", "skills", skill));
28984
28706
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
28985
28707
  }
28986
28708
  return created;
@@ -29321,11 +29043,11 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
29321
29043
  }
29322
29044
  function generateClaudeCodeContext() {
29323
29045
  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");
29046
+ const thisDir = dirname10(fileURLToPath6(import.meta.url));
29047
+ const repoRoot = resolve16(thisDir, "..", "..", "..");
29048
+ const claudeMdPath = join24(repoRoot, "CLAUDE.md");
29049
+ if (existsSync24(claudeMdPath)) {
29050
+ return readFileSync23(claudeMdPath, "utf-8");
29329
29051
  }
29330
29052
  } catch {
29331
29053
  }
@@ -29841,16 +29563,8 @@ var init_rabbitmqBackend = __esm({
29841
29563
  process.stdout.write(String(msgCount));
29842
29564
  closeConnection();
29843
29565
  }
29844
- else if (operation === "purge") {
29845
- // Queue.Purge
29846
- const qBuf = Buffer.from(queueName, "utf-8");
29847
- const purgePayload = Buffer.alloc(4 + qBuf.length);
29848
- purgePayload.writeUInt16BE(0, 0);
29849
- purgePayload.writeUInt8(qBuf.length, 2);
29850
- qBuf.copy(purgePayload, 3);
29851
- purgePayload.writeUInt8(0, 3 + qBuf.length); // no-wait=false
29852
- sendMethod(1, 50, 30, purgePayload);
29853
- }
29566
+ // No "purge" operation: clear()/purge() refuse by name (ADR-0022),
29567
+ // so nothing ever sends Queue.Purge and the drain path is gone.
29854
29568
  }
29855
29569
  else if (classId === 60 && methodId === 71) {
29856
29570
  // Basic.Get-Ok \u2014 message body will follow in content frames
@@ -29861,11 +29575,6 @@ var init_rabbitmqBackend = __esm({
29861
29575
  process.stdout.write("__EMPTY__");
29862
29576
  closeConnection();
29863
29577
  }
29864
- else if (classId === 50 && methodId === 31) {
29865
- // Queue.Purge-Ok
29866
- process.stdout.write("__PURGED__");
29867
- closeConnection();
29868
- }
29869
29578
  else if (classId === 10 && methodId === 50) {
29870
29579
  // Connection.Close (server-initiated, e.g. a channel/protocol error)
29871
29580
  // \u2192 send Connection.Close-Ok and exit non-zero so the caller sees the
@@ -29995,8 +29704,15 @@ var init_rabbitmqBackend = __esm({
29995
29704
  const num = parseInt(result, 10);
29996
29705
  return isNaN(num) ? 0 : num;
29997
29706
  }
29998
- clear(queue) {
29999
- this.execSync("purge", queue);
29707
+ clear(_queue) {
29708
+ throw new Error(
29709
+ "The rabbitmq queue backend cannot perform clear(): RabbitMQ cannot address messages by status (basic.get pops the head of the queue), so a status-addressed clear would have to drain the entire live queue and destroy pending work. Use the file or mongodb backend."
29710
+ );
29711
+ }
29712
+ purge(_queue, _status) {
29713
+ throw new Error(
29714
+ "The rabbitmq queue backend cannot perform purge(): RabbitMQ cannot address messages by status (basic.get pops the head of the queue), so a status-addressed purge would have to drain the entire live queue and destroy pending work. Use the file or mongodb backend."
29715
+ );
30000
29716
  }
30001
29717
  };
30002
29718
  }
@@ -30558,6 +30274,14 @@ var init_kafkaBackend = __esm({
30558
30274
  return 0;
30559
30275
  }
30560
30276
  clear(_queue) {
30277
+ throw new Error(
30278
+ "The kafka queue backend cannot perform clear(): Kafka has no notion of job status and cannot delete records on demand. A log is read in offset order and records leave only by retention. Use the file or mongodb backend."
30279
+ );
30280
+ }
30281
+ purge(_queue, _status) {
30282
+ throw new Error(
30283
+ "The kafka queue backend cannot perform purge(): Kafka has no notion of job status to purge by. A log is read in offset order and records leave only by retention. Use the file or mongodb backend."
30284
+ );
30561
30285
  }
30562
30286
  };
30563
30287
  }
@@ -31317,7 +31041,6 @@ __export(src_exports2, {
31317
31041
  RouteRef: () => RouteRef,
31318
31042
  Router: () => Router,
31319
31043
  SafeString: () => SafeString2,
31320
- ScssCompiler: () => ScssCompiler,
31321
31044
  SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
31322
31045
  ServiceRunner: () => ServiceRunner,
31323
31046
  Session: () => Session,
@@ -31514,7 +31237,6 @@ var init_src2 = __esm({
31514
31237
  init_session();
31515
31238
  init_i18n();
31516
31239
  init_fakeData();
31517
- init_scss();
31518
31240
  init_queue();
31519
31241
  init_job();
31520
31242
  init_mqtt();
@@ -32073,8 +31795,8 @@ __export(sqlite_exports, {
32073
31795
  SQLiteAdapter: () => SQLiteAdapter
32074
31796
  });
32075
31797
  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";
31798
+ import { mkdirSync as mkdirSync15 } from "node:fs";
31799
+ import { dirname as dirname11, isAbsolute as isAbsolute4, join as join25, resolve as resolve17 } from "node:path";
32078
31800
  function isIdentifier(str) {
32079
31801
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
32080
31802
  }
@@ -32107,13 +31829,13 @@ function resolveSqlitePath(dbPath) {
32107
31829
  if (dbPath === ":memory:") return dbPath;
32108
31830
  let path8 = dbPath;
32109
31831
  if (!isAbsolute4(path8)) {
32110
- path8 = join26(process.cwd(), path8);
32111
- mkdirSync16(dirname12(path8), { recursive: true });
31832
+ path8 = join25(process.cwd(), path8);
31833
+ mkdirSync15(dirname11(path8), { recursive: true });
32112
31834
  } else {
32113
- const cwd = resolve18(process.cwd());
32114
- const abs = resolve18(path8);
31835
+ const cwd = resolve17(process.cwd());
31836
+ const abs = resolve17(path8);
32115
31837
  if (abs.startsWith(cwd + "/") || abs === cwd) {
32116
- mkdirSync16(dirname12(abs), { recursive: true });
31838
+ mkdirSync15(dirname11(abs), { recursive: true });
32117
31839
  }
32118
31840
  }
32119
31841
  return path8;
@@ -32461,7 +32183,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
32461
32183
  const elapsedMs = () => performance.now() - startedAt;
32462
32184
  if (budgetMs === null) return attempt();
32463
32185
  const started = attempt();
32464
- return new Promise((resolve21, reject) => {
32186
+ return new Promise((resolve20, reject) => {
32465
32187
  let expired = false;
32466
32188
  const timer = setTimeout(() => {
32467
32189
  expired = true;
@@ -32471,7 +32193,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
32471
32193
  (arrived) => {
32472
32194
  clearTimeout(timer);
32473
32195
  if (expired) abandon?.(arrived);
32474
- else resolve21(arrived);
32196
+ else resolve20(arrived);
32475
32197
  },
32476
32198
  (failure) => {
32477
32199
  clearTimeout(timer);
@@ -33028,10 +32750,10 @@ var init_mysql = __esm({
33028
32750
  ...timeoutOption
33029
32751
  });
33030
32752
  }
33031
- return new Promise((resolve21, reject) => {
32753
+ return new Promise((resolve20, reject) => {
33032
32754
  this.connection.connect((err) => {
33033
32755
  if (err) reject(err);
33034
- else resolve21();
32756
+ else resolve20();
33035
32757
  });
33036
32758
  });
33037
32759
  },
@@ -33053,10 +32775,10 @@ var init_mysql = __esm({
33053
32775
  }
33054
32776
  }
33055
32777
  queryPromise(sql, params) {
33056
- return new Promise((resolve21, reject) => {
32778
+ return new Promise((resolve20, reject) => {
33057
32779
  this.connection.query(sql, params ?? [], (err, results) => {
33058
32780
  if (err) reject(err);
33059
- else resolve21(results);
32781
+ else resolve20(results);
33060
32782
  });
33061
32783
  });
33062
32784
  }
@@ -33426,11 +33148,11 @@ var init_mssql = __esm({
33426
33148
  };
33427
33149
  }
33428
33150
  await withConnectTimeout(
33429
- () => new Promise((resolve21, reject) => {
33151
+ () => new Promise((resolve20, reject) => {
33430
33152
  this.connection = new Connection(tediousConfig);
33431
33153
  this.connection.on("connect", (err) => {
33432
33154
  if (err) reject(err);
33433
- else resolve21();
33155
+ else resolve20();
33434
33156
  });
33435
33157
  this.connection.connect();
33436
33158
  }),
@@ -33475,11 +33197,11 @@ var init_mssql = __esm({
33475
33197
  const tediousModule = requireTedious();
33476
33198
  const Request = tediousModule.Request;
33477
33199
  const TYPES = tediousModule.TYPES;
33478
- return new Promise((resolve21, reject) => {
33200
+ return new Promise((resolve20, reject) => {
33479
33201
  const rows = [];
33480
33202
  const request = new Request(sql, (err, rowCount) => {
33481
33203
  if (err) reject(err);
33482
- else resolve21({ rows, rowCount });
33204
+ else resolve20({ rows, rowCount });
33483
33205
  });
33484
33206
  if (params) {
33485
33207
  params.forEach((p, i) => {
@@ -33682,8 +33404,8 @@ var init_mssql = __esm({
33682
33404
  throw new Error("Use startTransactionAsync() for MSSQL.");
33683
33405
  }
33684
33406
  async startTransactionAsync() {
33685
- await new Promise((resolve21, reject) => {
33686
- this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
33407
+ await new Promise((resolve20, reject) => {
33408
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve20());
33687
33409
  });
33688
33410
  this._inTransaction = true;
33689
33411
  }
@@ -33691,8 +33413,8 @@ var init_mssql = __esm({
33691
33413
  throw new Error("Use commitAsync() for MSSQL.");
33692
33414
  }
33693
33415
  async commitAsync() {
33694
- await new Promise((resolve21, reject) => {
33695
- this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
33416
+ await new Promise((resolve20, reject) => {
33417
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve20());
33696
33418
  });
33697
33419
  this._inTransaction = false;
33698
33420
  }
@@ -33700,8 +33422,8 @@ var init_mssql = __esm({
33700
33422
  throw new Error("Use rollbackAsync() for MSSQL.");
33701
33423
  }
33702
33424
  async rollbackAsync() {
33703
- await new Promise((resolve21, reject) => {
33704
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
33425
+ await new Promise((resolve20, reject) => {
33426
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve20());
33705
33427
  });
33706
33428
  this._inTransaction = false;
33707
33429
  }
@@ -33971,8 +33693,8 @@ var init_firebird = __esm({
33971
33693
  fbConfig.database = normalizeFirebirdDbIdentifier(fbConfig.database);
33972
33694
  }
33973
33695
  this.db = await withConnectTimeout(
33974
- () => new Promise((resolve21, reject) => {
33975
- fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve21(db));
33696
+ () => new Promise((resolve20, reject) => {
33697
+ fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve20(db));
33976
33698
  }),
33977
33699
  connectTimeoutMillis(),
33978
33700
  fbConfig.host,
@@ -34039,20 +33761,20 @@ var init_firebird = __esm({
34039
33761
  return this.transaction ?? this.db;
34040
33762
  }
34041
33763
  queryPromise(sql, params) {
34042
- return new Promise((resolve21, reject) => {
33764
+ return new Promise((resolve20, reject) => {
34043
33765
  const translated = this.translateSql(sql);
34044
33766
  this.statementHandle().query(translated, params ?? [], (err, result) => {
34045
33767
  if (err) reject(err);
34046
- else resolve21(result ?? []);
33768
+ else resolve20(result ?? []);
34047
33769
  });
34048
33770
  });
34049
33771
  }
34050
33772
  executePromise(sql, params) {
34051
- return new Promise((resolve21, reject) => {
33773
+ return new Promise((resolve20, reject) => {
34052
33774
  const translated = this.translateSql(sql);
34053
33775
  this.statementHandle().execute(translated, params ?? [], (err) => {
34054
33776
  if (err) reject(err);
34055
- else resolve21();
33777
+ else resolve20();
34056
33778
  });
34057
33779
  });
34058
33780
  }
@@ -34168,12 +33890,12 @@ var init_firebird = __esm({
34168
33890
  }
34169
33891
  async startTransactionAsync() {
34170
33892
  this.ensureConnected();
34171
- await new Promise((resolve21, reject) => {
33893
+ await new Promise((resolve20, reject) => {
34172
33894
  this.db.transaction(0, (err, transaction) => {
34173
33895
  if (err) reject(err);
34174
33896
  else {
34175
33897
  this.transaction = transaction;
34176
- resolve21();
33898
+ resolve20();
34177
33899
  }
34178
33900
  });
34179
33901
  });
@@ -34183,12 +33905,12 @@ var init_firebird = __esm({
34183
33905
  }
34184
33906
  async commitAsync() {
34185
33907
  if (!this.transaction) throw new Error("No active transaction to commit.");
34186
- await new Promise((resolve21, reject) => {
33908
+ await new Promise((resolve20, reject) => {
34187
33909
  this.transaction.commit((err) => {
34188
33910
  if (err) reject(err);
34189
33911
  else {
34190
33912
  this.transaction = null;
34191
- resolve21();
33913
+ resolve20();
34192
33914
  }
34193
33915
  });
34194
33916
  });
@@ -34198,12 +33920,12 @@ var init_firebird = __esm({
34198
33920
  }
34199
33921
  async rollbackAsync() {
34200
33922
  if (!this.transaction) throw new Error("No active transaction to rollback.");
34201
- await new Promise((resolve21, reject) => {
33923
+ await new Promise((resolve20, reject) => {
34202
33924
  this.transaction.rollback((err) => {
34203
33925
  if (err) reject(err);
34204
33926
  else {
34205
33927
  this.transaction = null;
34206
- resolve21();
33928
+ resolve20();
34207
33929
  }
34208
33930
  });
34209
33931
  });
@@ -35264,6 +34986,7 @@ __export(database_exports, {
35264
34986
  getNamedAdapter: () => getNamedAdapter,
35265
34987
  initDatabase: () => initDatabase,
35266
34988
  parseDatabaseUrl: () => parseDatabaseUrl,
34989
+ probeTotal: () => probeTotal,
35267
34990
  resetRequestCaches: () => resetRequestCaches2,
35268
34991
  resolveDbPool: () => resolveDbPool,
35269
34992
  setAdapter: () => setAdapter,
@@ -35317,6 +35040,29 @@ async function adapterCreateTable(adapter, name, columns) {
35317
35040
  if (adapter.createTableAsync) await adapter.createTableAsync(name, columns);
35318
35041
  else adapter.createTable(name, columns);
35319
35042
  }
35043
+ async function probeTotal(adapter, sql, params, limit) {
35044
+ if (limit === void 0 || limit <= 0) return void 0;
35045
+ try {
35046
+ const alias = adapter.countSubqueryAlias;
35047
+ const suffix = alias ? ` AS ${alias}` : "";
35048
+ const rows = await adapterFetch(
35049
+ adapter,
35050
+ `SELECT COUNT(*) AS tina4_total FROM (${sql}
35051
+ )${suffix}`,
35052
+ params,
35053
+ void 0,
35054
+ void 0,
35055
+ true
35056
+ );
35057
+ const row = Array.isArray(rows) ? rows[0] : void 0;
35058
+ if (!row) return void 0;
35059
+ const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
35060
+ const n = Number(value);
35061
+ return Number.isFinite(n) ? n : void 0;
35062
+ } catch {
35063
+ return void 0;
35064
+ }
35065
+ }
35320
35066
  function extractLastInsertId(result) {
35321
35067
  if (result && typeof result === "object") {
35322
35068
  const r = result;
@@ -35775,65 +35521,13 @@ var init_database = __esm({
35775
35521
  try {
35776
35522
  const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
35777
35523
  this.lastError = null;
35778
- const total = await this.countProbe(adapter, sql, params, limit);
35524
+ const total = await probeTotal(adapter, sql, params, limit);
35779
35525
  return new DatabaseResult(rows, void 0, total, limit, offset, adapter, sql);
35780
35526
  } catch (e) {
35781
35527
  this.lastError = e?.message ?? String(e);
35782
35528
  throw e;
35783
35529
  }
35784
35530
  }
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
35531
  /**
35838
35532
  * Fetch a single row or null.
35839
35533
  *
@@ -36519,18 +36213,18 @@ var init_database = __esm({
36519
36213
  });
36520
36214
 
36521
36215
  // 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";
36216
+ import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
36217
+ import { join as join26, extname as extname8 } from "node:path";
36524
36218
  async function discoverModels(modelsDir) {
36525
36219
  const models = [];
36526
36220
  let files;
36527
36221
  try {
36528
- files = readdirSync18(modelsDir);
36222
+ files = readdirSync17(modelsDir);
36529
36223
  } catch {
36530
36224
  return models;
36531
36225
  }
36532
36226
  for (const file of files) {
36533
- const filePath = join27(modelsDir, file);
36227
+ const filePath = join26(modelsDir, file);
36534
36228
  const stat = statSync17(filePath);
36535
36229
  if (!stat.isFile()) continue;
36536
36230
  const ext = extname8(file);
@@ -36545,6 +36239,10 @@ async function discoverModels(modelsDir) {
36545
36239
  }
36546
36240
  const definition = {
36547
36241
  tableName: ModelClass.tableName,
36242
+ // The class name is the type name a generated OpenAPI client wants
36243
+ // (`Item`, not `items`). Carry it so Swagger keys components.schemas by
36244
+ // it. A model exported as `default` keeps its declared class name here.
36245
+ className: typeof ModelClass.name === "string" && ModelClass.name ? ModelClass.name : void 0,
36548
36246
  fields: ModelClass.fields,
36549
36247
  fieldMapping: ModelClass.fieldMapping,
36550
36248
  softDelete: ModelClass.softDelete ?? false,
@@ -36568,8 +36266,8 @@ var init_model = __esm({
36568
36266
  });
36569
36267
 
36570
36268
  // 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";
36269
+ import { existsSync as existsSync25, readdirSync as readdirSync18, readFileSync as readFileSync24, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15 } from "node:fs";
36270
+ import { join as join27, resolve as resolve18 } from "node:path";
36573
36271
  function unwrapAdapter(db) {
36574
36272
  let cur = db;
36575
36273
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -36877,16 +36575,16 @@ async function rollback(migrationsDir, delimiter2) {
36877
36575
  }
36878
36576
  return rolledBack2;
36879
36577
  }
36880
- const dir = resolve19(migrationsDir ?? "migrations");
36578
+ const dir = resolve18(migrationsDir ?? "migrations");
36881
36579
  const delim = delimiter2 ?? ";";
36882
36580
  const db = getAdapter();
36883
36581
  const migrations = await getLastBatchMigrations();
36884
36582
  const rolledBack = [];
36885
36583
  for (const migration of migrations) {
36886
36584
  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();
36585
+ const downPath = join27(dir, downFile);
36586
+ if (existsSync25(downPath)) {
36587
+ const sqlContent = readFileSync24(downPath, "utf-8").trim();
36890
36588
  if (sqlContent) {
36891
36589
  const statements = splitStatements(sqlContent, delim);
36892
36590
  try {
@@ -37046,15 +36744,15 @@ function warnUnprefixedMigrations(files) {
37046
36744
  }
37047
36745
  async function migrate(adapter, options) {
37048
36746
  const db = adapter ?? getAdapter();
37049
- const dir = resolve19(options?.migrationsDir ?? "migrations");
36747
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
37050
36748
  const delimiter2 = options?.delimiter ?? ";";
37051
36749
  const result = { applied: [], skipped: [], failed: [] };
37052
- if (!existsSync26(dir)) {
36750
+ if (!existsSync25(dir)) {
37053
36751
  return result;
37054
36752
  }
37055
36753
  await ensureMigrationTableOn(db);
37056
36754
  const files = sortMigrationFiles(
37057
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
36755
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37058
36756
  );
37059
36757
  if (files.length === 0) return result;
37060
36758
  warnUnprefixedMigrations(files);
@@ -37085,7 +36783,7 @@ async function migrate(adapter, options) {
37085
36783
  result.skipped.push(file);
37086
36784
  continue;
37087
36785
  }
37088
- const sqlContent = readFileSync25(join28(dir, file), "utf-8").trim();
36786
+ const sqlContent = readFileSync24(join27(dir, file), "utf-8").trim();
37089
36787
  if (!sqlContent) {
37090
36788
  result.skipped.push(file);
37091
36789
  continue;
@@ -37120,21 +36818,21 @@ async function migrate(adapter, options) {
37120
36818
  }
37121
36819
  async function status(adapter, options) {
37122
36820
  const db = adapter ?? getAdapter();
37123
- const dir = resolve19(options?.migrationsDir ?? "migrations");
36821
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
37124
36822
  const result = { completed: [], pending: [] };
37125
- if (!existsSync26(dir)) {
36823
+ if (!existsSync25(dir)) {
37126
36824
  return result;
37127
36825
  }
37128
36826
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
37129
36827
  const files2 = sortMigrationFiles(
37130
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
36828
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37131
36829
  );
37132
36830
  result.pending = files2;
37133
36831
  return result;
37134
36832
  }
37135
36833
  await ensureMigrationTableOn(db);
37136
36834
  const files = sortMigrationFiles(
37137
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
36835
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37138
36836
  );
37139
36837
  const appliedNames = /* @__PURE__ */ new Set();
37140
36838
  try {
@@ -37159,12 +36857,18 @@ async function status(adapter, options) {
37159
36857
  return result;
37160
36858
  }
37161
36859
  async function createMigration(description, options) {
37162
- if (options?.kind === "class") {
36860
+ const kind = (options?.kind ?? "sql").trim().toLowerCase();
36861
+ if (!["sql", "code", "class"].includes(kind)) {
36862
+ throw new Error(
36863
+ `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.`
36864
+ );
36865
+ }
36866
+ if (kind === "code" || kind === "class") {
37163
36867
  return createClassMigration(description, options);
37164
36868
  }
37165
- const dir = resolve19(options?.migrationsDir ?? "migrations");
37166
- if (!existsSync26(dir)) {
37167
- mkdirSync17(dir, { recursive: true });
36869
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
36870
+ if (!existsSync25(dir)) {
36871
+ mkdirSync16(dir, { recursive: true });
37168
36872
  }
37169
36873
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
37170
36874
  const now = /* @__PURE__ */ new Date();
@@ -37178,8 +36882,8 @@ async function createMigration(description, options) {
37178
36882
  ].join("");
37179
36883
  const upFileName = `${timestamp}_${safeName}.sql`;
37180
36884
  const downFileName = `${timestamp}_${safeName}.down.sql`;
37181
- const upPath = join28(dir, upFileName);
37182
- const downPath = join28(dir, downFileName);
36885
+ const upPath = join27(dir, upFileName);
36886
+ const downPath = join27(dir, downFileName);
37183
36887
  const upTemplate = `-- Migration: ${description}
37184
36888
  -- Created: ${now.toISOString()}
37185
36889
 
@@ -37188,14 +36892,14 @@ async function createMigration(description, options) {
37188
36892
  -- Created: ${now.toISOString()}
37189
36893
 
37190
36894
  `;
37191
- writeFileSync16(upPath, upTemplate, "utf-8");
37192
- writeFileSync16(downPath, downTemplate, "utf-8");
36895
+ writeFileSync15(upPath, upTemplate, "utf-8");
36896
+ writeFileSync15(downPath, downTemplate, "utf-8");
37193
36897
  return { upPath, downPath };
37194
36898
  }
37195
36899
  async function createClassMigration(description, options) {
37196
- const dir = resolve19(options?.migrationsDir ?? "migrations");
37197
- if (!existsSync26(dir)) {
37198
- mkdirSync17(dir, { recursive: true });
36900
+ const dir = resolve18(options?.migrationsDir ?? "migrations");
36901
+ if (!existsSync25(dir)) {
36902
+ mkdirSync16(dir, { recursive: true });
37199
36903
  }
37200
36904
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
37201
36905
  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 +36913,7 @@ async function createClassMigration(description, options) {
37209
36913
  String(now.getSeconds()).padStart(2, "0")
37210
36914
  ].join("");
37211
36915
  const fileName = `${timestamp}_${safeName}.ts`;
37212
- const filePath = join28(dir, fileName);
36916
+ const filePath = join27(dir, fileName);
37213
36917
  const content = `// Migration: ${description}
37214
36918
  // Created: ${now.toISOString()}
37215
36919
 
@@ -37225,7 +36929,7 @@ export class ${className} {
37225
36929
  }
37226
36930
  }
37227
36931
  `;
37228
- writeFileSync16(filePath, content, "utf-8");
36932
+ writeFileSync15(filePath, content, "utf-8");
37229
36933
  return filePath;
37230
36934
  }
37231
36935
  var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
@@ -37286,15 +36990,13 @@ var init_migration = __esm({
37286
36990
  * Scaffold a new migration file.
37287
36991
  *
37288
36992
  * kind="sql" — creates {timestamp}_{description}.sql + .down.sql (default)
37289
- * kind="class" — creates {timestamp}_{description}.ts with a TypeScript class template
36993
+ * kind="code" — creates {timestamp}_{description}.ts with a TypeScript class
36994
+ * template. "class" is accepted as a legacy alias.
37290
36995
  *
37291
36996
  * Returns the path to the created up file (or class file).
37292
36997
  */
37293
36998
  async create(description, kind = "sql") {
37294
- if (kind === "class") {
37295
- return createClassMigration(description, { migrationsDir: this.dir });
37296
- }
37297
- return createMigration(description, { migrationsDir: this.dir });
36999
+ return createMigration(description, { migrationsDir: this.dir, kind });
37298
37000
  }
37299
37001
  /** Return list of completed (applied) migration filenames. */
37300
37002
  async getApplied() {
@@ -37308,10 +37010,10 @@ var init_migration = __esm({
37308
37010
  }
37309
37011
  /** Return sorted list of all migration files on disk (excludes .down.sql). */
37310
37012
  getFiles() {
37311
- const dir = resolve19(this.dir);
37312
- if (!existsSync26(dir)) return [];
37013
+ const dir = resolve18(this.dir);
37014
+ if (!existsSync25(dir)) return [];
37313
37015
  return sortMigrationFiles(
37314
- readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37016
+ readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
37315
37017
  );
37316
37018
  }
37317
37019
  };
@@ -37537,15 +37239,8 @@ function generateCrudRoutes(models, options = {}) {
37537
37239
  const total = Number(countRow[0]?.total ?? 0);
37538
37240
  const limit = qp.limit ?? 100;
37539
37241
  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
- });
37242
+ const offset = (page - 1) * limit;
37243
+ res.json(new DatabaseResult(rows, void 0, total, limit, offset).toPaginate());
37549
37244
  }
37550
37245
  });
37551
37246
  routes.push({
@@ -37699,6 +37394,7 @@ var init_autoCrud = __esm({
37699
37394
  "src/autoCrud.ts"() {
37700
37395
  "use strict";
37701
37396
  init_database();
37397
+ init_databaseResult();
37702
37398
  init_query();
37703
37399
  init_validation();
37704
37400
  AutoCrud = class _AutoCrud {
@@ -37953,17 +37649,19 @@ var init_queryBuilder = __esm({
37953
37649
  this.ensureDb();
37954
37650
  const sql = this.toSql();
37955
37651
  const allParams = [...this.params, ...this.havingParams];
37652
+ const queryParams = allParams.length > 0 ? allParams : void 0;
37956
37653
  const rows = await adapterFetch(
37957
37654
  this.db,
37958
37655
  sql,
37959
- allParams.length > 0 ? allParams : void 0,
37656
+ queryParams,
37960
37657
  this.limitVal,
37961
37658
  this.offsetVal
37962
37659
  );
37660
+ const total = await probeTotal(this.db, sql, queryParams, this.limitVal);
37963
37661
  return new DatabaseResult(
37964
37662
  rows,
37965
37663
  void 0,
37966
- void 0,
37664
+ total,
37967
37665
  this.limitVal,
37968
37666
  this.offsetVal,
37969
37667
  this.db,
@@ -39828,8 +39526,8 @@ var init_seeder = __esm({
39828
39526
  // src/docstore.ts
39829
39527
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
39830
39528
  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";
39529
+ import { mkdirSync as mkdirSync17 } from "node:fs";
39530
+ import { dirname as dirname12, isAbsolute as isAbsolute5, join as join28 } from "node:path";
39833
39531
  function iso(d) {
39834
39532
  return d.toISOString();
39835
39533
  }
@@ -40094,8 +39792,8 @@ function resolveStorePath(dbPath) {
40094
39792
  if (dbPath === ":memory:") return dbPath;
40095
39793
  let path8 = dbPath;
40096
39794
  if (!isAbsolute5(path8)) {
40097
- path8 = join29(process.cwd(), path8);
40098
- mkdirSync18(dirname13(path8), { recursive: true });
39795
+ path8 = join28(process.cwd(), path8);
39796
+ mkdirSync17(dirname12(path8), { recursive: true });
40099
39797
  }
40100
39798
  return path8;
40101
39799
  }
@@ -40609,8 +40307,8 @@ var init_attachment = __esm({
40609
40307
 
40610
40308
  // src/realtime/storage.ts
40611
40309
  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";
40310
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, statSync as statSync18 } from "node:fs";
40311
+ import { resolve as resolve19, sep as sep3 } from "node:path";
40614
40312
  import { createRequire as createRequire8 } from "node:module";
40615
40313
  function storageKey(filename = "") {
40616
40314
  let ext = "";
@@ -40646,23 +40344,23 @@ var init_storage = __esm({
40646
40344
  LocalStorage = class {
40647
40345
  directory;
40648
40346
  constructor(directory) {
40649
- this.directory = resolve20(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
40650
- mkdirSync19(this.directory, { recursive: true });
40347
+ this.directory = resolve19(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
40348
+ mkdirSync18(this.directory, { recursive: true });
40651
40349
  }
40652
40350
  // Resolve inside the root and reject any traversal attempt.
40653
40351
  pathFor(key) {
40654
- const target = resolve20(this.directory, key);
40352
+ const target = resolve19(this.directory, key);
40655
40353
  if (target !== this.directory && !target.startsWith(this.directory + sep3)) {
40656
40354
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
40657
40355
  }
40658
40356
  return target;
40659
40357
  }
40660
40358
  put(key, data) {
40661
- writeFileSync17(this.pathFor(key), data);
40359
+ writeFileSync16(this.pathFor(key), data);
40662
40360
  }
40663
40361
  get(key) {
40664
40362
  try {
40665
- return readFileSync26(this.pathFor(key));
40363
+ return readFileSync25(this.pathFor(key));
40666
40364
  } catch {
40667
40365
  return null;
40668
40366
  }
@@ -41072,7 +40770,6 @@ __export(index_exports, {
41072
40770
  DatabaseUrl: () => DatabaseUrl,
41073
40771
  DocStoreDriverMissing: () => DocStoreDriverMissing,
41074
40772
  FakeData: () => FakeData2,
41075
- FetchResult: () => FetchResult,
41076
40773
  FirebirdAdapter: () => FirebirdAdapter,
41077
40774
  InvalidId: () => InvalidId,
41078
40775
  LocalStorage: () => LocalStorage,
@@ -41171,7 +40868,6 @@ __export(index_exports, {
41171
40868
  });
41172
40869
  var init_index = __esm({
41173
40870
  "src/index.ts"() {
41174
- init_types();
41175
40871
  init_databaseResult();
41176
40872
  init_database();
41177
40873
  init_database();
@@ -41212,7 +40908,6 @@ export {
41212
40908
  DatabaseUrl,
41213
40909
  DocStoreDriverMissing,
41214
40910
  FakeData2 as FakeData,
41215
- FetchResult,
41216
40911
  FirebirdAdapter,
41217
40912
  InvalidId,
41218
40913
  LocalStorage,