@yrest/cli 0.7.0 → 0.8.1

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.
@@ -106,7 +106,7 @@ function registerInit(program2) {
106
106
 
107
107
  // src/cli/commands/serve.ts
108
108
  import { watchFile } from "fs";
109
- import { join, resolve as resolve3 } from "path";
109
+ import { join as join2, resolve as resolve3 } from "path";
110
110
 
111
111
  // src/storage/yrestStorage.ts
112
112
  import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
@@ -196,6 +196,11 @@ function createYrestStorage(filePath) {
196
196
  import Fastify from "fastify";
197
197
  import cors from "@fastify/cors";
198
198
 
199
+ // src/router/templates/about.template.ts
200
+ import { readFileSync as readFileSync2 } from "fs";
201
+ import { dirname as dirname2, join } from "path";
202
+ import { fileURLToPath } from "url";
203
+
199
204
  // src/utils/interpolate.ts
200
205
  import { randomUUID as randomUUID2 } from "crypto";
201
206
  function getPath(obj, path) {
@@ -243,6 +248,15 @@ function hasTemplates(value) {
243
248
  }
244
249
 
245
250
  // src/router/templates/about.template.ts
251
+ var _dir = dirname2(fileURLToPath(import.meta.url));
252
+ var LOGO_SRC = (() => {
253
+ try {
254
+ const buf = readFileSync2(join(_dir, "../../assets/logo-color.png"));
255
+ return `data:image/png;base64,${buf.toString("base64")}`;
256
+ } catch {
257
+ return "";
258
+ }
259
+ })();
246
260
  var METHOD_COLOR = {
247
261
  GET: "#3fb950",
248
262
  POST: "#58a6ff",
@@ -251,8 +265,11 @@ var METHOD_COLOR = {
251
265
  DELETE: "#f85149",
252
266
  fn: "#f0883e"
253
267
  };
268
+ function escapeHtml(str) {
269
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
270
+ }
254
271
  function badge(label, color, bg) {
255
- return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${label}</span>`;
272
+ return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${escapeHtml(label)}</span>`;
256
273
  }
257
274
  function methodBadge(method) {
258
275
  const color = METHOD_COLOR[method] ?? "#7d8590";
@@ -262,7 +279,7 @@ function endpointRow(method, path, desc) {
262
279
  return `
263
280
  <tr>
264
281
  <td class="method-cell">${methodBadge(method)}</td>
265
- <td class="path-cell"><code>${path}</code></td>
282
+ <td class="path-cell"><code>${escapeHtml(path)}</code></td>
266
283
  <td class="desc-cell">${desc}</td>
267
284
  </tr>`;
268
285
  }
@@ -296,7 +313,7 @@ function resourceAccordion(name, base, isOpen) {
296
313
  return `
297
314
  <details class="resource-card" ${isOpen ? "open" : ""}>
298
315
  <summary>
299
- <span class="resource-name">/${name}</span>
316
+ <span class="resource-name">/${escapeHtml(name)}</span>
300
317
  <span class="route-count">6 routes</span>
301
318
  </summary>
302
319
  <table>
@@ -380,7 +397,7 @@ curl -X POST ${host}/_snapshot/reset`
380
397
  }
381
398
  if (firstCustomRoute) {
382
399
  const method = firstCustomRoute.method?.toUpperCase() ?? "GET";
383
- const fullPath = `${host}${base}${firstCustomRoute.path}`;
400
+ const fullPath = `${host}${base}${escapeHtml(firstCustomRoute.path ?? "")}`;
384
401
  const curlFlag = method === "GET" ? "" : `-X ${method} `;
385
402
  examples.push(`# Custom route
386
403
  curl ${curlFlag}${fullPath}`);
@@ -401,6 +418,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
401
418
  modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
402
419
  if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
403
420
  if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
421
+ if (options.idStrategy !== "increment")
422
+ modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
404
423
  const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
405
424
  const nestedRows = [];
406
425
  for (const [child, fields] of Object.entries(relations)) {
@@ -443,6 +462,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
443
462
  ${customRoutes.map((r) => {
444
463
  const fullPath = `${base}${r.path}`;
445
464
  const tags = [];
465
+ if (r.error) {
466
+ tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
467
+ }
446
468
  if (r.delay && r.delay > 0) {
447
469
  tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
448
470
  }
@@ -456,9 +478,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
456
478
  tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
457
479
  }
458
480
  let desc;
459
- if (r.handler) {
481
+ if (r.error) {
482
+ desc = `Error injection \u2014 <code>${r.error}</code>`;
483
+ } else if (r.handler) {
460
484
  const found = handlers.has(r.handler);
461
- desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
485
+ const handlerName = escapeHtml(r.handler);
486
+ desc = found ? `Handler \u2014 <code>${handlerName}()</code>` : `Handler \u2014 <code>${handlerName}()</code> <span style="color:#f85149">(not loaded)</span>`;
462
487
  } else if (r.scenarios?.length) {
463
488
  const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
464
489
  desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
@@ -620,7 +645,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
620
645
 
621
646
  <div class="banner">
622
647
  <div class="banner-inner">
623
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
648
+ ${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
624
649
  <p>Zero-config REST API mock server</p>
625
650
  <div class="banner-meta">
626
651
  <span>URL <strong>${host}</strong></span>
@@ -703,6 +728,10 @@ function nextId(items) {
703
728
  const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
704
729
  return ids.length > 0 ? Math.max(...ids) + 1 : 1;
705
730
  }
731
+ function generateId(items, strategy) {
732
+ if (strategy === "uuid") return crypto.randomUUID();
733
+ return nextId(items);
734
+ }
706
735
  function firstParam(value) {
707
736
  if (value === void 0) return void 0;
708
737
  return Array.isArray(value) ? value[0] : value;
@@ -727,6 +756,7 @@ function applyOperator(itemValue, op, filterValue) {
727
756
  case "_start":
728
757
  return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
729
758
  case "_regex": {
759
+ if (filterValue.length > 200) return false;
730
760
  try {
731
761
  return new RegExp(filterValue, "i").test(strItem);
732
762
  } catch {
@@ -790,10 +820,10 @@ function findById(items, id) {
790
820
  function findIndexById(items, id) {
791
821
  return items.findIndex((i) => String(i["id"]) === id);
792
822
  }
793
- function createItem(storage, resource, body) {
823
+ function createItem(storage, resource, body, idStrategy = "increment") {
794
824
  const collection = storage.getCollection(resource) ?? [];
795
825
  const item = {
796
- id: body["id"] !== void 0 ? body["id"] : nextId(collection),
826
+ id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
797
827
  ...body
798
828
  };
799
829
  storage.setCollection(resource, [...collection, item]);
@@ -971,7 +1001,12 @@ var CollectionRouteCommand = class {
971
1001
  if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
972
1002
  return reply.status(400).send({ error: "Request body must be a JSON object" });
973
1003
  }
974
- const item = createItem(this.storage, this.resource, req.body);
1004
+ const item = createItem(
1005
+ this.storage,
1006
+ this.resource,
1007
+ req.body,
1008
+ this.options.idStrategy
1009
+ );
975
1010
  return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
976
1011
  });
977
1012
  }
@@ -1061,6 +1096,10 @@ var CustomRouteCommand = class {
1061
1096
  if (route.delay && route.delay > 0) {
1062
1097
  await new Promise((resolve5) => setTimeout(resolve5, route.delay));
1063
1098
  }
1099
+ if (route.error) {
1100
+ const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
1101
+ return reply.status(route.error).send(body2);
1102
+ }
1064
1103
  for (const [key, value] of Object.entries(headers)) {
1065
1104
  reply.header(key, value);
1066
1105
  }
@@ -1357,22 +1396,36 @@ var yrestOptionsSchema = z.object({
1357
1396
  pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
1358
1397
  enabled: v !== false,
1359
1398
  limit: v === false || v === true ? 10 : v
1360
- }))
1399
+ })),
1400
+ /**
1401
+ * Strategy used to generate `id` values for new items when no `id` is provided in the body.
1402
+ *
1403
+ * - `"increment"` (default) — next integer above the current max id in the collection.
1404
+ * - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
1405
+ */
1406
+ idStrategy: z.enum(["increment", "uuid"]).default("increment")
1361
1407
  });
1362
1408
 
1363
1409
  // src/config/loadConfigFile.ts
1364
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1410
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
1365
1411
  import { parse as parse2 } from "yaml";
1366
1412
  function loadConfigFile(configPath) {
1367
1413
  if (!existsSync2(configPath)) return {};
1368
- const raw = readFileSync2(configPath, "utf8");
1414
+ const raw = readFileSync3(configPath, "utf8");
1369
1415
  return parse2(raw) ?? {};
1370
1416
  }
1371
1417
 
1372
1418
  // src/utils/handlers.ts
1373
1419
  import { existsSync as existsSync3 } from "fs";
1420
+ var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
1374
1421
  async function loadHandlers(filePath) {
1375
1422
  if (!existsSync3(filePath)) return /* @__PURE__ */ new Map();
1423
+ if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
1424
+ console.error(
1425
+ ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
1426
+ );
1427
+ return /* @__PURE__ */ new Map();
1428
+ }
1376
1429
  try {
1377
1430
  const mod = await import(filePath);
1378
1431
  const map = /* @__PURE__ */ new Map();
@@ -1402,8 +1455,12 @@ function registerServe(program2) {
1402
1455
  ).option(
1403
1456
  "--handlers <file>",
1404
1457
  "Path to a JavaScript file exporting custom route handler functions"
1458
+ ).option(
1459
+ "--id-strategy <strategy>",
1460
+ "Id generation strategy for new items: increment (default) or uuid",
1461
+ "increment"
1405
1462
  ).action(async (file, flags, cmd) => {
1406
- const fileConfig = loadConfigFile(join(process.cwd(), "yrest.config.yml"));
1463
+ const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
1407
1464
  const cliOverrides = Object.fromEntries(
1408
1465
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
1409
1466
  );
@@ -1506,8 +1563,8 @@ function registerServe(program2) {
1506
1563
  }
1507
1564
 
1508
1565
  // src/cli/commands/handler.ts
1509
- import { existsSync as existsSync4, readFileSync as readFileSync3, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
1510
- import { join as join2, resolve as resolve4, basename } from "path";
1566
+ import { existsSync as existsSync4, readFileSync as readFileSync4, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
1567
+ import { join as join3, resolve as resolve4, basename } from "path";
1511
1568
  import { parse as parse3, stringify as stringify2 } from "yaml";
1512
1569
  var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
1513
1570
  // Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
@@ -1534,7 +1591,7 @@ function registerHandler(program2) {
1534
1591
  "--register",
1535
1592
  "Also add a _routes entry to db.yml linking this handler to method + path"
1536
1593
  ).action((name, flags) => {
1537
- const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
1594
+ const fileConfig = loadConfigFile(join3(process.cwd(), "yrest.config.yml"));
1538
1595
  const handlersPath = resolve4(
1539
1596
  fileConfig.handlers ?? "yrest.handlers.js"
1540
1597
  );
@@ -1547,7 +1604,7 @@ function registerHandler(program2) {
1547
1604
  );
1548
1605
  console.log(` Created ${basename(handlersPath)}`);
1549
1606
  } else {
1550
- const existing = readFileSync3(handlersPath, "utf8");
1607
+ const existing = readFileSync4(handlersPath, "utf8");
1551
1608
  if (existing.includes(`function ${name}(`)) {
1552
1609
  console.error(` Error: handler "${name}" already exists in ${basename(handlersPath)}`);
1553
1610
  process.exit(1);
@@ -1564,7 +1621,7 @@ function registerHandler(program2) {
1564
1621
  console.error(` Error: database file not found at ${dbPath}`);
1565
1622
  process.exit(1);
1566
1623
  }
1567
- const raw = parse3(readFileSync3(dbPath, "utf8")) ?? {};
1624
+ const raw = parse3(readFileSync4(dbPath, "utf8")) ?? {};
1568
1625
  if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
1569
1626
  const routes = raw["_routes"];
1570
1627
  const alreadyRegistered = routes.some((r) => r["handler"] === name);
package/dist/index.d.mts CHANGED
@@ -112,6 +112,19 @@ type CustomRoute = {
112
112
  * Applied before any response is sent, regardless of which path resolved the response.
113
113
  */
114
114
  delay?: number;
115
+ /**
116
+ * Forces this route to always return the given HTTP status code as an error,
117
+ * bypassing handlers, scenarios and the static response entirely.
118
+ * Applied after `delay:` so slow-error scenarios still work.
119
+ *
120
+ * @example
121
+ * // Always return 503
122
+ * error: 503
123
+ * errorBody: { message: "Service unavailable" }
124
+ */
125
+ error?: number;
126
+ /** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
127
+ errorBody?: unknown;
115
128
  };
116
129
  /**
117
130
  * In-memory store backed by a YAML file.
@@ -251,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
251
264
  enabled: boolean;
252
265
  limit: number;
253
266
  }, number | boolean | undefined>;
267
+ /**
268
+ * Strategy used to generate `id` values for new items when no `id` is provided in the body.
269
+ *
270
+ * - `"increment"` (default) — next integer above the current max id in the collection.
271
+ * - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
272
+ */
273
+ idStrategy: z.ZodDefault<z.ZodEnum<["increment", "uuid"]>>;
254
274
  }, "strip", z.ZodTypeAny, {
255
275
  file: string;
256
276
  port: number;
@@ -264,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
264
284
  enabled: boolean;
265
285
  limit: number;
266
286
  };
287
+ idStrategy: "increment" | "uuid";
267
288
  handlers?: string | undefined;
268
289
  }, {
269
290
  file: string;
@@ -276,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
276
297
  delay?: number | undefined;
277
298
  handlers?: string | undefined;
278
299
  pageable?: number | boolean | undefined;
300
+ idStrategy?: "increment" | "uuid" | undefined;
279
301
  }>;
280
302
  /**
281
303
  * Resolved server configuration after Zod validation and transformation.
package/dist/index.d.ts CHANGED
@@ -112,6 +112,19 @@ type CustomRoute = {
112
112
  * Applied before any response is sent, regardless of which path resolved the response.
113
113
  */
114
114
  delay?: number;
115
+ /**
116
+ * Forces this route to always return the given HTTP status code as an error,
117
+ * bypassing handlers, scenarios and the static response entirely.
118
+ * Applied after `delay:` so slow-error scenarios still work.
119
+ *
120
+ * @example
121
+ * // Always return 503
122
+ * error: 503
123
+ * errorBody: { message: "Service unavailable" }
124
+ */
125
+ error?: number;
126
+ /** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
127
+ errorBody?: unknown;
115
128
  };
116
129
  /**
117
130
  * In-memory store backed by a YAML file.
@@ -251,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
251
264
  enabled: boolean;
252
265
  limit: number;
253
266
  }, number | boolean | undefined>;
267
+ /**
268
+ * Strategy used to generate `id` values for new items when no `id` is provided in the body.
269
+ *
270
+ * - `"increment"` (default) — next integer above the current max id in the collection.
271
+ * - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
272
+ */
273
+ idStrategy: z.ZodDefault<z.ZodEnum<["increment", "uuid"]>>;
254
274
  }, "strip", z.ZodTypeAny, {
255
275
  file: string;
256
276
  port: number;
@@ -264,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
264
284
  enabled: boolean;
265
285
  limit: number;
266
286
  };
287
+ idStrategy: "increment" | "uuid";
267
288
  handlers?: string | undefined;
268
289
  }, {
269
290
  file: string;
@@ -276,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
276
297
  delay?: number | undefined;
277
298
  handlers?: string | undefined;
278
299
  pageable?: number | boolean | undefined;
300
+ idStrategy?: "increment" | "uuid" | undefined;
279
301
  }>;
280
302
  /**
281
303
  * Resolved server configuration after Zod validation and transformation.
package/dist/index.js CHANGED
@@ -31,9 +31,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // node_modules/tsup/assets/cjs_shims.js
34
+ var getImportMetaUrl, importMetaUrl;
34
35
  var init_cjs_shims = __esm({
35
36
  "node_modules/tsup/assets/cjs_shims.js"() {
36
37
  "use strict";
38
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
39
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
37
40
  }
38
41
  });
39
42
 
@@ -56,8 +59,8 @@ __export(yrestStorage_exports, {
56
59
  createYrestStorage: () => createYrestStorage
57
60
  });
58
61
  function createYrestStorage(filePath) {
59
- const absPath = (0, import_node_path.resolve)(filePath);
60
- const raw = (0, import_yaml2.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
62
+ const absPath = (0, import_node_path2.resolve)(filePath);
63
+ const raw = (0, import_yaml2.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
61
64
  const relations = raw["_rel"] ?? {};
62
65
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
63
66
  const data = Object.fromEntries(
@@ -89,12 +92,12 @@ function createYrestStorage(filePath) {
89
92
  if (Object.keys(relations).length > 0) payload._rel = relations;
90
93
  if (routes.length > 0) payload._routes = routes;
91
94
  Object.assign(payload, data);
92
- const tmp = (0, import_node_path.resolve)((0, import_node_path.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
93
- (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml2.stringify)(payload), "utf8");
94
- (0, import_node_fs2.renameSync)(tmp, absPath);
95
+ const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
96
+ (0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml2.stringify)(payload), "utf8");
97
+ (0, import_node_fs3.renameSync)(tmp, absPath);
95
98
  },
96
99
  reload() {
97
- const fresh = (0, import_yaml2.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
100
+ const fresh = (0, import_yaml2.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
98
101
  const freshRelations = fresh["_rel"] ?? {};
99
102
  const freshData = Object.fromEntries(
100
103
  Object.entries(fresh).filter(([key]) => key !== "_rel" && key !== "_routes")
@@ -124,13 +127,13 @@ function createYrestStorage(filePath) {
124
127
  }
125
128
  };
126
129
  }
127
- var import_node_fs2, import_node_path, import_node_crypto2, import_yaml2;
130
+ var import_node_fs3, import_node_path2, import_node_crypto2, import_yaml2;
128
131
  var init_yrestStorage = __esm({
129
132
  "src/storage/yrestStorage.ts"() {
130
133
  "use strict";
131
134
  init_cjs_shims();
132
- import_node_fs2 = require("fs");
133
- import_node_path = require("path");
135
+ import_node_fs3 = require("fs");
136
+ import_node_path2 = require("path");
134
137
  import_node_crypto2 = require("crypto");
135
138
  import_yaml2 = require("yaml");
136
139
  init_deepCopy();
@@ -194,13 +197,20 @@ function dedent(str) {
194
197
 
195
198
  // src/api/yrestServer.ts
196
199
  init_cjs_shims();
197
- var import_node_path2 = require("path");
200
+ var import_node_path3 = require("path");
198
201
 
199
202
  // src/utils/handlers.ts
200
203
  init_cjs_shims();
201
204
  var import_node_fs = require("fs");
205
+ var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
202
206
  async function loadHandlers(filePath) {
203
207
  if (!(0, import_node_fs.existsSync)(filePath)) return /* @__PURE__ */ new Map();
208
+ if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
209
+ console.error(
210
+ ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
211
+ );
212
+ return /* @__PURE__ */ new Map();
213
+ }
204
214
  try {
205
215
  const mod = await import(filePath);
206
216
  const map = /* @__PURE__ */ new Map();
@@ -237,6 +247,9 @@ init_cjs_shims();
237
247
 
238
248
  // src/router/templates/about.template.ts
239
249
  init_cjs_shims();
250
+ var import_node_fs2 = require("fs");
251
+ var import_node_path = require("path");
252
+ var import_node_url = require("url");
240
253
 
241
254
  // src/utils/interpolate.ts
242
255
  init_cjs_shims();
@@ -286,6 +299,15 @@ function hasTemplates(value) {
286
299
  }
287
300
 
288
301
  // src/router/templates/about.template.ts
302
+ var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
303
+ var LOGO_SRC = (() => {
304
+ try {
305
+ const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/logo-color.png"));
306
+ return `data:image/png;base64,${buf.toString("base64")}`;
307
+ } catch {
308
+ return "";
309
+ }
310
+ })();
289
311
  var METHOD_COLOR = {
290
312
  GET: "#3fb950",
291
313
  POST: "#58a6ff",
@@ -294,8 +316,11 @@ var METHOD_COLOR = {
294
316
  DELETE: "#f85149",
295
317
  fn: "#f0883e"
296
318
  };
319
+ function escapeHtml(str) {
320
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
321
+ }
297
322
  function badge(label, color, bg) {
298
- return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${label}</span>`;
323
+ return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${escapeHtml(label)}</span>`;
299
324
  }
300
325
  function methodBadge(method) {
301
326
  const color = METHOD_COLOR[method] ?? "#7d8590";
@@ -305,7 +330,7 @@ function endpointRow(method, path, desc) {
305
330
  return `
306
331
  <tr>
307
332
  <td class="method-cell">${methodBadge(method)}</td>
308
- <td class="path-cell"><code>${path}</code></td>
333
+ <td class="path-cell"><code>${escapeHtml(path)}</code></td>
309
334
  <td class="desc-cell">${desc}</td>
310
335
  </tr>`;
311
336
  }
@@ -339,7 +364,7 @@ function resourceAccordion(name, base, isOpen) {
339
364
  return `
340
365
  <details class="resource-card" ${isOpen ? "open" : ""}>
341
366
  <summary>
342
- <span class="resource-name">/${name}</span>
367
+ <span class="resource-name">/${escapeHtml(name)}</span>
343
368
  <span class="route-count">6 routes</span>
344
369
  </summary>
345
370
  <table>
@@ -423,7 +448,7 @@ curl -X POST ${host}/_snapshot/reset`
423
448
  }
424
449
  if (firstCustomRoute) {
425
450
  const method = firstCustomRoute.method?.toUpperCase() ?? "GET";
426
- const fullPath = `${host}${base}${firstCustomRoute.path}`;
451
+ const fullPath = `${host}${base}${escapeHtml(firstCustomRoute.path ?? "")}`;
427
452
  const curlFlag = method === "GET" ? "" : `-X ${method} `;
428
453
  examples.push(`# Custom route
429
454
  curl ${curlFlag}${fullPath}`);
@@ -444,6 +469,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
444
469
  modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
445
470
  if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
446
471
  if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
472
+ if (options.idStrategy !== "increment")
473
+ modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
447
474
  const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
448
475
  const nestedRows = [];
449
476
  for (const [child, fields] of Object.entries(relations)) {
@@ -486,6 +513,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
486
513
  ${customRoutes.map((r) => {
487
514
  const fullPath = `${base}${r.path}`;
488
515
  const tags = [];
516
+ if (r.error) {
517
+ tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
518
+ }
489
519
  if (r.delay && r.delay > 0) {
490
520
  tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
491
521
  }
@@ -499,9 +529,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
499
529
  tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
500
530
  }
501
531
  let desc;
502
- if (r.handler) {
532
+ if (r.error) {
533
+ desc = `Error injection \u2014 <code>${r.error}</code>`;
534
+ } else if (r.handler) {
503
535
  const found = handlers.has(r.handler);
504
- desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
536
+ const handlerName = escapeHtml(r.handler);
537
+ desc = found ? `Handler \u2014 <code>${handlerName}()</code>` : `Handler \u2014 <code>${handlerName}()</code> <span style="color:#f85149">(not loaded)</span>`;
505
538
  } else if (r.scenarios?.length) {
506
539
  const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
507
540
  desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
@@ -663,7 +696,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
663
696
 
664
697
  <div class="banner">
665
698
  <div class="banner-inner">
666
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
699
+ ${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
667
700
  <p>Zero-config REST API mock server</p>
668
701
  <div class="banner-meta">
669
702
  <span>URL <strong>${host}</strong></span>
@@ -750,6 +783,10 @@ function nextId(items) {
750
783
  const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
751
784
  return ids.length > 0 ? Math.max(...ids) + 1 : 1;
752
785
  }
786
+ function generateId(items, strategy) {
787
+ if (strategy === "uuid") return crypto.randomUUID();
788
+ return nextId(items);
789
+ }
753
790
  function firstParam(value) {
754
791
  if (value === void 0) return void 0;
755
792
  return Array.isArray(value) ? value[0] : value;
@@ -775,6 +812,7 @@ function applyOperator(itemValue, op, filterValue) {
775
812
  case "_start":
776
813
  return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
777
814
  case "_regex": {
815
+ if (filterValue.length > 200) return false;
778
816
  try {
779
817
  return new RegExp(filterValue, "i").test(strItem);
780
818
  } catch {
@@ -839,10 +877,10 @@ function findById(items, id) {
839
877
  function findIndexById(items, id) {
840
878
  return items.findIndex((i) => String(i["id"]) === id);
841
879
  }
842
- function createItem(storage, resource, body) {
880
+ function createItem(storage, resource, body, idStrategy = "increment") {
843
881
  const collection = storage.getCollection(resource) ?? [];
844
882
  const item = {
845
- id: body["id"] !== void 0 ? body["id"] : nextId(collection),
883
+ id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
846
884
  ...body
847
885
  };
848
886
  storage.setCollection(resource, [...collection, item]);
@@ -1021,7 +1059,12 @@ var CollectionRouteCommand = class {
1021
1059
  if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
1022
1060
  return reply.status(400).send({ error: "Request body must be a JSON object" });
1023
1061
  }
1024
- const item = createItem(this.storage, this.resource, req.body);
1062
+ const item = createItem(
1063
+ this.storage,
1064
+ this.resource,
1065
+ req.body,
1066
+ this.options.idStrategy
1067
+ );
1025
1068
  return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
1026
1069
  });
1027
1070
  }
@@ -1115,6 +1158,10 @@ var CustomRouteCommand = class {
1115
1158
  if (route.delay && route.delay > 0) {
1116
1159
  await new Promise((resolve3) => setTimeout(resolve3, route.delay));
1117
1160
  }
1161
+ if (route.error) {
1162
+ const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
1163
+ return reply.status(route.error).send(body2);
1164
+ }
1118
1165
  for (const [key, value] of Object.entries(headers)) {
1119
1166
  reply.header(key, value);
1120
1167
  }
@@ -1385,7 +1432,7 @@ function createYrestServer(options) {
1385
1432
  return {
1386
1433
  async start() {
1387
1434
  const storage = "data" in options && options.data !== void 0 ? createInMemoryStorage(options.data) : (await Promise.resolve().then(() => (init_yrestStorage(), yrestStorage_exports))).createYrestStorage(resolvedOptions.file);
1388
- const handlers = resolvedOptions.handlers ? await loadHandlers((0, import_node_path2.resolve)(resolvedOptions.handlers)) : /* @__PURE__ */ new Map();
1435
+ const handlers = resolvedOptions.handlers ? await loadHandlers((0, import_node_path3.resolve)(resolvedOptions.handlers)) : /* @__PURE__ */ new Map();
1389
1436
  _inner = createYrestServerFromStorage(storage, resolvedOptions, handlers);
1390
1437
  await _inner.start();
1391
1438
  },
@@ -1412,7 +1459,8 @@ function buildOptions(opts) {
1412
1459
  readonly: opts.readonly ?? false,
1413
1460
  delay: opts.delay ?? 0,
1414
1461
  handlers: opts.handlers,
1415
- pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
1462
+ pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
1463
+ idStrategy: "increment"
1416
1464
  };
1417
1465
  }
1418
1466
  function createInMemoryStorage(data) {
@@ -1496,7 +1544,14 @@ var yrestOptionsSchema = import_zod.z.object({
1496
1544
  pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
1497
1545
  enabled: v !== false,
1498
1546
  limit: v === false || v === true ? 10 : v
1499
- }))
1547
+ })),
1548
+ /**
1549
+ * Strategy used to generate `id` values for new items when no `id` is provided in the body.
1550
+ *
1551
+ * - `"increment"` (default) — next integer above the current max id in the collection.
1552
+ * - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
1553
+ */
1554
+ idStrategy: import_zod.z.enum(["increment", "uuid"]).default("increment")
1500
1555
  });
1501
1556
  // Annotate the CommonJS export names for ESM import in node:
1502
1557
  0 && (module.exports = {