@gzl10/nexus-backend 0.19.0 → 0.20.0

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.
package/dist/main.js CHANGED
@@ -106,7 +106,7 @@ var init_package = __esm({
106
106
  "package.json"() {
107
107
  package_default = {
108
108
  name: "@gzl10/nexus-backend",
109
- version: "0.19.0",
109
+ version: "0.20.0",
110
110
  description: "Backend as a Service (BaaS) with Express 5, Knex and CASL",
111
111
  type: "module",
112
112
  main: "./dist/index.js",
@@ -9367,18 +9367,20 @@ function createSystemController(ctx) {
9367
9367
  /**
9368
9368
  * GET /system/capabilities
9369
9369
  * Public endpoint — no authentication required.
9370
- * Returns backend version and registered plugin codes.
9370
+ * Resolves all registered capabilities (core + plugin-contributed).
9371
9371
  */
9372
- getCapabilities(_req, res) {
9373
- res.set("Cache-Control", "public, max-age=300");
9374
- const manifest = engine.getCoreManifest();
9375
- const plugins = engine.getPlugins();
9376
- const body = {
9377
- version: manifest.version,
9378
- plugins: plugins.map((p) => p.code),
9379
- locales: ctx.locales
9380
- };
9381
- res.json(body);
9372
+ async getCapabilities(_req, res) {
9373
+ try {
9374
+ res.set("Cache-Control", "public, max-age=300");
9375
+ const body = await ctx.capabilities.resolve();
9376
+ res.json(body);
9377
+ } catch (err) {
9378
+ ctx.core.logger.error({ err }, "Failed to resolve capabilities");
9379
+ res.set("Cache-Control", "no-cache");
9380
+ res.status(500).json({
9381
+ error: { code: "INTERNAL_ERROR", message: "Failed to resolve capabilities" }
9382
+ });
9383
+ }
9382
9384
  }
9383
9385
  };
9384
9386
  function toManifestDTO(manifest, req) {
@@ -9951,8 +9953,8 @@ function registerCoreVars(registry2) {
9951
9953
  registry2.register("auth", "core", [
9952
9954
  {
9953
9955
  name: "AUTH_SECRET",
9954
- description: { en: "JWT signing secret. Must be at least 32 characters. Use a cryptographically random string in production", es: "Clave de firma JWT. M\xEDnimo 32 caracteres. Usar una cadena criptogr\xE1ficamente aleatoria en producci\xF3n" },
9955
- required: true,
9956
+ description: { en: "JWT signing secret (optional). If not set, auto-generated and persisted in database. Must be at least 32 characters. Set explicitly for multi-instance deployments", es: "Clave de firma JWT (opcional). Si no se establece, se genera autom\xE1ticamente y se persiste en la base de datos. M\xEDnimo 32 caracteres. Establecer expl\xEDcitamente para despliegues multi-instancia" },
9957
+ required: false,
9956
9958
  sensitive: true
9957
9959
  },
9958
9960
  {
@@ -10469,96 +10471,70 @@ var init_system = __esm({
10469
10471
  }
10470
10472
  });
10471
10473
 
10472
- // src/modules/ui-settings/ui-branding.entity.ts
10473
- import { useTextField as useTextField7, useImageField } from "@gzl10/nexus-sdk/fields";
10474
- var uiBrandingEntity;
10475
- var init_ui_branding_entity = __esm({
10476
- "src/modules/ui-settings/ui-branding.entity.ts"() {
10474
+ // src/modules/ui-settings/ui-style-guide.entity.ts
10475
+ import { useTextField as useTextField7, useImageField, useSelectField as useSelectField6, useColorField, useNumberField as useNumberField4, useSwitchField as useSwitchField2 } from "@gzl10/nexus-sdk/fields";
10476
+ var uiStyleGuideEntity;
10477
+ var init_ui_style_guide_entity = __esm({
10478
+ "src/modules/ui-settings/ui-style-guide.entity.ts"() {
10477
10479
  "use strict";
10478
- uiBrandingEntity = {
10480
+ uiStyleGuideEntity = {
10479
10481
  type: "single",
10480
10482
  realtime: "sync",
10481
- key: "ui_branding",
10482
- label: { en: "Branding", es: "Identidad Corporativa" },
10483
- icon: "mdi:palette-swatch-outline",
10483
+ key: "ui_style_guide",
10484
+ label: { en: "Style Guide", es: "Gu\xEDa de Estilos" },
10485
+ icon: "mdi:palette-outline",
10484
10486
  public: true,
10485
- routePrefix: "/ui-branding",
10487
+ routePrefix: "/ui-style-guide",
10486
10488
  defaults: {
10487
10489
  appName: "Nexus",
10488
10490
  logo: null,
10489
10491
  logoDark: null,
10490
- favicon: null
10492
+ favicon: null,
10493
+ primaryColor: "#3B82F6",
10494
+ font: "space-grotesk",
10495
+ typographyScale: "default",
10496
+ borderRadius: 8,
10497
+ glassEffect: true,
10498
+ theme: "system",
10499
+ loginLayout: "centered"
10491
10500
  },
10492
10501
  fields: {
10493
10502
  appName: useTextField7({
10494
10503
  label: { en: "App Name", es: "Nombre de la App" },
10495
- hint: { en: "Displayed in the header, browser tab and emails", es: "Se muestra en el header, pesta\xF1a del navegador y emails" },
10504
+ hint: { en: "Displayed in header, browser tab and emails", es: "Se muestra en header, pesta\xF1a del navegador y emails" },
10496
10505
  size: 100,
10497
10506
  required: true
10498
10507
  }),
10499
10508
  logo: useImageField({
10500
10509
  label: { en: "Logo (Light Theme)", es: "Logo (Tema Claro)" },
10501
- hint: { en: "Recommended: SVG or PNG with transparent background, max 200px height", es: "Recomendado: SVG o PNG con fondo transparente, m\xE1x 200px de alto" },
10510
+ hint: { en: "SVG or PNG with transparent background, max 200px height", es: "SVG o PNG con fondo transparente, m\xE1x 200px de alto" },
10502
10511
  folder: "branding",
10503
10512
  isPublic: true,
10504
10513
  dedupe: true
10505
10514
  }),
10506
10515
  logoDark: useImageField({
10507
10516
  label: { en: "Logo (Dark Theme)", es: "Logo (Tema Oscuro)" },
10508
- hint: { en: "Optional: Use a lighter version for dark backgrounds", es: "Opcional: Usa una versi\xF3n m\xE1s clara para fondos oscuros" },
10517
+ hint: { en: "Lighter version for dark backgrounds", es: "Versi\xF3n m\xE1s clara para fondos oscuros" },
10509
10518
  folder: "branding",
10510
10519
  isPublic: true,
10511
10520
  dedupe: true
10512
10521
  }),
10513
10522
  favicon: useImageField({
10514
10523
  label: { en: "Favicon", es: "Favicon" },
10515
- hint: { en: "Browser tab icon. Recommended: 32x32 or 64x64 PNG/ICO", es: "Icono de pesta\xF1a del navegador. Recomendado: 32x32 o 64x64 PNG/ICO" },
10524
+ hint: { en: "32x32 or 64x64 PNG/ICO", es: "32x32 o 64x64 PNG/ICO" },
10516
10525
  accept: "image/x-icon,image/png,image/svg+xml",
10517
10526
  maxSize: "256KB",
10518
10527
  folder: "branding",
10519
10528
  isPublic: true,
10520
10529
  dedupe: true
10521
- })
10522
- },
10523
- casl: {
10524
- subject: "UiBranding",
10525
- permissions: {
10526
- ADMIN: { actions: ["read", "update"] }
10527
- }
10528
- }
10529
- };
10530
- }
10531
- });
10532
-
10533
- // src/modules/ui-settings/ui-theme.entity.ts
10534
- import { useSelectField as useSelectField6, useColorField } from "@gzl10/nexus-sdk/fields";
10535
- var uiThemeEntity;
10536
- var init_ui_theme_entity = __esm({
10537
- "src/modules/ui-settings/ui-theme.entity.ts"() {
10538
- "use strict";
10539
- uiThemeEntity = {
10540
- type: "single",
10541
- realtime: "sync",
10542
- key: "ui_theme",
10543
- label: { en: "Theme & Colors", es: "Tema y Colores" },
10544
- icon: "mdi:palette",
10545
- public: true,
10546
- routePrefix: "/ui-theme",
10547
- defaults: {
10548
- // Typography
10549
- font: "space-grotesk",
10550
- // Theme & Colors
10551
- theme: "system",
10552
- primaryColor: "#3B82F6",
10553
- dopamineTheme: "none",
10554
- // Layout
10555
- loginLayout: "centered"
10556
- },
10557
- fields: {
10558
- // === Typography ===
10530
+ }),
10531
+ primaryColor: useColorField({
10532
+ label: { en: "Primary Color", es: "Color Principal" },
10533
+ hint: { en: "Accent color for buttons, links and highlights", es: "Color de acento para botones, enlaces y resaltados" }
10534
+ }),
10559
10535
  font: useSelectField6({
10560
10536
  label: { en: "Font", es: "Fuente" },
10561
- hint: { en: "Primary font for headings and UI elements", es: "Fuente principal para t\xEDtulos y elementos de interfaz" },
10537
+ hint: { en: "Primary font for headings and UI", es: "Fuente principal para t\xEDtulos e interfaz" },
10562
10538
  options: [
10563
10539
  { value: "space-grotesk", label: "Space Grotesk" },
10564
10540
  { value: "inter", label: "Inter" },
@@ -10568,37 +10544,38 @@ var init_ui_theme_entity = __esm({
10568
10544
  { value: "system", label: { en: "System Default", es: "Sistema" } }
10569
10545
  ]
10570
10546
  }),
10571
- // === Theme & Colors ===
10547
+ typographyScale: useSelectField6({
10548
+ label: { en: "Typography Scale", es: "Escala Tipogr\xE1fica" },
10549
+ hint: { en: "Font size scaling (WCAG 1.4.4)", es: "Escala de tama\xF1os de fuente (WCAG 1.4.4)" },
10550
+ options: [
10551
+ { value: "compact", label: { en: "Compact", es: "Compacta" } },
10552
+ { value: "default", label: { en: "Default", es: "Por defecto" } },
10553
+ { value: "relaxed", label: { en: "Relaxed", es: "Relajada" } }
10554
+ ]
10555
+ }),
10556
+ borderRadius: useNumberField4({
10557
+ label: { en: "Border Radius", es: "Radio de Bordes" },
10558
+ hint: { en: "Corner roundness in pixels (0 = sharp, 16 = very round)", es: "Redondeo de esquinas en p\xEDxeles (0 = cuadrado, 16 = muy redondo)" },
10559
+ defaultValue: 8,
10560
+ validation: { min: 0, max: 16 }
10561
+ }),
10562
+ glassEffect: useSwitchField2({
10563
+ label: { en: "Glass Effect", es: "Efecto Glass" },
10564
+ hint: { en: "Glassmorphism blur on cards and modals", es: "Desenfoque glassmorphism en cards y modales" },
10565
+ defaultValue: true
10566
+ }),
10572
10567
  theme: useSelectField6({
10573
10568
  label: { en: "Theme", es: "Tema" },
10574
- hint: { en: "System follows your device preferences", es: "Sistema sigue las preferencias de tu dispositivo" },
10569
+ hint: { en: "System follows device preferences", es: "Sistema sigue las preferencias del dispositivo" },
10575
10570
  options: [
10576
10571
  { value: "light", label: { en: "Light", es: "Claro" } },
10577
10572
  { value: "dark", label: { en: "Dark", es: "Oscuro" } },
10578
10573
  { value: "system", label: { en: "System", es: "Sistema" } }
10579
10574
  ]
10580
10575
  }),
10581
- dopamineTheme: useSelectField6({
10582
- label: { en: "Dopamine Theme", es: "Tema Dopamina" },
10583
- hint: { en: "Vibrant color presets optimized for light and dark modes (2025/2026 trends)", es: "Presets de colores vibrantes optimizados para modo claro y oscuro (tendencias 2025/2026)" },
10584
- options: [
10585
- { value: "none", label: { en: "None (Custom Color)", es: "Ninguno (Color personalizado)" } },
10586
- { value: "electric", label: { en: "Electric (Cobalt Blue)", es: "El\xE9ctrico (Azul Cobalto)" } },
10587
- { value: "sunset", label: { en: "Sunset (Coral Red)", es: "Atardecer (Rojo Coral)" } },
10588
- { value: "ocean", label: { en: "Ocean (Teal)", es: "Oc\xE9ano (Verde Azulado)" } },
10589
- { value: "forest", label: { en: "Forest (Mint)", es: "Bosque (Menta)" } },
10590
- { value: "lavender", label: { en: "Lavender (Violet)", es: "Lavanda (Violeta)" } },
10591
- { value: "cherry", label: { en: "Cherry (Fuchsia)", es: "Cereza (Fucsia)" } },
10592
- { value: "amber", label: { en: "Amber (Golden)", es: "\xC1mbar (Dorado)" } },
10593
- { value: "tangerine", label: { en: "Tangerine (Orange)", es: "Mandarina (Naranja)" } },
10594
- { value: "slate", label: { en: "Slate (Cool Gray)", es: "Pizarra (Gris Fr\xEDo)" } },
10595
- { value: "bronze", label: { en: "Bronze (Earth)", es: "Bronce (Tierra)" } }
10596
- ]
10597
- }),
10598
- // === Login Layout ===
10599
10576
  loginLayout: useSelectField6({
10600
10577
  label: { en: "Login Layout", es: "Dise\xF1o de Login" },
10601
- hint: { en: "Visual layout for authentication pages", es: "Dise\xF1o visual para p\xE1ginas de autenticaci\xF3n" },
10578
+ hint: { en: "Auth page visual layout", es: "Dise\xF1o visual de p\xE1ginas de autenticaci\xF3n" },
10602
10579
  options: [
10603
10580
  { value: "centered", label: { en: "Centered", es: "Centrado" } },
10604
10581
  { value: "split", label: { en: "Split", es: "Dividido" } },
@@ -10606,140 +10583,10 @@ var init_ui_theme_entity = __esm({
10606
10583
  { value: "floating", label: { en: "Floating", es: "Flotante" } },
10607
10584
  { value: "minimal", label: { en: "Minimal", es: "Minimalista" } }
10608
10585
  ]
10609
- }),
10610
- primaryColor: {
10611
- ...useColorField({
10612
- label: { en: "Primary Color", es: "Color Principal" },
10613
- hint: { en: "Custom accent color for buttons, links and highlights", es: "Color de acento personalizado para botones, enlaces y resaltados" }
10614
- }),
10615
- // Hide when a dopamine theme is active (show only if 'none')
10616
- hidden: { field: "dopamineTheme", $ne: "none" }
10617
- }
10618
- },
10619
- casl: {
10620
- subject: "UiTheme",
10621
- permissions: {
10622
- ADMIN: { actions: ["read", "update"] }
10623
- }
10624
- }
10625
- };
10626
- }
10627
- });
10628
-
10629
- // src/modules/ui-settings/ui-effects.entity.ts
10630
- import { useSelectField as useSelectField7, useSwitchField as useSwitchField2 } from "@gzl10/nexus-sdk/fields";
10631
- var uiEffectsEntity;
10632
- var init_ui_effects_entity = __esm({
10633
- "src/modules/ui-settings/ui-effects.entity.ts"() {
10634
- "use strict";
10635
- uiEffectsEntity = {
10636
- type: "single",
10637
- realtime: "sync",
10638
- key: "ui_effects",
10639
- label: { en: "Visual Effects", es: "Efectos Visuales" },
10640
- icon: "mdi:shimmer",
10641
- public: true,
10642
- routePrefix: "/ui-effects",
10643
- defaults: {
10644
- glassIntensity: "medium",
10645
- borderStyle: "rounded",
10646
- enableAnimations: true,
10647
- enableOrganicShapes: false
10648
- },
10649
- fields: {
10650
- glassIntensity: useSelectField7({
10651
- label: { en: "Glass Intensity", es: "Intensidad Glass" },
10652
- hint: { en: "Glassmorphism blur effect on cards and modals", es: "Efecto de desenfoque glassmorphism en cards y modales" },
10653
- options: [
10654
- { value: "none", label: { en: "None", es: "Ninguno" } },
10655
- { value: "low", label: { en: "Low", es: "Baja" } },
10656
- { value: "medium", label: { en: "Medium", es: "Media" } },
10657
- { value: "high", label: { en: "High", es: "Alta" } }
10658
- ]
10659
- }),
10660
- borderStyle: useSelectField7({
10661
- label: { en: "Border Style", es: "Estilo de Bordes" },
10662
- hint: { en: "Corner radius for buttons, cards and inputs", es: "Radio de esquinas para botones, cards e inputs" },
10663
- options: [
10664
- { value: "sharp", label: { en: "Sharp", es: "Cuadrados" } },
10665
- { value: "rounded", label: { en: "Rounded", es: "Redondeados" } },
10666
- { value: "organic", label: { en: "Organic", es: "Org\xE1nicos" } }
10667
- ]
10668
- }),
10669
- enableAnimations: useSwitchField2({
10670
- label: { en: "Enable Animations", es: "Habilitar Animaciones" },
10671
- hint: { en: "Micro-interactions and transitions (hover, focus, page changes)", es: "Micro-interacciones y transiciones (hover, focus, cambios de p\xE1gina)" },
10672
- defaultValue: true,
10673
- meta: { sortable: true }
10674
- }),
10675
- enableOrganicShapes: useSwitchField2({
10676
- label: { en: "Enable Organic Shapes", es: "Habilitar Formas Org\xE1nicas" },
10677
- hint: { en: "Asymmetric border-radius for a more natural, playful look", es: "Border-radius asim\xE9trico para un aspecto m\xE1s natural y divertido" },
10678
- defaultValue: false,
10679
- meta: { sortable: true },
10680
- hidden: { field: "borderStyle", $ne: "organic" }
10681
- })
10682
- },
10683
- casl: {
10684
- subject: "UiEffects",
10685
- permissions: {
10686
- ADMIN: { actions: ["read", "update"] }
10687
- }
10688
- }
10689
- };
10690
- }
10691
- });
10692
-
10693
- // src/modules/ui-settings/ui-accessibility.entity.ts
10694
- import { useSelectField as useSelectField8, useSwitchField as useSwitchField3 } from "@gzl10/nexus-sdk/fields";
10695
- var uiAccessibilityEntity;
10696
- var init_ui_accessibility_entity = __esm({
10697
- "src/modules/ui-settings/ui-accessibility.entity.ts"() {
10698
- "use strict";
10699
- uiAccessibilityEntity = {
10700
- type: "single",
10701
- realtime: "sync",
10702
- key: "ui_accessibility",
10703
- label: { en: "Accessibility", es: "Accesibilidad" },
10704
- icon: "mdi:human",
10705
- public: true,
10706
- routePrefix: "/ui-accessibility",
10707
- defaults: {
10708
- typographyScale: "default",
10709
- reducedMotion: false,
10710
- highContrast: false
10711
- },
10712
- fields: {
10713
- typographyScale: useSelectField8({
10714
- label: { en: "Typography Scale", es: "Escala Tipogr\xE1fica" },
10715
- hint: { en: "Adjusts font sizes for better readability (WCAG 1.4.4)", es: "Ajusta tama\xF1os de fuente para mejor legibilidad (WCAG 1.4.4)" },
10716
- options: [
10717
- { value: "compact", label: { en: "Compact (smaller)", es: "Compacta (m\xE1s peque\xF1a)" } },
10718
- { value: "default", label: { en: "Default", es: "Por defecto" } },
10719
- { value: "relaxed", label: { en: "Relaxed (larger)", es: "Relajada (m\xE1s grande)" } }
10720
- ]
10721
- }),
10722
- reducedMotion: useSwitchField3({
10723
- label: { en: "Reduced Motion", es: "Movimiento Reducido" },
10724
- defaultValue: false,
10725
- meta: { sortable: true },
10726
- hint: {
10727
- en: "Minimizes animations for users sensitive to motion (WCAG 2.1)",
10728
- es: "Minimiza animaciones para usuarios sensibles al movimiento (WCAG 2.1)"
10729
- }
10730
- }),
10731
- highContrast: useSwitchField3({
10732
- label: { en: "High Contrast", es: "Alto Contraste" },
10733
- defaultValue: false,
10734
- meta: { sortable: true },
10735
- hint: {
10736
- en: "Increases text contrast for better readability (WCAG AAA)",
10737
- es: "Aumenta el contraste del texto para mejor legibilidad (WCAG AAA)"
10738
- }
10739
10586
  })
10740
10587
  },
10741
10588
  casl: {
10742
- subject: "UiAccessibility",
10589
+ subject: "UiStyleGuide",
10743
10590
  permissions: {
10744
10591
  ADMIN: { actions: ["read", "update"] }
10745
10592
  }
@@ -10749,44 +10596,62 @@ var init_ui_accessibility_entity = __esm({
10749
10596
  });
10750
10597
 
10751
10598
  // src/modules/ui-settings/index.ts
10599
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
10600
+ import { join as join6 } from "path";
10752
10601
  var uiSettingsModule;
10753
10602
  var init_ui_settings = __esm({
10754
10603
  "src/modules/ui-settings/index.ts"() {
10755
10604
  "use strict";
10756
- init_ui_branding_entity();
10757
- init_ui_theme_entity();
10758
- init_ui_effects_entity();
10759
- init_ui_accessibility_entity();
10760
- init_ui_branding_entity();
10761
- init_ui_theme_entity();
10762
- init_ui_effects_entity();
10763
- init_ui_accessibility_entity();
10605
+ init_ui_style_guide_entity();
10606
+ init_ui_style_guide_entity();
10764
10607
  uiSettingsModule = {
10765
10608
  name: "ui-settings",
10766
- label: { en: "UI Settings", es: "Configuraci\xF3n de UI" },
10609
+ label: { en: "Style Guide", es: "Gu\xEDa de Estilos" },
10767
10610
  icon: "mdi:palette-outline",
10768
10611
  description: {
10769
- en: "User interface configuration: branding, themes, visual effects, and accessibility",
10770
- es: "Configuraci\xF3n de interfaz de usuario: marca, temas, efectos visuales y accesibilidad"
10612
+ en: "Brand identity and visual style configuration",
10613
+ es: "Configuraci\xF3n de identidad de marca y estilo visual"
10771
10614
  },
10772
10615
  type: "core",
10773
10616
  category: "settings",
10774
10617
  dependencies: ["logger"],
10775
- definitions: [
10776
- uiBrandingEntity,
10777
- uiThemeEntity,
10778
- uiEffectsEntity,
10779
- uiAccessibilityEntity
10780
- ],
10781
- routePrefix: "/ui-settings"
10618
+ definitions: [uiStyleGuideEntity],
10619
+ routePrefix: "/ui-settings",
10620
+ /**
10621
+ * Seed style guide from data/seeds/ui-style-guide.json if it exists.
10622
+ * Idempotent: only inserts if no record exists yet.
10623
+ *
10624
+ * Workflow:
10625
+ * 1. Configure style guide in dev via admin UI
10626
+ * 2. Export: GET /api/v1/ui-settings/ui-style-guide → save to data/seeds/ui-style-guide.json
10627
+ * 3. Commit the seed file
10628
+ * 4. On other environments, seed runs automatically on startup
10629
+ */
10630
+ seed: async (ctx) => {
10631
+ const db2 = ctx.db.knex;
10632
+ const existing = await db2("single_records").where("key", "ui_style_guide").first();
10633
+ if (existing) return;
10634
+ const seedPath = join6(process.cwd(), "data", "seeds", "ui-style-guide.json");
10635
+ if (!existsSync5(seedPath)) return;
10636
+ try {
10637
+ const seedData = JSON.parse(readFileSync4(seedPath, "utf-8"));
10638
+ await db2("single_records").insert({
10639
+ key: "ui_style_guide",
10640
+ value: JSON.stringify(seedData)
10641
+ });
10642
+ ctx.core.logger.info("Seeded ui_style_guide from data/seeds/ui-style-guide.json");
10643
+ } catch {
10644
+ ctx.core.logger.warn("Failed to parse data/seeds/ui-style-guide.json, skipping seed");
10645
+ }
10646
+ }
10782
10647
  };
10783
10648
  }
10784
10649
  });
10785
10650
 
10786
10651
  // src/modules/storage/drivers/filesystem.driver.ts
10787
- import { createReadStream, createWriteStream, existsSync as existsSync5, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
10652
+ import { createReadStream, createWriteStream, existsSync as existsSync6, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
10788
10653
  import { readFile, readdir, stat, copyFile, rename, mkdir } from "fs/promises";
10789
- import { join as join6, dirname as dirname4, extname, basename } from "path";
10654
+ import { join as join7, dirname as dirname4, extname, basename } from "path";
10790
10655
  import { createHash } from "crypto";
10791
10656
  var FilesystemDriver;
10792
10657
  var init_filesystem_driver = __esm({
@@ -10800,7 +10665,7 @@ var init_filesystem_driver = __esm({
10800
10665
  this.basePath = config3.basePath;
10801
10666
  this.baseUrl = config3.baseUrl;
10802
10667
  this.generateId = config3.generateId;
10803
- if (!existsSync5(this.basePath)) {
10668
+ if (!existsSync6(this.basePath)) {
10804
10669
  mkdirSync2(this.basePath, { recursive: true });
10805
10670
  }
10806
10671
  }
@@ -10810,9 +10675,9 @@ var init_filesystem_driver = __esm({
10810
10675
  const diskFilename = `${id}${ext}`;
10811
10676
  const folder = options?.folder || "";
10812
10677
  const relativePath = folder ? `${folder}/${diskFilename}` : diskFilename;
10813
- const fullPath = join6(this.basePath, relativePath);
10678
+ const fullPath = join7(this.basePath, relativePath);
10814
10679
  const dir = dirname4(fullPath);
10815
- if (!existsSync5(dir)) {
10680
+ if (!existsSync6(dir)) {
10816
10681
  mkdirSync2(dir, { recursive: true });
10817
10682
  }
10818
10683
  const hash = createHash("sha256").update(buffer).digest("hex");
@@ -10836,25 +10701,25 @@ var init_filesystem_driver = __esm({
10836
10701
  };
10837
10702
  }
10838
10703
  async get(path4) {
10839
- const fullPath = join6(this.basePath, path4);
10840
- if (!existsSync5(fullPath)) {
10704
+ const fullPath = join7(this.basePath, path4);
10705
+ if (!existsSync6(fullPath)) {
10841
10706
  throw new Error(`File not found: ${path4}`);
10842
10707
  }
10843
10708
  return createReadStream(fullPath);
10844
10709
  }
10845
10710
  async getBuffer(path4) {
10846
- const fullPath = join6(this.basePath, path4);
10711
+ const fullPath = join7(this.basePath, path4);
10847
10712
  return readFile(fullPath);
10848
10713
  }
10849
10714
  async delete(path4) {
10850
- const fullPath = join6(this.basePath, path4);
10851
- if (existsSync5(fullPath)) {
10715
+ const fullPath = join7(this.basePath, path4);
10716
+ if (existsSync6(fullPath)) {
10852
10717
  unlinkSync(fullPath);
10853
10718
  }
10854
10719
  }
10855
10720
  async exists(path4) {
10856
- const fullPath = join6(this.basePath, path4);
10857
- return existsSync5(fullPath);
10721
+ const fullPath = join7(this.basePath, path4);
10722
+ return existsSync6(fullPath);
10858
10723
  }
10859
10724
  getUrl(path4) {
10860
10725
  if (!this.baseUrl) return null;
@@ -10864,14 +10729,14 @@ var init_filesystem_driver = __esm({
10864
10729
  // EXTENDED METHODS
10865
10730
  // ============================================================================
10866
10731
  async list(folder) {
10867
- const targetPath = folder ? join6(this.basePath, folder) : this.basePath;
10868
- if (!existsSync5(targetPath)) {
10732
+ const targetPath = folder ? join7(this.basePath, folder) : this.basePath;
10733
+ if (!existsSync6(targetPath)) {
10869
10734
  return [];
10870
10735
  }
10871
10736
  const entries = await readdir(targetPath, { withFileTypes: true });
10872
10737
  const results = [];
10873
10738
  for (const entry of entries) {
10874
- const entryPath = join6(targetPath, entry.name);
10739
+ const entryPath = join7(targetPath, entry.name);
10875
10740
  const relativePath = folder ? `${folder}/${entry.name}` : entry.name;
10876
10741
  const stats = await stat(entryPath);
10877
10742
  results.push({
@@ -10885,10 +10750,10 @@ var init_filesystem_driver = __esm({
10885
10750
  return results;
10886
10751
  }
10887
10752
  async copy(src, dst) {
10888
- const srcPath = join6(this.basePath, src);
10889
- const dstPath = join6(this.basePath, dst);
10753
+ const srcPath = join7(this.basePath, src);
10754
+ const dstPath = join7(this.basePath, dst);
10890
10755
  const dstDir = dirname4(dstPath);
10891
- if (!existsSync5(dstDir)) {
10756
+ if (!existsSync6(dstDir)) {
10892
10757
  await mkdir(dstDir, { recursive: true });
10893
10758
  }
10894
10759
  await copyFile(srcPath, dstPath);
@@ -10911,10 +10776,10 @@ var init_filesystem_driver = __esm({
10911
10776
  };
10912
10777
  }
10913
10778
  async move(src, dst) {
10914
- const srcPath = join6(this.basePath, src);
10915
- const dstPath = join6(this.basePath, dst);
10779
+ const srcPath = join7(this.basePath, src);
10780
+ const dstPath = join7(this.basePath, dst);
10916
10781
  const dstDir = dirname4(dstPath);
10917
- if (!existsSync5(dstDir)) {
10782
+ if (!existsSync6(dstDir)) {
10918
10783
  await mkdir(dstDir, { recursive: true });
10919
10784
  }
10920
10785
  await rename(srcPath, dstPath);
@@ -10937,7 +10802,7 @@ var init_filesystem_driver = __esm({
10937
10802
  };
10938
10803
  }
10939
10804
  async getMetadata(path4) {
10940
- const fullPath = join6(this.basePath, path4);
10805
+ const fullPath = join7(this.basePath, path4);
10941
10806
  const stats = await stat(fullPath);
10942
10807
  return {
10943
10808
  path: path4,
@@ -10951,8 +10816,8 @@ var init_filesystem_driver = __esm({
10951
10816
  throw new Error("Signed URLs are not supported by the filesystem driver");
10952
10817
  }
10953
10818
  async createFolder(path4) {
10954
- const fullPath = join6(this.basePath, path4);
10955
- if (!existsSync5(fullPath)) {
10819
+ const fullPath = join7(this.basePath, path4);
10820
+ if (!existsSync6(fullPath)) {
10956
10821
  await mkdir(fullPath, { recursive: true });
10957
10822
  }
10958
10823
  }
@@ -11183,8 +11048,8 @@ var init_s3_driver = __esm({
11183
11048
  });
11184
11049
 
11185
11050
  // src/modules/storage/storage.config.ts
11186
- import { join as join7, isAbsolute } from "path";
11187
- import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
11051
+ import { join as join8, isAbsolute } from "path";
11052
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
11188
11053
  function getDefaultScope(driver) {
11189
11054
  return driver === "s3" ? DEFAULT_S3_SCOPE : DEFAULT_FILESYSTEM_SCOPE;
11190
11055
  }
@@ -11199,7 +11064,7 @@ function resolveStoragePath(path4, projPath) {
11199
11064
  return path4;
11200
11065
  }
11201
11066
  const cleanPath = path4.startsWith("./") ? path4.slice(2) : path4;
11202
- return join7(projPath, "data", cleanPath);
11067
+ return join8(projPath, "data", cleanPath);
11203
11068
  }
11204
11069
  async function getConfigByScope(db2, scope) {
11205
11070
  if (!generateIdFn) {
@@ -11230,7 +11095,7 @@ function buildConfigFromRow(row) {
11230
11095
  if (row.driver === "filesystem") {
11231
11096
  const fsMeta = metadata;
11232
11097
  config3.basePath = resolveStoragePath(fsMeta.basePath || "./storage", projectPath);
11233
- if (!existsSync6(config3.basePath)) {
11098
+ if (!existsSync7(config3.basePath)) {
11234
11099
  mkdirSync3(config3.basePath, { recursive: true });
11235
11100
  }
11236
11101
  } else if (row.driver === "s3") {
@@ -11262,7 +11127,7 @@ function buildConfigFromEnv(driver) {
11262
11127
  if (driver === "filesystem") {
11263
11128
  const rawPath = process.env["STORAGE_PATH"] || "./storage";
11264
11129
  config3.basePath = resolveStoragePath(rawPath, projectPath);
11265
- if (!existsSync6(config3.basePath)) {
11130
+ if (!existsSync7(config3.basePath)) {
11266
11131
  mkdirSync3(config3.basePath, { recursive: true });
11267
11132
  }
11268
11133
  } else if (driver === "s3") {
@@ -11701,7 +11566,7 @@ var init_storage_service = __esm({
11701
11566
  });
11702
11567
 
11703
11568
  // src/modules/storage/storage.entity.ts
11704
- import { useIdField as useIdField3, useTextField as useTextField8, useSelectField as useSelectField9, useUrlField, useNumberField as useNumberField4, useCheckboxField as useCheckboxField3, useJsonField as useJsonField2, useMetadataField, usePublicField } from "@gzl10/nexus-sdk/fields";
11569
+ import { useIdField as useIdField3, useTextField as useTextField8, useSelectField as useSelectField7, useUrlField, useNumberField as useNumberField5, useCheckboxField as useCheckboxField3, useJsonField as useJsonField2, useMetadataField, usePublicField } from "@gzl10/nexus-sdk/fields";
11705
11570
  var DEFAULT_MAX_SIZE2, storageConfigEntity, storageFilesEntity;
11706
11571
  var init_storage_entity = __esm({
11707
11572
  "src/modules/storage/storage.entity.ts"() {
@@ -11736,7 +11601,7 @@ var init_storage_entity = __esm({
11736
11601
  hint: { en: "Unique identifier (e.g. default_filesystem, default_s3)", es: "Identificador \xFAnico (ej: default_filesystem, default_s3)" }
11737
11602
  }),
11738
11603
  driver: {
11739
- ...useSelectField9({
11604
+ ...useSelectField7({
11740
11605
  label: { en: "Driver", es: "Controlador" },
11741
11606
  required: true,
11742
11607
  hint: { en: "Storage backend", es: "Backend de almacenamiento" },
@@ -11753,7 +11618,7 @@ var init_storage_entity = __esm({
11753
11618
  size: 500,
11754
11619
  nullable: true
11755
11620
  }),
11756
- max_file_size: useNumberField4({
11621
+ max_file_size: useNumberField5({
11757
11622
  label: { en: "Max File Size (bytes)", es: "Tama\xF1o m\xE1ximo de archivo (bytes)" },
11758
11623
  hint: { en: "Maximum size in bytes (default: 10MB = 10485760)", es: "Tama\xF1o m\xE1ximo en bytes (default: 10MB = 10485760)" },
11759
11624
  nullable: false,
@@ -11946,7 +11811,7 @@ var init_storage_entity = __esm({
11946
11811
  index: true,
11947
11812
  meta: { sortable: true, searchable: true }
11948
11813
  }),
11949
- size: useNumberField4({
11814
+ size: useNumberField5({
11950
11815
  label: { en: "Size", es: "Tama\xF1o" },
11951
11816
  required: true,
11952
11817
  nullable: false,
@@ -12488,7 +12353,7 @@ var init_storage = __esm({
12488
12353
  });
12489
12354
 
12490
12355
  // src/modules/users/users.entity.ts
12491
- import { useIdField as useIdField4, useSelectField as useSelectField10, useEmailField, usePasswordField, useTextField as useTextField9, useDatetimeField as useDatetimeField3, useCheckboxField as useCheckboxField4, useImageField as useImageField2, useNameField as useNameField2, useMetadataField as useMetadataField2, useDescriptionField as useDescriptionField2 } from "@gzl10/nexus-sdk/fields";
12356
+ import { useIdField as useIdField4, useSelectField as useSelectField8, useEmailField, usePasswordField, useTextField as useTextField9, useDatetimeField as useDatetimeField3, useCheckboxField as useCheckboxField4, useImageField as useImageField2, useNameField as useNameField2, useMetadataField as useMetadataField2, useDescriptionField as useDescriptionField2 } from "@gzl10/nexus-sdk/fields";
12492
12357
  import { z as z3 } from "zod";
12493
12358
  var userEntity, roleEntity, userRoleEntity;
12494
12359
  var init_users_entity = __esm({
@@ -12562,7 +12427,7 @@ var init_users_entity = __esm({
12562
12427
  label: { en: "Marketing Opt-in", es: "Aceptar marketing" },
12563
12428
  meta: { exportable: true, showInForm: false, showInDisplay: false }
12564
12429
  }),
12565
- locale: useSelectField10({
12430
+ locale: useSelectField8({
12566
12431
  label: { en: "Language", es: "Idioma" },
12567
12432
  options: [
12568
12433
  { value: "es", label: { en: "Spanish", es: "Espa\xF1ol" } },
@@ -12572,13 +12437,13 @@ var init_users_entity = __esm({
12572
12437
  meta: { sortable: true },
12573
12438
  defaultValue: "en"
12574
12439
  }),
12575
- timezone: useSelectField10({
12440
+ timezone: useSelectField8({
12576
12441
  label: { en: "Timezone", es: "Zona horaria" },
12577
12442
  master: "timezones",
12578
12443
  meta: { sortable: true },
12579
12444
  defaultValue: "timezones:Europe/Madrid"
12580
12445
  }),
12581
- type: useSelectField10({
12446
+ type: useSelectField8({
12582
12447
  label: { en: "Type", es: "Tipo" },
12583
12448
  defaultValue: "human",
12584
12449
  options: [
@@ -12747,7 +12612,7 @@ var init_users_entity = __esm({
12747
12612
  expose: false,
12748
12613
  fields: {
12749
12614
  id: useIdField4(),
12750
- user_id: useSelectField10({
12615
+ user_id: useSelectField8({
12751
12616
  label: { en: "User", es: "Usuario" },
12752
12617
  required: true,
12753
12618
  table: "users",
@@ -12758,7 +12623,7 @@ var init_users_entity = __esm({
12758
12623
  labelField: "name",
12759
12624
  meta: { searchable: true }
12760
12625
  }),
12761
- role_id: useSelectField10({
12626
+ role_id: useSelectField8({
12762
12627
  label: { en: "Role", es: "Rol" },
12763
12628
  required: true,
12764
12629
  table: "roles",
@@ -13702,7 +13567,7 @@ var init_users = __esm({
13702
13567
  });
13703
13568
 
13704
13569
  // src/modules/auth/auth.entity.ts
13705
- import { useIdField as useIdField5, useTextField as useTextField10, useSelectField as useSelectField11, useDatetimeField as useDatetimeField4, useEmailField as useEmailField2, useMetadataField as useMetadataField3, useExpiresAtField } from "@gzl10/nexus-sdk/fields";
13570
+ import { useIdField as useIdField5, useTextField as useTextField10, useSelectField as useSelectField9, useDatetimeField as useDatetimeField4, useEmailField as useEmailField2, useMetadataField as useMetadataField3, useExpiresAtField } from "@gzl10/nexus-sdk/fields";
13706
13571
  var refreshTokenEntity, authIdentitiesEntity;
13707
13572
  var init_auth_entity = __esm({
13708
13573
  "src/modules/auth/auth.entity.ts"() {
@@ -13783,7 +13648,7 @@ var init_auth_entity = __esm({
13783
13648
  order: 5,
13784
13649
  fields: {
13785
13650
  id: useIdField5(),
13786
- user_id: useSelectField11({
13651
+ user_id: useSelectField9({
13787
13652
  label: { en: "User", es: "Usuario" },
13788
13653
  table: "users",
13789
13654
  column: "id",
@@ -13867,8 +13732,327 @@ var init_auth_routes = __esm({
13867
13732
  }
13868
13733
  });
13869
13734
 
13870
- // src/modules/auth/auth.config.ts
13735
+ // src/config/env.ts
13871
13736
  import { z as z5 } from "zod";
13737
+ function resolveConfig() {
13738
+ env = envSchema.parse(process.env);
13739
+ process.env["TZ"] = env.TZ;
13740
+ resolvedConfig = {
13741
+ nodeEnv: env.NODE_ENV,
13742
+ port: env.PORT,
13743
+ host: "0.0.0.0",
13744
+ ui: {
13745
+ enabled: env.NEXUS_UI_ENABLED,
13746
+ base: env.NEXUS_UI_BASE,
13747
+ path: env.NEXUS_UI_PATH
13748
+ },
13749
+ corsOrigin: env.CORS_ORIGIN,
13750
+ databaseUrl: env.DATABASE_URL,
13751
+ adminEmail: env.ADMIN_EMAIL,
13752
+ adminPassword: env.ADMIN_PASSWORD,
13753
+ timezone: env.TZ
13754
+ };
13755
+ return resolvedConfig;
13756
+ }
13757
+ function getConfig() {
13758
+ if (!resolvedConfig) {
13759
+ return resolveConfig();
13760
+ }
13761
+ return resolvedConfig;
13762
+ }
13763
+ function resetConfig() {
13764
+ resolvedConfig = null;
13765
+ }
13766
+ var envSchema, env, resolvedConfig;
13767
+ var init_env = __esm({
13768
+ "src/config/env.ts"() {
13769
+ "use strict";
13770
+ envSchema = z5.object({
13771
+ NODE_ENV: z5.enum(["development", "production", "test"]).default("development"),
13772
+ PORT: z5.coerce.number().default(3e3),
13773
+ CORS_ORIGIN: z5.string().default("*"),
13774
+ BACKEND_URL: z5.string().optional(),
13775
+ DATABASE_URL: z5.string().default("file:./dev.db"),
13776
+ REDIS_URL: z5.string().url().optional(),
13777
+ REDIS_PREFIX: z5.string().default("nexus"),
13778
+ ADMIN_EMAIL: z5.string().email().optional(),
13779
+ ADMIN_PASSWORD: z5.string().min(6).optional(),
13780
+ COOKIE_DOMAIN: z5.string().optional(),
13781
+ TZ: z5.string().default("UTC"),
13782
+ TRUST_PROXY: z5.coerce.boolean().default(false),
13783
+ NEXUS_UI_ENABLED: z5.coerce.boolean().default(true),
13784
+ NEXUS_UI_BASE: z5.string().default("/"),
13785
+ NEXUS_UI_PATH: z5.string().default("../ui/dist"),
13786
+ NEXUS_CHECK_DRIFT: z5.coerce.boolean().optional(),
13787
+ NEXUS_FAIL_ON_DRIFT: z5.coerce.boolean().optional(),
13788
+ FRPC_SERVER: z5.string().optional(),
13789
+ FRPC_SERVER_PORT: z5.coerce.number().default(7e3),
13790
+ FRPC_TOKEN: z5.string().optional(),
13791
+ FRPC_SUBDOMAIN: z5.string().optional()
13792
+ });
13793
+ env = envSchema.parse(process.env);
13794
+ process.env["TZ"] = env.TZ;
13795
+ resolvedConfig = null;
13796
+ }
13797
+ });
13798
+
13799
+ // src/db/sql-utils.ts
13800
+ function extractTableFromSql(sql, type2) {
13801
+ const match = sql.match(SQL_PATTERNS[type2]);
13802
+ return match?.[1];
13803
+ }
13804
+ function extractTableFromSelect(sql) {
13805
+ return extractTableFromSql(sql, "select");
13806
+ }
13807
+ function extractTableFromInsert(sql) {
13808
+ return extractTableFromSql(sql, "insert");
13809
+ }
13810
+ function extractTableFromUpdate(sql) {
13811
+ return extractTableFromSql(sql, "update");
13812
+ }
13813
+ function extractTableFromDelete(sql) {
13814
+ return extractTableFromSql(sql, "delete");
13815
+ }
13816
+ var SQL_PATTERNS;
13817
+ var init_sql_utils = __esm({
13818
+ "src/db/sql-utils.ts"() {
13819
+ "use strict";
13820
+ SQL_PATTERNS = {
13821
+ select: /from\s+["'`]?(\w+)["'`]?/i,
13822
+ insert: /insert into\s+["'`]?(\w+)["'`]?/i,
13823
+ update: /update\s+["'`]?(\w+)["'`]?/i,
13824
+ delete: /delete from\s+["'`]?(\w+)["'`]?/i
13825
+ };
13826
+ }
13827
+ });
13828
+
13829
+ // src/db/sqlite-compat.ts
13830
+ function createSqliteBooleanProcessor() {
13831
+ const booleanColumns = /* @__PURE__ */ new Map();
13832
+ function registerBooleanColumn2(table, column) {
13833
+ if (!booleanColumns.has(table)) {
13834
+ booleanColumns.set(table, /* @__PURE__ */ new Set());
13835
+ }
13836
+ booleanColumns.get(table).add(column);
13837
+ }
13838
+ function convertBooleans2(table, row) {
13839
+ const columns = booleanColumns.get(table);
13840
+ if (!columns || columns.size === 0) return row;
13841
+ const result = { ...row };
13842
+ for (const column of columns) {
13843
+ if (column in result) {
13844
+ const value = result[column];
13845
+ if (value === 0 || value === 1) {
13846
+ result[column] = value === 1;
13847
+ }
13848
+ }
13849
+ }
13850
+ return result;
13851
+ }
13852
+ function postProcess(result, queryContext) {
13853
+ const sql = queryContext?.sql?.toLowerCase() ?? "";
13854
+ if (!sql.startsWith("select")) return result;
13855
+ const table = extractTableFromSelect(sql);
13856
+ if (!table) return result;
13857
+ if (Array.isArray(result)) {
13858
+ return result.map(
13859
+ (row) => typeof row === "object" && row !== null ? convertBooleans2(table, row) : row
13860
+ );
13861
+ }
13862
+ if (typeof result === "object" && result !== null) {
13863
+ return convertBooleans2(table, result);
13864
+ }
13865
+ return result;
13866
+ }
13867
+ function clear() {
13868
+ booleanColumns.clear();
13869
+ }
13870
+ return { registerBooleanColumn: registerBooleanColumn2, convertBooleans: convertBooleans2, postProcess, clear };
13871
+ }
13872
+ var defaultProcessor, registerBooleanColumn, convertBooleans, sqlitePostProcess;
13873
+ var init_sqlite_compat = __esm({
13874
+ "src/db/sqlite-compat.ts"() {
13875
+ "use strict";
13876
+ init_sql_utils();
13877
+ defaultProcessor = createSqliteBooleanProcessor();
13878
+ registerBooleanColumn = defaultProcessor.registerBooleanColumn;
13879
+ convertBooleans = defaultProcessor.convertBooleans;
13880
+ sqlitePostProcess = defaultProcessor.postProcess;
13881
+ }
13882
+ });
13883
+
13884
+ // src/config/database.ts
13885
+ import { join as join9, dirname as dirname6, isAbsolute as isAbsolute2 } from "path";
13886
+ import { mkdirSync as mkdirSync4 } from "fs";
13887
+ function getDatabaseConfig() {
13888
+ const url = getConfig().databaseUrl;
13889
+ if (IN_MEMORY_RE.test(url)) {
13890
+ return {
13891
+ client: "better-sqlite3",
13892
+ connection: { filename: ":memory:" },
13893
+ useNullAsDefault: true,
13894
+ postProcessResponse: sqlitePostProcess,
13895
+ pool: { min: 0, max: 1 }
13896
+ };
13897
+ }
13898
+ if (url.startsWith("file:") || url.startsWith("sqlite:")) {
13899
+ let filename = url.replace(/^(file:|sqlite:)/, "");
13900
+ if (!isAbsolute2(filename)) {
13901
+ filename = join9(getProjectPath(), "data", filename);
13902
+ }
13903
+ mkdirSync4(dirname6(filename), { recursive: true });
13904
+ return {
13905
+ client: "better-sqlite3",
13906
+ connection: { filename },
13907
+ useNullAsDefault: true,
13908
+ postProcessResponse: sqlitePostProcess
13909
+ };
13910
+ }
13911
+ if (url.startsWith("postgresql://") || url.startsWith("postgres://")) {
13912
+ return {
13913
+ client: "pg",
13914
+ connection: url,
13915
+ pool: { min: 2, max: 10 }
13916
+ };
13917
+ }
13918
+ if (url.startsWith("mysql://")) {
13919
+ const offsetMinutes = (/* @__PURE__ */ new Date()).getTimezoneOffset();
13920
+ const offsetHours = Math.abs(Math.floor(offsetMinutes / 60));
13921
+ const offsetMins = Math.abs(offsetMinutes % 60);
13922
+ const sign = offsetMinutes <= 0 ? "+" : "-";
13923
+ const tzOffset = `${sign}${String(offsetHours).padStart(2, "0")}:${String(offsetMins).padStart(2, "0")}`;
13924
+ return {
13925
+ client: "mysql2",
13926
+ connection: {
13927
+ uri: url,
13928
+ timezone: tzOffset
13929
+ // mysql2 requiere formato "+HH:MM"
13930
+ },
13931
+ pool: { min: 2, max: 10 }
13932
+ };
13933
+ }
13934
+ throw new Error(`Unsupported database URL: ${url}`);
13935
+ }
13936
+ function getDatabaseType() {
13937
+ const url = getConfig().databaseUrl;
13938
+ if (IN_MEMORY_RE.test(url) || url.startsWith("file:") || url.startsWith("sqlite:")) return "sqlite";
13939
+ if (url.startsWith("postgresql://") || url.startsWith("postgres://")) return "postgresql";
13940
+ if (url.startsWith("mysql://")) return "mysql";
13941
+ return "sqlite";
13942
+ }
13943
+ function getDatabasePath() {
13944
+ const url = getConfig().databaseUrl;
13945
+ if (IN_MEMORY_RE.test(url)) return ":memory:";
13946
+ if (url.startsWith("file:") || url.startsWith("sqlite:")) {
13947
+ let filename = url.replace(/^(file:|sqlite:)/, "");
13948
+ if (!isAbsolute2(filename)) {
13949
+ filename = join9(getProjectPath(), "data", filename);
13950
+ }
13951
+ return filename;
13952
+ }
13953
+ try {
13954
+ const parsed = new URL(url);
13955
+ parsed.password = "***";
13956
+ return parsed.toString();
13957
+ } catch {
13958
+ return url.replace(/:\/\/[^@]+@/, "://***@");
13959
+ }
13960
+ }
13961
+ var IN_MEMORY_RE;
13962
+ var init_database = __esm({
13963
+ "src/config/database.ts"() {
13964
+ "use strict";
13965
+ init_env();
13966
+ init_paths();
13967
+ init_sqlite_compat();
13968
+ IN_MEMORY_RE = /^((file:|sqlite:)?:memory:?)$/;
13969
+ }
13970
+ });
13971
+
13972
+ // src/core/crypto/secret-resolver.ts
13973
+ import { randomBytes } from "crypto";
13974
+ function setSecretResolverDb(db2) {
13975
+ _db = db2;
13976
+ }
13977
+ function _resetSecretResolver() {
13978
+ _secret = null;
13979
+ _db = null;
13980
+ }
13981
+ async function initAuthSecret(logger2) {
13982
+ const envSecret = process.env["AUTH_SECRET"];
13983
+ if (envSecret) {
13984
+ if (envSecret.length < 32) {
13985
+ throw new Error(`AUTH_SECRET must be at least 32 characters (current: ${envSecret.length})`);
13986
+ }
13987
+ _secret = envSecret;
13988
+ return;
13989
+ }
13990
+ if (!_db) {
13991
+ throw new Error("Secret resolver: database not initialized. Call setSecretResolverDb() first.");
13992
+ }
13993
+ const existing = await _db(SINGLE_RECORDS).where("key", SECRET_KEY).first();
13994
+ if (existing?.value) {
13995
+ const parsed2 = typeof existing.value === "string" ? JSON.parse(existing.value) : existing.value;
13996
+ if (parsed2.secret) {
13997
+ _secret = parsed2.secret;
13998
+ logger2.info("AUTH_SECRET loaded from database");
13999
+ warnIfProduction(logger2);
14000
+ return;
14001
+ }
14002
+ }
14003
+ const generated = randomBytes(SECRET_LENGTH).toString("base64url");
14004
+ const value = JSON.stringify({ secret: generated });
14005
+ const id = randomBytes(13).toString("base64url");
14006
+ const now = /* @__PURE__ */ new Date();
14007
+ const dialect = getDatabaseType();
14008
+ if (dialect === "postgresql") {
14009
+ await _db.raw(
14010
+ `INSERT INTO ${SINGLE_RECORDS} (id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (key) DO NOTHING`,
14011
+ [id, SECRET_KEY, value, now, now]
14012
+ );
14013
+ } else if (dialect === "mysql") {
14014
+ await _db.raw(
14015
+ "INSERT IGNORE INTO `single_records` (id, `key`, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
14016
+ [id, SECRET_KEY, value, now, now]
14017
+ );
14018
+ } else {
14019
+ await _db.raw(
14020
+ `INSERT OR IGNORE INTO ${SINGLE_RECORDS} (id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`,
14021
+ [id, SECRET_KEY, value, now, now]
14022
+ );
14023
+ }
14024
+ const persisted = await _db(SINGLE_RECORDS).where("key", SECRET_KEY).first();
14025
+ const parsed = typeof persisted.value === "string" ? JSON.parse(persisted.value) : persisted.value;
14026
+ _secret = parsed.secret;
14027
+ logger2.info("AUTH_SECRET auto-generated and persisted in database");
14028
+ warnIfProduction(logger2);
14029
+ }
14030
+ function warnIfProduction(logger2) {
14031
+ if (process.env["NODE_ENV"] === "production") {
14032
+ logger2.warn("AUTH_SECRET not set via environment. Using auto-generated secret from database. For multi-instance deployments, set AUTH_SECRET explicitly.");
14033
+ }
14034
+ }
14035
+ function getAuthSecret() {
14036
+ if (_secret) return _secret;
14037
+ const envSecret = process.env["AUTH_SECRET"];
14038
+ if (envSecret) return envSecret;
14039
+ throw new Error("AUTH_SECRET not initialized. Call initAuthSecret() during startup.");
14040
+ }
14041
+ var SINGLE_RECORDS, SECRET_KEY, SECRET_LENGTH, _secret, _db;
14042
+ var init_secret_resolver = __esm({
14043
+ "src/core/crypto/secret-resolver.ts"() {
14044
+ "use strict";
14045
+ init_database();
14046
+ SINGLE_RECORDS = "single_records";
14047
+ SECRET_KEY = "nexus_auth_secret";
14048
+ SECRET_LENGTH = 48;
14049
+ _secret = null;
14050
+ _db = null;
14051
+ }
14052
+ });
14053
+
14054
+ // src/modules/auth/auth.config.ts
14055
+ import { z as z6 } from "zod";
13872
14056
  function configError(module, errors) {
13873
14057
  console.error(`
13874
14058
  ${"\u2550".repeat(60)}`);
@@ -13883,12 +14067,6 @@ function parseAuthEnv() {
13883
14067
  if (!result.success) {
13884
14068
  const errors = result.error.issues.map((issue) => {
13885
14069
  const path4 = issue.path.join(".");
13886
- if (path4 === "AUTH_SECRET" && issue.code === "invalid_type") {
13887
- return "AUTH_SECRET is required. Set it in your .env file or environment.";
13888
- }
13889
- if (path4 === "AUTH_SECRET" && issue.code === "too_small") {
13890
- return `AUTH_SECRET must be at least 32 characters (current: ${String(process.env["AUTH_SECRET"]).length})`;
13891
- }
13892
14070
  return `${path4}: ${issue.message}`;
13893
14071
  });
13894
14072
  configError("auth", errors);
@@ -13907,7 +14085,7 @@ function getAuthEnv() {
13907
14085
  function getAuthConfig() {
13908
14086
  const authEnv = getAuthEnv();
13909
14087
  return {
13910
- secret: authEnv.AUTH_SECRET,
14088
+ secret: getAuthSecret(),
13911
14089
  accessExpires: authEnv.AUTH_ACCESS_EXPIRES,
13912
14090
  refreshExpires: authEnv.AUTH_REFRESH_EXPIRES,
13913
14091
  rateLimitMax: authEnv.AUTH_RATE_LIMIT_MAX,
@@ -13924,24 +14102,24 @@ var authEnvSchema, _authEnv;
13924
14102
  var init_auth_config = __esm({
13925
14103
  "src/modules/auth/auth.config.ts"() {
13926
14104
  "use strict";
13927
- authEnvSchema = z5.object({
13928
- AUTH_SECRET: z5.string().min(32, "AUTH_SECRET must be at least 32 characters"),
13929
- AUTH_ACCESS_EXPIRES: z5.string().default("15m"),
13930
- AUTH_REFRESH_EXPIRES: z5.string().default("7d"),
14105
+ init_secret_resolver();
14106
+ authEnvSchema = z6.object({
14107
+ AUTH_ACCESS_EXPIRES: z6.string().default("15m"),
14108
+ AUTH_REFRESH_EXPIRES: z6.string().default("7d"),
13931
14109
  // Rate limiting (default: 5 requests per 15 minutes)
13932
- AUTH_RATE_LIMIT_MAX: z5.coerce.number().default(5),
13933
- AUTH_RATE_LIMIT_WINDOW: z5.coerce.number().default(900),
14110
+ AUTH_RATE_LIMIT_MAX: z6.coerce.number().default(5),
14111
+ AUTH_RATE_LIMIT_WINDOW: z6.coerce.number().default(900),
13934
14112
  // seconds
13935
14113
  // Cookie domain for SSO across subdomains (e.g., '.example.com')
13936
- AUTH_COOKIE_DOMAIN: z5.string().optional(),
14114
+ AUTH_COOKIE_DOMAIN: z6.string().optional(),
13937
14115
  // Challenge threshold: failed attempts before requiring OTP (default: 2)
13938
- AUTH_CHALLENGE_THRESHOLD: z5.coerce.number().default(2),
14116
+ AUTH_CHALLENGE_THRESHOLD: z6.coerce.number().default(2),
13939
14117
  // Skip OTP verification for registration (DEVELOPMENT ONLY - rejected in production)
13940
- AUTH_SKIP_REGISTER_OTP: z5.coerce.boolean().default(false),
14118
+ AUTH_SKIP_REGISTER_OTP: z6.coerce.boolean().default(false),
13941
14119
  // Disable self-registration via POST /auth/register (default: false)
13942
- AUTH_DISABLE_REGISTRATION: z5.coerce.boolean().default(false),
14120
+ AUTH_DISABLE_REGISTRATION: z6.coerce.boolean().default(false),
13943
14121
  // Disable auto-creation of users on first OIDC login (default: false)
13944
- AUTH_DISABLE_AUTO_CREATE: z5.coerce.boolean().default(false)
14122
+ AUTH_DISABLE_AUTO_CREATE: z6.coerce.boolean().default(false)
13945
14123
  });
13946
14124
  }
13947
14125
  });
@@ -14099,7 +14277,7 @@ var init_auth_middleware = __esm({
14099
14277
  });
14100
14278
 
14101
14279
  // src/modules/auth/auth.pat.entity.ts
14102
- import { useIdField as useIdField6, useTextField as useTextField11, useSelectField as useSelectField12, useDatetimeField as useDatetimeField5, useExpiresAtField as useExpiresAtField2 } from "@gzl10/nexus-sdk/fields";
14280
+ import { useIdField as useIdField6, useTextField as useTextField11, useSelectField as useSelectField10, useDatetimeField as useDatetimeField5, useExpiresAtField as useExpiresAtField2 } from "@gzl10/nexus-sdk/fields";
14103
14281
  var personalTokenEntity;
14104
14282
  var init_auth_pat_entity = __esm({
14105
14283
  "src/modules/auth/auth.pat.entity.ts"() {
@@ -14116,7 +14294,7 @@ var init_auth_pat_entity = __esm({
14116
14294
  routePrefix: "/personal-tokens",
14117
14295
  fields: {
14118
14296
  id: useIdField6(),
14119
- user_id: useSelectField12({
14297
+ user_id: useSelectField10({
14120
14298
  label: { en: "User", es: "Usuario" },
14121
14299
  table: "users",
14122
14300
  column: "id",
@@ -14151,7 +14329,7 @@ var init_auth_pat_entity = __esm({
14151
14329
  unique: true,
14152
14330
  meta: { exportable: false }
14153
14331
  }),
14154
- scope: useSelectField12({
14332
+ scope: useSelectField10({
14155
14333
  label: { en: "Permission", es: "Permiso" },
14156
14334
  required: true,
14157
14335
  options: [
@@ -14174,46 +14352,10 @@ var init_auth_pat_entity = __esm({
14174
14352
  casl: {
14175
14353
  subject: "AuthPersonalToken",
14176
14354
  permissions: {
14177
- "*": [
14178
- { actions: ["read", "delete"], conditions: { user_id: "${user.id}" } }
14179
- ]
14180
- }
14181
- }
14182
- };
14183
- }
14184
- });
14185
-
14186
- // src/modules/auth/actions/providers.action.ts
14187
- var providersAction;
14188
- var init_providers_action = __esm({
14189
- "src/modules/auth/actions/providers.action.ts"() {
14190
- "use strict";
14191
- init_auth_config();
14192
- providersAction = {
14193
- key: "providers",
14194
- label: { en: "Get Auth Providers", es: "Obtener proveedores de autenticaci\xF3n" },
14195
- icon: "mdi:account-key",
14196
- scope: "module",
14197
- hidden: true,
14198
- method: "GET",
14199
- skipAuth: true,
14200
- handler: async (ctx) => {
14201
- const providerServices = ctx.services.getBySuffix(".provider");
14202
- const results = await Promise.all(
14203
- providerServices.map(async ({ service }) => {
14204
- try {
14205
- return await service.getInfo();
14206
- } catch {
14207
- return null;
14208
- }
14209
- })
14210
- );
14211
- const providers = results.filter((info) => info !== null);
14212
- const config3 = getAuthConfig();
14213
- return {
14214
- providers,
14215
- registrationEnabled: !config3.disableRegistration
14216
- };
14355
+ "*": [
14356
+ { actions: ["read", "delete"], conditions: { user_id: "${user.id}" } }
14357
+ ]
14358
+ }
14217
14359
  }
14218
14360
  };
14219
14361
  }
@@ -14775,7 +14917,6 @@ var authActions;
14775
14917
  var init_actions2 = __esm({
14776
14918
  "src/modules/auth/actions/index.ts"() {
14777
14919
  "use strict";
14778
- init_providers_action();
14779
14920
  init_login_action();
14780
14921
  init_register_action();
14781
14922
  init_forgot_password_action();
@@ -14791,7 +14932,6 @@ var init_actions2 = __esm({
14791
14932
  init_list_tokens_action();
14792
14933
  init_revoke_token_action();
14793
14934
  init_impersonate_action();
14794
- init_providers_action();
14795
14935
  init_login_action();
14796
14936
  init_register_action();
14797
14937
  init_forgot_password_action();
@@ -14807,7 +14947,6 @@ var init_actions2 = __esm({
14807
14947
  init_list_tokens_action();
14808
14948
  init_revoke_token_action();
14809
14949
  authActions = [
14810
- providersAction,
14811
14950
  loginAction,
14812
14951
  registerAction,
14813
14952
  forgotPasswordAction,
@@ -15026,7 +15165,7 @@ var init_otp_manager = __esm({
15026
15165
  });
15027
15166
 
15028
15167
  // src/modules/auth/auth.service.ts
15029
- import { createHash as createHash4, randomBytes } from "crypto";
15168
+ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
15030
15169
  function createAuthService(ctx) {
15031
15170
  const { errors, abilities, crypto: crypto3 } = ctx.core;
15032
15171
  const { generateId: generateId4 } = ctx.core;
@@ -15443,7 +15582,7 @@ function createAuthService(ctx) {
15443
15582
  },
15444
15583
  // === Personal Access Tokens ===
15445
15584
  async createPersonalToken(userId, input, requestInfo) {
15446
- const rawToken = "nxs_" + randomBytes(32).toString("hex");
15585
+ const rawToken = "nxs_" + randomBytes2(32).toString("hex");
15447
15586
  const tokenHash = createHash4("sha256").update(rawToken).digest("hex");
15448
15587
  const tokenPrefix = "nxs_..." + rawToken.slice(-6);
15449
15588
  const id = generateId4();
@@ -15609,38 +15748,38 @@ var init_auth_service = __esm({
15609
15748
  });
15610
15749
 
15611
15750
  // src/modules/auth/auth.types.ts
15612
- import { z as z6 } from "zod";
15751
+ import { z as z7 } from "zod";
15613
15752
  var loginSchema, registerSchema, forgotPasswordSchema, resetPasswordSchema, createPersonalTokenSchema;
15614
15753
  var init_auth_types = __esm({
15615
15754
  "src/modules/auth/auth.types.ts"() {
15616
15755
  "use strict";
15617
- loginSchema = z6.object({
15618
- email: z6.string().email("Invalid email"),
15619
- password: z6.string().min(1, "Password required"),
15620
- otp: z6.string().length(6).optional(),
15621
- deviceId: z6.string().max(64).optional(),
15622
- deviceName: z6.string().max(100).optional()
15756
+ loginSchema = z7.object({
15757
+ email: z7.string().email("Invalid email"),
15758
+ password: z7.string().min(1, "Password required"),
15759
+ otp: z7.string().length(6).optional(),
15760
+ deviceId: z7.string().max(64).optional(),
15761
+ deviceName: z7.string().max(100).optional()
15623
15762
  });
15624
- registerSchema = z6.object({
15625
- email: z6.string().email("Invalid email"),
15626
- password: z6.string().min(8, "Password must be at least 8 characters"),
15627
- name: z6.string().min(2, "Name must be at least 2 characters"),
15628
- otp: z6.string().length(6).optional(),
15629
- deviceId: z6.string().max(64).optional(),
15630
- deviceName: z6.string().max(100).optional()
15763
+ registerSchema = z7.object({
15764
+ email: z7.string().email("Invalid email"),
15765
+ password: z7.string().min(8, "Password must be at least 8 characters"),
15766
+ name: z7.string().min(2, "Name must be at least 2 characters"),
15767
+ otp: z7.string().length(6).optional(),
15768
+ deviceId: z7.string().max(64).optional(),
15769
+ deviceName: z7.string().max(100).optional()
15631
15770
  });
15632
- forgotPasswordSchema = z6.object({
15633
- email: z6.string().email("Invalid email")
15771
+ forgotPasswordSchema = z7.object({
15772
+ email: z7.string().email("Invalid email")
15634
15773
  });
15635
- resetPasswordSchema = z6.object({
15636
- email: z6.string().email("Invalid email"),
15637
- otp: z6.string().length(6, "OTP must be 6 digits"),
15638
- newPassword: z6.string().min(8, "Password must be at least 8 characters")
15774
+ resetPasswordSchema = z7.object({
15775
+ email: z7.string().email("Invalid email"),
15776
+ otp: z7.string().length(6, "OTP must be 6 digits"),
15777
+ newPassword: z7.string().min(8, "Password must be at least 8 characters")
15639
15778
  });
15640
- createPersonalTokenSchema = z6.object({
15641
- name: z6.string().min(1).max(100),
15642
- scope: z6.enum(["readonly", "readwrite"]),
15643
- expires_at: z6.string().datetime().optional()
15779
+ createPersonalTokenSchema = z7.object({
15780
+ name: z7.string().min(1).max(100),
15781
+ scope: z7.enum(["readonly", "readwrite"]),
15782
+ expires_at: z7.string().datetime().optional()
15644
15783
  });
15645
15784
  }
15646
15785
  });
@@ -15654,12 +15793,12 @@ async function seed3(ctx) {
15654
15793
  const db2 = ctx.db.knex;
15655
15794
  const { logger: logger2, generateId: generateId4 } = ctx.core;
15656
15795
  const { nowTimestamp: nowTimestamp2 } = ctx.db;
15657
- const hasTable = await db2.schema.hasTable(SINGLE_RECORDS);
15796
+ const hasTable = await db2.schema.hasTable(SINGLE_RECORDS2);
15658
15797
  if (!hasTable) {
15659
15798
  logger2.debug("single_records table not found, skipping auth seed");
15660
15799
  return;
15661
15800
  }
15662
- const existing = await db2(SINGLE_RECORDS).where({ key: AUTH_CONFIG_KEY }).first();
15801
+ const existing = await db2(SINGLE_RECORDS2).where({ key: AUTH_CONFIG_KEY }).first();
15663
15802
  if (existing) {
15664
15803
  logger2.debug("Auth config already seeded");
15665
15804
  return;
@@ -15672,7 +15811,7 @@ async function seed3(ctx) {
15672
15811
  rate_limit_window: parseInt(process.env["AUTH_RATE_LIMIT_WINDOW"] || "") || 900,
15673
15812
  cookie_domain: process.env["AUTH_COOKIE_DOMAIN"] || null
15674
15813
  };
15675
- await db2(SINGLE_RECORDS).insert({
15814
+ await db2(SINGLE_RECORDS2).insert({
15676
15815
  id: generateId4(),
15677
15816
  key: AUTH_CONFIG_KEY,
15678
15817
  value: JSON.stringify(defaults),
@@ -15681,11 +15820,11 @@ async function seed3(ctx) {
15681
15820
  });
15682
15821
  logger2.info("Auth config seeded from environment variables");
15683
15822
  }
15684
- var SINGLE_RECORDS, AUTH_CONFIG_KEY;
15823
+ var SINGLE_RECORDS2, AUTH_CONFIG_KEY;
15685
15824
  var init_auth_seed = __esm({
15686
15825
  "src/modules/auth/auth.seed.ts"() {
15687
15826
  "use strict";
15688
- SINGLE_RECORDS = "single_records";
15827
+ SINGLE_RECORDS2 = "single_records";
15689
15828
  AUTH_CONFIG_KEY = "auth_config";
15690
15829
  }
15691
15830
  });
@@ -15751,7 +15890,7 @@ var init_auth = __esm({
15751
15890
  });
15752
15891
 
15753
15892
  // src/modules/mail/mail.config.ts
15754
- import { z as z7 } from "zod";
15893
+ import { z as z8 } from "zod";
15755
15894
  function getMailConfig() {
15756
15895
  return {
15757
15896
  host: mailEnv.SMTP_HOST,
@@ -15765,13 +15904,13 @@ var mailEnvSchema, mailEnv;
15765
15904
  var init_mail_config = __esm({
15766
15905
  "src/modules/mail/mail.config.ts"() {
15767
15906
  "use strict";
15768
- mailEnvSchema = z7.object({
15769
- SMTP_HOST: z7.string().default("localhost"),
15770
- SMTP_PORT: z7.coerce.number().default(1025),
15771
- SMTP_SECURE: z7.string().default("false").transform((v) => v === "true" || v === "1"),
15772
- SMTP_USER: z7.string().optional(),
15773
- SMTP_PASS: z7.string().optional(),
15774
- SMTP_FROM: z7.string().default("noreply@nexus.local")
15907
+ mailEnvSchema = z8.object({
15908
+ SMTP_HOST: z8.string().default("localhost"),
15909
+ SMTP_PORT: z8.coerce.number().default(1025),
15910
+ SMTP_SECURE: z8.string().default("false").transform((v) => v === "true" || v === "1"),
15911
+ SMTP_USER: z8.string().optional(),
15912
+ SMTP_PASS: z8.string().optional(),
15913
+ SMTP_FROM: z8.string().default("noreply@nexus.local")
15775
15914
  });
15776
15915
  mailEnv = mailEnvSchema.parse(process.env);
15777
15916
  }
@@ -15779,8 +15918,8 @@ var init_mail_config = __esm({
15779
15918
 
15780
15919
  // src/modules/mail/mail.service.ts
15781
15920
  import nodemailer from "nodemailer";
15782
- import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
15783
- import { join as join8 } from "path";
15921
+ import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
15922
+ import { join as join10 } from "path";
15784
15923
  function getMailService() {
15785
15924
  if (!mailServiceInstance) {
15786
15925
  throw new Error("MailService not initialized. Call initMailService() first.");
@@ -15797,8 +15936,8 @@ var init_mail_service = __esm({
15797
15936
  "src/modules/mail/mail.service.ts"() {
15798
15937
  "use strict";
15799
15938
  init_mail_config();
15800
- TEMPLATE_REL_PATH = join8("public", "mail", "base.html");
15801
- LOGO_REL_PATH = join8("public", "nexus", "nexus-light-512.png");
15939
+ TEMPLATE_REL_PATH = join10("public", "mail", "base.html");
15940
+ LOGO_REL_PATH = join10("public", "nexus", "nexus-light-512.png");
15802
15941
  mailServiceInstance = null;
15803
15942
  MailService = class {
15804
15943
  transporter;
@@ -15812,10 +15951,10 @@ var init_mail_service = __esm({
15812
15951
  this.logger = logger2.child({ service: "mail" });
15813
15952
  this.loggerService = loggerService;
15814
15953
  const libPath = options?.libPath ?? process.cwd();
15815
- this.template = readFileSync4(join8(libPath, TEMPLATE_REL_PATH), "utf-8");
15816
- const logoPath = join8(libPath, LOGO_REL_PATH);
15817
- if (existsSync7(logoPath)) {
15818
- const logoBase64 = readFileSync4(logoPath).toString("base64");
15954
+ this.template = readFileSync5(join10(libPath, TEMPLATE_REL_PATH), "utf-8");
15955
+ const logoPath = join10(libPath, LOGO_REL_PATH);
15956
+ if (existsSync8(logoPath)) {
15957
+ const logoBase64 = readFileSync5(logoPath).toString("base64");
15819
15958
  this.defaultLogoUrl = `data:image/png;base64,${logoBase64}`;
15820
15959
  } else {
15821
15960
  this.defaultLogoUrl = "";
@@ -15907,7 +16046,7 @@ var init_mail_service = __esm({
15907
16046
  });
15908
16047
 
15909
16048
  // src/modules/mail/mail.entity.ts
15910
- import { useIdField as useIdField7, useTextField as useTextField12, useSelectField as useSelectField13, useNumberField as useNumberField5, useSwitchField as useSwitchField4, useEmailField as useEmailField3, usePasswordField as usePasswordField2, useTextareaField as useTextareaField3, useTagsField as useTagsField2, useDatetimeField as useDatetimeField6 } from "@gzl10/nexus-sdk/fields";
16049
+ import { useIdField as useIdField7, useTextField as useTextField12, useSelectField as useSelectField11, useNumberField as useNumberField6, useSwitchField as useSwitchField3, useEmailField as useEmailField3, usePasswordField as usePasswordField2, useTextareaField as useTextareaField3, useTagsField as useTagsField2, useDatetimeField as useDatetimeField6 } from "@gzl10/nexus-sdk/fields";
15911
16050
  import nodemailer2 from "nodemailer";
15912
16051
  async function getMailConfigFromDB(ctx) {
15913
16052
  const configService = ctx.services["config"];
@@ -15976,12 +16115,12 @@ var init_mail_entity = __esm({
15976
16115
  nullable: false,
15977
16116
  hint: { en: "Default: SMTP_HOST env var", es: "Por defecto: variable SMTP_HOST" }
15978
16117
  }),
15979
- port: useNumberField5({
16118
+ port: useNumberField6({
15980
16119
  label: { en: "Port", es: "Puerto" },
15981
16120
  nullable: false,
15982
16121
  hint: { en: "Default: SMTP_PORT env var", es: "Por defecto: variable SMTP_PORT" }
15983
16122
  }),
15984
- secure: useSwitchField4({
16123
+ secure: useSwitchField3({
15985
16124
  label: { en: "TLS/SSL", es: "TLS/SSL" },
15986
16125
  hint: { en: "Default: SMTP_SECURE env var", es: "Por defecto: variable SMTP_SECURE" }
15987
16126
  }),
@@ -16184,7 +16323,7 @@ var init_mail_entity = __esm({
16184
16323
  meta: { searchable: true, sortable: true }
16185
16324
  }),
16186
16325
  status: {
16187
- ...useSelectField13({
16326
+ ...useSelectField11({
16188
16327
  label: { en: "Status", es: "Estado" },
16189
16328
  options: [
16190
16329
  { value: "pending", label: { en: "Pending", es: "Pendiente" } },
@@ -16208,7 +16347,7 @@ var init_mail_entity = __esm({
16208
16347
  label: { en: "Error", es: "Error" },
16209
16348
  nullable: true
16210
16349
  }),
16211
- sent_by: useSelectField13({
16350
+ sent_by: useSelectField11({
16212
16351
  label: { en: "Sent by", es: "Enviado por" },
16213
16352
  table: "users",
16214
16353
  column: "id",
@@ -16814,7 +16953,7 @@ var init_toggle_plugin_action = __esm({
16814
16953
  });
16815
16954
 
16816
16955
  // src/modules/plugins/plugins.entity.ts
16817
- import { useTextField as useTextField13, useSelectField as useSelectField14, useCheckboxField as useCheckboxField5 } from "@gzl10/nexus-sdk/fields";
16956
+ import { useTextField as useTextField13, useSelectField as useSelectField12, useCheckboxField as useCheckboxField5 } from "@gzl10/nexus-sdk/fields";
16818
16957
  import { OFFICIAL_PLUGINS } from "@gzl10/nexus-sdk";
16819
16958
  var allowPluginManagement, pluginsEntity;
16820
16959
  var init_plugins_entity = __esm({
@@ -16856,7 +16995,7 @@ var init_plugins_entity = __esm({
16856
16995
  size: 20,
16857
16996
  nullable: false
16858
16997
  }),
16859
- category: useSelectField14({
16998
+ category: useSelectField12({
16860
16999
  label: { en: "Category", es: "Categor\xEDa" },
16861
17000
  options: [
16862
17001
  { value: "content", label: { en: "Content", es: "Contenido" } },
@@ -16935,7 +17074,7 @@ var init_plugins_entity = __esm({
16935
17074
 
16936
17075
  // src/modules/plugins/plugins.routes.ts
16937
17076
  import { Router } from "express";
16938
- import { existsSync as existsSync8 } from "fs";
17077
+ import { existsSync as existsSync9 } from "fs";
16939
17078
  function createPluginRoutes(ctx) {
16940
17079
  const router = Router();
16941
17080
  let imageMap = null;
@@ -16943,7 +17082,7 @@ function createPluginRoutes(ctx) {
16943
17082
  if (!imageMap) {
16944
17083
  const discovered = await ctx.core.plugins.discover();
16945
17084
  imageMap = new Map(
16946
- discovered.filter((p) => p.image && existsSync8(p.image)).map((p) => [p.code, p.image])
17085
+ discovered.filter((p) => p.image && existsSync9(p.image)).map((p) => [p.code, p.image])
16947
17086
  );
16948
17087
  }
16949
17088
  return imageMap;
@@ -16998,7 +17137,7 @@ var init_plugins = __esm({
16998
17137
  import {
16999
17138
  useIdField as useIdField8,
17000
17139
  useTextField as useTextField14,
17001
- useSelectField as useSelectField15,
17140
+ useSelectField as useSelectField13,
17002
17141
  useTextareaField as useTextareaField4,
17003
17142
  useJsonField as useJsonField3,
17004
17143
  useDatetimeField as useDatetimeField7,
@@ -17040,7 +17179,7 @@ var init_audit_entity = __esm({
17040
17179
  }),
17041
17180
  validation: { min: 1, max: 100 }
17042
17181
  },
17043
- actor_id: useSelectField15({
17182
+ actor_id: useSelectField13({
17044
17183
  label: { en: "Actor", es: "Actor" },
17045
17184
  table: "users",
17046
17185
  column: "id",
@@ -17327,7 +17466,11 @@ import { Server as SocketServer } from "socket.io";
17327
17466
  import jwt3 from "jsonwebtoken";
17328
17467
  import { DEFAULT_TENANT_ID, entityRoom } from "@gzl10/nexus-sdk";
17329
17468
  function initSocketIO(httpServer, options) {
17330
- jwtSecret = options?.jwtSecret ?? process.env["AUTH_SECRET"] ?? null;
17469
+ try {
17470
+ jwtSecret = options?.jwtSecret ?? getAuthSecret();
17471
+ } catch {
17472
+ jwtSecret = null;
17473
+ }
17331
17474
  if (!jwtSecret) {
17332
17475
  logger.warn("Socket.IO authentication disabled: AUTH_SECRET not configured. All connections will be treated as guests.");
17333
17476
  }
@@ -17572,6 +17715,7 @@ var init_socket = __esm({
17572
17715
  "use strict";
17573
17716
  init_emitter();
17574
17717
  init_logger();
17718
+ init_secret_resolver();
17575
17719
  io = null;
17576
17720
  jwtSecret = null;
17577
17721
  userSockets = /* @__PURE__ */ new Map();
@@ -18093,70 +18237,6 @@ var init_validate_middleware = __esm({
18093
18237
  }
18094
18238
  });
18095
18239
 
18096
- // src/config/env.ts
18097
- import { z as z8 } from "zod";
18098
- function resolveConfig() {
18099
- env = envSchema.parse(process.env);
18100
- process.env["TZ"] = env.TZ;
18101
- resolvedConfig = {
18102
- nodeEnv: env.NODE_ENV,
18103
- port: env.PORT,
18104
- host: "0.0.0.0",
18105
- ui: {
18106
- enabled: env.NEXUS_UI_ENABLED,
18107
- base: env.NEXUS_UI_BASE,
18108
- path: env.NEXUS_UI_PATH
18109
- },
18110
- corsOrigin: env.CORS_ORIGIN,
18111
- databaseUrl: env.DATABASE_URL,
18112
- adminEmail: env.ADMIN_EMAIL,
18113
- adminPassword: env.ADMIN_PASSWORD,
18114
- timezone: env.TZ
18115
- };
18116
- return resolvedConfig;
18117
- }
18118
- function getConfig() {
18119
- if (!resolvedConfig) {
18120
- return resolveConfig();
18121
- }
18122
- return resolvedConfig;
18123
- }
18124
- function resetConfig() {
18125
- resolvedConfig = null;
18126
- }
18127
- var envSchema, env, resolvedConfig;
18128
- var init_env = __esm({
18129
- "src/config/env.ts"() {
18130
- "use strict";
18131
- envSchema = z8.object({
18132
- NODE_ENV: z8.enum(["development", "production", "test"]).default("development"),
18133
- PORT: z8.coerce.number().default(3e3),
18134
- CORS_ORIGIN: z8.string().default("*"),
18135
- BACKEND_URL: z8.string().optional(),
18136
- DATABASE_URL: z8.string().default("file:./dev.db"),
18137
- REDIS_URL: z8.string().url().optional(),
18138
- REDIS_PREFIX: z8.string().default("nexus"),
18139
- ADMIN_EMAIL: z8.string().email().optional(),
18140
- ADMIN_PASSWORD: z8.string().min(6).optional(),
18141
- COOKIE_DOMAIN: z8.string().optional(),
18142
- TZ: z8.string().default("UTC"),
18143
- TRUST_PROXY: z8.coerce.boolean().default(false),
18144
- NEXUS_UI_ENABLED: z8.coerce.boolean().default(true),
18145
- NEXUS_UI_BASE: z8.string().default("/"),
18146
- NEXUS_UI_PATH: z8.string().default("../ui/dist"),
18147
- NEXUS_CHECK_DRIFT: z8.coerce.boolean().optional(),
18148
- NEXUS_FAIL_ON_DRIFT: z8.coerce.boolean().optional(),
18149
- FRPC_SERVER: z8.string().optional(),
18150
- FRPC_SERVER_PORT: z8.coerce.number().default(7e3),
18151
- FRPC_TOKEN: z8.string().optional(),
18152
- FRPC_SUBDOMAIN: z8.string().optional()
18153
- });
18154
- env = envSchema.parse(process.env);
18155
- process.env["TZ"] = env.TZ;
18156
- resolvedConfig = null;
18157
- }
18158
- });
18159
-
18160
18240
  // src/core/middleware/error.middleware.ts
18161
18241
  import { ZodError } from "zod";
18162
18242
  import { ForbiddenError as CASLForbiddenError2 } from "@casl/ability";
@@ -18334,20 +18414,17 @@ var init_hash = __esm({
18334
18414
  });
18335
18415
 
18336
18416
  // src/core/crypto/symmetric.ts
18337
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, hkdfSync } from "crypto";
18417
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, hkdfSync } from "crypto";
18338
18418
  function getKey() {
18339
18419
  if (derivedKey) return derivedKey;
18340
- const secret = process.env["AUTH_SECRET"];
18341
- if (!secret) {
18342
- throw new Error("AUTH_SECRET not configured. Required for symmetric encryption.");
18343
- }
18420
+ const secret = getAuthSecret();
18344
18421
  const keyBytes = hkdfSync("sha256", secret, "", HKDF_INFO, KEY_LENGTH);
18345
18422
  derivedKey = Buffer.from(keyBytes);
18346
18423
  return derivedKey;
18347
18424
  }
18348
18425
  function encrypt(plaintext) {
18349
18426
  const key = getKey();
18350
- const iv = randomBytes2(IV_LENGTH);
18427
+ const iv = randomBytes3(IV_LENGTH);
18351
18428
  const cipher = createCipheriv(ALGORITHM, key, iv);
18352
18429
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
18353
18430
  const authTag = cipher.getAuthTag();
@@ -18371,6 +18448,7 @@ var ALGORITHM, IV_LENGTH, KEY_LENGTH, HKDF_INFO, derivedKey;
18371
18448
  var init_symmetric = __esm({
18372
18449
  "src/core/crypto/symmetric.ts"() {
18373
18450
  "use strict";
18451
+ init_secret_resolver();
18374
18452
  ALGORITHM = "aes-256-gcm";
18375
18453
  IV_LENGTH = 12;
18376
18454
  KEY_LENGTH = 32;
@@ -18596,8 +18674,8 @@ var init_safe_json = __esm({
18596
18674
 
18597
18675
  // src/core/spa-handler.ts
18598
18676
  import express from "express";
18599
- import { resolve, join as join9 } from "path";
18600
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
18677
+ import { resolve, join as join11 } from "path";
18678
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
18601
18679
  function createServeSPA(app, httpServer) {
18602
18680
  return async (endpoint, distPath, options = {}) => {
18603
18681
  const {
@@ -18619,7 +18697,7 @@ function createServeSPA(app, httpServer) {
18619
18697
  registeredEndpoints.add(endpoint);
18620
18698
  if (env.NODE_ENV === "development" && viteSrc) {
18621
18699
  const srcPath = resolve(getProjectPath(), viteSrc);
18622
- if (!existsSync9(srcPath)) {
18700
+ if (!existsSync10(srcPath)) {
18623
18701
  logger.warn({ endpoint, viteSrc, resolved: srcPath }, "Vite source not found \u2014 falling back to static");
18624
18702
  } else {
18625
18703
  const mounted = await mountViteDevMiddleware(app, endpoint, srcPath, httpServer);
@@ -18633,6 +18711,7 @@ async function mountViteDevMiddleware(app, endpoint, srcPath, httpServer) {
18633
18711
  try {
18634
18712
  const vite = await import("vite");
18635
18713
  const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18714
+ const singletonDeps = ["vue", "vue-router", "pinia", "naive-ui"];
18636
18715
  const server2 = await vite.createServer({
18637
18716
  root: srcPath,
18638
18717
  server: {
@@ -18640,6 +18719,12 @@ async function mountViteDevMiddleware(app, endpoint, srcPath, httpServer) {
18640
18719
  allowedHosts: true,
18641
18720
  hmr: httpServer ? { server: httpServer } : true
18642
18721
  },
18722
+ resolve: {
18723
+ dedupe: singletonDeps
18724
+ },
18725
+ optimizeDeps: {
18726
+ include: singletonDeps
18727
+ },
18643
18728
  plugins: [{
18644
18729
  name: "nexus-config-inject",
18645
18730
  transformIndexHtml(html) {
@@ -18675,24 +18760,24 @@ function mountStaticSPA(app, endpoint, distPath, options) {
18675
18760
  resolvedPath = distPath;
18676
18761
  } else {
18677
18762
  const projectPath2 = resolve(getProjectPath(), distPath);
18678
- if (existsSync9(projectPath2)) {
18763
+ if (existsSync10(projectPath2)) {
18679
18764
  resolvedPath = projectPath2;
18680
18765
  } else {
18681
18766
  resolvedPath = resolve(getLibPath(), distPath);
18682
18767
  }
18683
18768
  }
18684
- if (!existsSync9(resolvedPath)) {
18769
+ if (!existsSync10(resolvedPath)) {
18685
18770
  logger.warn({ endpoint, distPath, hint: "Build the frontend first" }, `SPA directory not found: ${resolvedPath}`);
18686
18771
  return;
18687
18772
  }
18688
- const indexPath = join9(resolvedPath, index);
18689
- if (!existsSync9(indexPath)) {
18773
+ const indexPath = join11(resolvedPath, index);
18774
+ if (!existsSync10(indexPath)) {
18690
18775
  logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`);
18691
18776
  }
18692
18777
  app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }));
18693
18778
  let injectedHtml = "";
18694
- if (existsSync9(indexPath)) {
18695
- const rawHtml = readFileSync5(indexPath, "utf-8");
18779
+ if (existsSync10(indexPath)) {
18780
+ const rawHtml = readFileSync6(indexPath, "utf-8");
18696
18781
  const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : "/api/v1";
18697
18782
  const nexusConfig = JSON.stringify({ apiUrl });
18698
18783
  injectedHtml = rawHtml.replace(
@@ -19267,11 +19352,7 @@ var init_cache = __esm({
19267
19352
  // src/core/jwt/index.ts
19268
19353
  import jwt4 from "jsonwebtoken";
19269
19354
  function getSecret() {
19270
- const secret = process.env["AUTH_SECRET"];
19271
- if (!secret) {
19272
- throw new Error("AUTH_SECRET not configured. Set AUTH_SECRET env var.");
19273
- }
19274
- return secret;
19355
+ return getAuthSecret();
19275
19356
  }
19276
19357
  function verifyAccessToken(token) {
19277
19358
  return jwt4.verify(token, getSecret());
@@ -19279,6 +19360,7 @@ function verifyAccessToken(token) {
19279
19360
  var init_jwt = __esm({
19280
19361
  "src/core/jwt/index.ts"() {
19281
19362
  "use strict";
19363
+ init_secret_resolver();
19282
19364
  }
19283
19365
  });
19284
19366
 
@@ -19388,91 +19470,6 @@ var init_schema_helpers = __esm({
19388
19470
  }
19389
19471
  });
19390
19472
 
19391
- // src/db/sql-utils.ts
19392
- function extractTableFromSql(sql, type2) {
19393
- const match = sql.match(SQL_PATTERNS[type2]);
19394
- return match?.[1];
19395
- }
19396
- function extractTableFromSelect(sql) {
19397
- return extractTableFromSql(sql, "select");
19398
- }
19399
- function extractTableFromInsert(sql) {
19400
- return extractTableFromSql(sql, "insert");
19401
- }
19402
- function extractTableFromUpdate(sql) {
19403
- return extractTableFromSql(sql, "update");
19404
- }
19405
- function extractTableFromDelete(sql) {
19406
- return extractTableFromSql(sql, "delete");
19407
- }
19408
- var SQL_PATTERNS;
19409
- var init_sql_utils = __esm({
19410
- "src/db/sql-utils.ts"() {
19411
- "use strict";
19412
- SQL_PATTERNS = {
19413
- select: /from\s+["'`]?(\w+)["'`]?/i,
19414
- insert: /insert into\s+["'`]?(\w+)["'`]?/i,
19415
- update: /update\s+["'`]?(\w+)["'`]?/i,
19416
- delete: /delete from\s+["'`]?(\w+)["'`]?/i
19417
- };
19418
- }
19419
- });
19420
-
19421
- // src/db/sqlite-compat.ts
19422
- function createSqliteBooleanProcessor() {
19423
- const booleanColumns = /* @__PURE__ */ new Map();
19424
- function registerBooleanColumn2(table, column) {
19425
- if (!booleanColumns.has(table)) {
19426
- booleanColumns.set(table, /* @__PURE__ */ new Set());
19427
- }
19428
- booleanColumns.get(table).add(column);
19429
- }
19430
- function convertBooleans2(table, row) {
19431
- const columns = booleanColumns.get(table);
19432
- if (!columns || columns.size === 0) return row;
19433
- const result = { ...row };
19434
- for (const column of columns) {
19435
- if (column in result) {
19436
- const value = result[column];
19437
- if (value === 0 || value === 1) {
19438
- result[column] = value === 1;
19439
- }
19440
- }
19441
- }
19442
- return result;
19443
- }
19444
- function postProcess(result, queryContext) {
19445
- const sql = queryContext?.sql?.toLowerCase() ?? "";
19446
- if (!sql.startsWith("select")) return result;
19447
- const table = extractTableFromSelect(sql);
19448
- if (!table) return result;
19449
- if (Array.isArray(result)) {
19450
- return result.map(
19451
- (row) => typeof row === "object" && row !== null ? convertBooleans2(table, row) : row
19452
- );
19453
- }
19454
- if (typeof result === "object" && result !== null) {
19455
- return convertBooleans2(table, result);
19456
- }
19457
- return result;
19458
- }
19459
- function clear() {
19460
- booleanColumns.clear();
19461
- }
19462
- return { registerBooleanColumn: registerBooleanColumn2, convertBooleans: convertBooleans2, postProcess, clear };
19463
- }
19464
- var defaultProcessor, registerBooleanColumn, convertBooleans, sqlitePostProcess;
19465
- var init_sqlite_compat = __esm({
19466
- "src/db/sqlite-compat.ts"() {
19467
- "use strict";
19468
- init_sql_utils();
19469
- defaultProcessor = createSqliteBooleanProcessor();
19470
- registerBooleanColumn = defaultProcessor.registerBooleanColumn;
19471
- convertBooleans = defaultProcessor.convertBooleans;
19472
- sqlitePostProcess = defaultProcessor.postProcess;
19473
- }
19474
- });
19475
-
19476
19473
  // src/runtime/types.ts
19477
19474
  var init_types = __esm({
19478
19475
  "src/runtime/types.ts"() {
@@ -23743,8 +23740,8 @@ var init_runtime = __esm({
23743
23740
  });
23744
23741
 
23745
23742
  // src/db/seed-runner.ts
23746
- import { existsSync as existsSync10 } from "fs";
23747
- import { join as join10 } from "path";
23743
+ import { existsSync as existsSync11 } from "fs";
23744
+ import { join as join12 } from "path";
23748
23745
  import { pathToFileURL } from "url";
23749
23746
  async function runModuleSeed(mod, ctx) {
23750
23747
  let seeded = false;
@@ -23752,8 +23749,8 @@ async function runModuleSeed(mod, ctx) {
23752
23749
  await mod.seed(ctx);
23753
23750
  seeded = true;
23754
23751
  } else {
23755
- const seedPath = join10(getLibPath(), "dist", "modules", mod.name, `${mod.name}.seed.js`);
23756
- if (existsSync10(seedPath)) {
23752
+ const seedPath = join12(getLibPath(), "dist", "modules", mod.name, `${mod.name}.seed.js`);
23753
+ if (existsSync11(seedPath)) {
23757
23754
  const seedModule = await import(pathToFileURL(seedPath).href);
23758
23755
  if (typeof seedModule.seed === "function") {
23759
23756
  await seedModule.seed(ctx);
@@ -23907,94 +23904,6 @@ var init_migration_sources = __esm({
23907
23904
  }
23908
23905
  });
23909
23906
 
23910
- // src/config/database.ts
23911
- import { join as join11, dirname as dirname6, isAbsolute as isAbsolute2 } from "path";
23912
- import { mkdirSync as mkdirSync4 } from "fs";
23913
- function getDatabaseConfig() {
23914
- const url = getConfig().databaseUrl;
23915
- if (IN_MEMORY_RE.test(url)) {
23916
- return {
23917
- client: "better-sqlite3",
23918
- connection: { filename: ":memory:" },
23919
- useNullAsDefault: true,
23920
- postProcessResponse: sqlitePostProcess,
23921
- pool: { min: 0, max: 1 }
23922
- };
23923
- }
23924
- if (url.startsWith("file:") || url.startsWith("sqlite:")) {
23925
- let filename = url.replace(/^(file:|sqlite:)/, "");
23926
- if (!isAbsolute2(filename)) {
23927
- filename = join11(getProjectPath(), "data", filename);
23928
- }
23929
- mkdirSync4(dirname6(filename), { recursive: true });
23930
- return {
23931
- client: "better-sqlite3",
23932
- connection: { filename },
23933
- useNullAsDefault: true,
23934
- postProcessResponse: sqlitePostProcess
23935
- };
23936
- }
23937
- if (url.startsWith("postgresql://") || url.startsWith("postgres://")) {
23938
- return {
23939
- client: "pg",
23940
- connection: url,
23941
- pool: { min: 2, max: 10 }
23942
- };
23943
- }
23944
- if (url.startsWith("mysql://")) {
23945
- const offsetMinutes = (/* @__PURE__ */ new Date()).getTimezoneOffset();
23946
- const offsetHours = Math.abs(Math.floor(offsetMinutes / 60));
23947
- const offsetMins = Math.abs(offsetMinutes % 60);
23948
- const sign = offsetMinutes <= 0 ? "+" : "-";
23949
- const tzOffset = `${sign}${String(offsetHours).padStart(2, "0")}:${String(offsetMins).padStart(2, "0")}`;
23950
- return {
23951
- client: "mysql2",
23952
- connection: {
23953
- uri: url,
23954
- timezone: tzOffset
23955
- // mysql2 requiere formato "+HH:MM"
23956
- },
23957
- pool: { min: 2, max: 10 }
23958
- };
23959
- }
23960
- throw new Error(`Unsupported database URL: ${url}`);
23961
- }
23962
- function getDatabaseType() {
23963
- const url = getConfig().databaseUrl;
23964
- if (IN_MEMORY_RE.test(url) || url.startsWith("file:") || url.startsWith("sqlite:")) return "sqlite";
23965
- if (url.startsWith("postgresql://") || url.startsWith("postgres://")) return "postgresql";
23966
- if (url.startsWith("mysql://")) return "mysql";
23967
- return "sqlite";
23968
- }
23969
- function getDatabasePath() {
23970
- const url = getConfig().databaseUrl;
23971
- if (IN_MEMORY_RE.test(url)) return ":memory:";
23972
- if (url.startsWith("file:") || url.startsWith("sqlite:")) {
23973
- let filename = url.replace(/^(file:|sqlite:)/, "");
23974
- if (!isAbsolute2(filename)) {
23975
- filename = join11(getProjectPath(), "data", filename);
23976
- }
23977
- return filename;
23978
- }
23979
- try {
23980
- const parsed = new URL(url);
23981
- parsed.password = "***";
23982
- return parsed.toString();
23983
- } catch {
23984
- return url.replace(/:\/\/[^@]+@/, "://***@");
23985
- }
23986
- }
23987
- var IN_MEMORY_RE;
23988
- var init_database = __esm({
23989
- "src/config/database.ts"() {
23990
- "use strict";
23991
- init_env();
23992
- init_paths();
23993
- init_sqlite_compat();
23994
- IN_MEMORY_RE = /^((file:|sqlite:)?:memory:?)$/;
23995
- }
23996
- });
23997
-
23998
23907
  // src/db/query-interceptor.ts
23999
23908
  function setupQueryInterceptor(knexInstance) {
24000
23909
  const queryTimings = /* @__PURE__ */ new Map();
@@ -24635,7 +24544,7 @@ var init_migration_helpers = __esm({
24635
24544
  // src/db/migration-generator.ts
24636
24545
  import path2 from "path";
24637
24546
  import fs2 from "fs/promises";
24638
- import { readFileSync as readFileSync6, mkdirSync as mkdirSync5, realpathSync } from "fs";
24547
+ import { readFileSync as readFileSync7, mkdirSync as mkdirSync5, realpathSync } from "fs";
24639
24548
  function getColumnIndexBytes(field) {
24640
24549
  if (!field?.db) return 255 * MYSQL_BYTES_PER_CHAR;
24641
24550
  const size = field.db.size ?? 255;
@@ -26549,8 +26458,8 @@ var init_db = __esm({
26549
26458
  });
26550
26459
 
26551
26460
  // src/config/load-config.ts
26552
- import { join as join12 } from "path";
26553
- import { existsSync as existsSync11, readFileSync as readFileSync7, readdirSync, statSync } from "fs";
26461
+ import { join as join13 } from "path";
26462
+ import { existsSync as existsSync12, readFileSync as readFileSync8, readdirSync, statSync } from "fs";
26554
26463
  import { pathToFileURL as pathToFileURL2 } from "url";
26555
26464
  function isPluginManifest(value) {
26556
26465
  if (!value || typeof value !== "object" || typeof value === "function") return false;
@@ -26566,25 +26475,25 @@ function extractPluginManifest(mod) {
26566
26475
  return null;
26567
26476
  }
26568
26477
  function resolvePluginEntry(projectPath2, pkgName) {
26569
- const pkgDir = join12(projectPath2, "node_modules", pkgName);
26570
- const pkgJsonPath = join12(pkgDir, "package.json");
26571
- if (!existsSync11(pkgJsonPath)) return null;
26478
+ const pkgDir = join13(projectPath2, "node_modules", pkgName);
26479
+ const pkgJsonPath = join13(pkgDir, "package.json");
26480
+ if (!existsSync12(pkgJsonPath)) return null;
26572
26481
  try {
26573
- const pluginPkg = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
26482
+ const pluginPkg = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
26574
26483
  const exports = pluginPkg.exports;
26575
26484
  const importEntry = exports?.["."]?.import;
26576
26485
  const entry = (typeof importEntry === "string" ? importEntry : importEntry?.default) ?? (typeof exports === "string" ? exports : null) ?? pluginPkg.main ?? pluginPkg.module ?? "index.js";
26577
- const entryPath = join12(pkgDir, entry);
26578
- return existsSync11(entryPath) ? entryPath : null;
26486
+ const entryPath = join13(pkgDir, entry);
26487
+ return existsSync12(entryPath) ? entryPath : null;
26579
26488
  } catch {
26580
26489
  return null;
26581
26490
  }
26582
26491
  }
26583
26492
  async function discoverPlugins(projectPath2) {
26584
- const pkgPath = join12(projectPath2, "package.json");
26493
+ const pkgPath = join13(projectPath2, "package.json");
26585
26494
  let pkg2;
26586
26495
  try {
26587
- pkg2 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
26496
+ pkg2 = JSON.parse(readFileSync8(pkgPath, "utf-8"));
26588
26497
  } catch {
26589
26498
  return [];
26590
26499
  }
@@ -26606,21 +26515,21 @@ async function discoverPlugins(projectPath2) {
26606
26515
  const manifest = extractPluginManifest(mod);
26607
26516
  if (manifest) {
26608
26517
  if (!manifest.migrationsDir) {
26609
- const defaultMigrationsDir = join12(projectPath2, "node_modules", pkgName, "migrations");
26610
- if (existsSync11(defaultMigrationsDir)) {
26518
+ const defaultMigrationsDir = join13(projectPath2, "node_modules", pkgName, "migrations");
26519
+ if (existsSync12(defaultMigrationsDir)) {
26611
26520
  manifest.migrationsDir = defaultMigrationsDir;
26612
26521
  }
26613
26522
  }
26614
26523
  if (!manifest.image) {
26615
- const imagePath = join12(projectPath2, "node_modules", pkgName, "image.png");
26616
- if (existsSync11(imagePath)) {
26524
+ const imagePath = join13(projectPath2, "node_modules", pkgName, "image.png");
26525
+ if (existsSync12(imagePath)) {
26617
26526
  manifest.image = imagePath;
26618
26527
  }
26619
26528
  }
26620
26529
  if (!manifest.llms) {
26621
- const llmsPath = join12(projectPath2, "node_modules", pkgName, "llms.txt");
26622
- if (existsSync11(llmsPath)) {
26623
- manifest.llms = readFileSync7(llmsPath, "utf-8");
26530
+ const llmsPath = join13(projectPath2, "node_modules", pkgName, "llms.txt");
26531
+ if (existsSync12(llmsPath)) {
26532
+ manifest.llms = readFileSync8(llmsPath, "utf-8");
26624
26533
  }
26625
26534
  }
26626
26535
  discovered.push(manifest);
@@ -26656,8 +26565,8 @@ function extractModuleManifests(mod) {
26656
26565
  return manifests;
26657
26566
  }
26658
26567
  async function discoverModules(projectPath2) {
26659
- const modulesDir = process.env["MODULES_DIR"] || join12(projectPath2, "src", "modules");
26660
- if (!existsSync11(modulesDir)) return [];
26568
+ const modulesDir = process.env["MODULES_DIR"] || join13(projectPath2, "src", "modules");
26569
+ if (!existsSync12(modulesDir)) return [];
26661
26570
  let entries;
26662
26571
  try {
26663
26572
  entries = readdirSync(modulesDir);
@@ -26666,7 +26575,7 @@ async function discoverModules(projectPath2) {
26666
26575
  }
26667
26576
  const discovered = [];
26668
26577
  for (const entry of entries) {
26669
- const entryPath = join12(modulesDir, entry);
26578
+ const entryPath = join13(modulesDir, entry);
26670
26579
  try {
26671
26580
  if (!statSync(entryPath).isDirectory()) continue;
26672
26581
  } catch {
@@ -26674,8 +26583,8 @@ async function discoverModules(projectPath2) {
26674
26583
  }
26675
26584
  let indexPath = null;
26676
26585
  for (const ext of ["index.js", "index.ts"]) {
26677
- const candidate = join12(entryPath, ext);
26678
- if (existsSync11(candidate)) {
26586
+ const candidate = join13(entryPath, ext);
26587
+ if (existsSync12(candidate)) {
26679
26588
  indexPath = candidate;
26680
26589
  break;
26681
26590
  }
@@ -26789,6 +26698,49 @@ var init_events_api = __esm({
26789
26698
  }
26790
26699
  });
26791
26700
 
26701
+ // src/engine/capabilities-registry.ts
26702
+ var CapabilitiesRegistry;
26703
+ var init_capabilities_registry = __esm({
26704
+ "src/engine/capabilities-registry.ts"() {
26705
+ "use strict";
26706
+ CapabilitiesRegistry = class {
26707
+ resolvers = /* @__PURE__ */ new Map();
26708
+ logger;
26709
+ constructor(logger2) {
26710
+ this.logger = logger2;
26711
+ }
26712
+ register(key, resolver) {
26713
+ if (this.resolvers.has(key)) {
26714
+ this.logger?.warn({ key }, "Capability key already registered, overwriting");
26715
+ }
26716
+ this.resolvers.set(key, resolver);
26717
+ }
26718
+ async resolve() {
26719
+ const entries = Array.from(this.resolvers.entries());
26720
+ const results = await Promise.allSettled(
26721
+ entries.map(async ([key, resolver]) => {
26722
+ const value = await resolver();
26723
+ return [key, value];
26724
+ })
26725
+ );
26726
+ const capabilities = {};
26727
+ for (const result of results) {
26728
+ if (result.status === "fulfilled") {
26729
+ const [key, value] = result.value;
26730
+ capabilities[key] = value;
26731
+ } else {
26732
+ this.logger?.warn(
26733
+ { err: result.reason?.message ?? String(result.reason) },
26734
+ "Capability resolver failed"
26735
+ );
26736
+ }
26737
+ }
26738
+ return capabilities;
26739
+ }
26740
+ };
26741
+ }
26742
+ });
26743
+
26792
26744
  // src/engine/context.ts
26793
26745
  import { ForbiddenError as CASLForbiddenError3, subject } from "@casl/ability";
26794
26746
  import { DEFAULT_TENANT_ID as DEFAULT_TENANT_ID2, DEFAULT_LOCALES } from "@gzl10/nexus-sdk";
@@ -27062,6 +27014,9 @@ function createModuleContext() {
27062
27014
  return name in adaptersRegistry;
27063
27015
  }
27064
27016
  };
27017
+ const capabilitiesRegistry = new CapabilitiesRegistry(
27018
+ logger.child({ service: "capabilities" })
27019
+ );
27065
27020
  const ctx = {
27066
27021
  tenantId: DEFAULT_TENANT_ID2,
27067
27022
  core: coreContext,
@@ -27072,6 +27027,7 @@ function createModuleContext() {
27072
27027
  engine: engineContext,
27073
27028
  services: servicesContext,
27074
27029
  adapters: adaptersContext,
27030
+ capabilities: capabilitiesRegistry,
27075
27031
  // Root-level shortcuts for frequently used utilities
27076
27032
  events: createEventsApi(nexusEvents, logger),
27077
27033
  createRouter: () => createRouter(),
@@ -27085,6 +27041,9 @@ function createModuleContext() {
27085
27041
  createEntityRouter: (controller, def) => createEntityRouter(controller, def, ctx)
27086
27042
  };
27087
27043
  ctx.db.seedModule = (mod) => runModuleSeed(mod, ctx);
27044
+ capabilitiesRegistry.register("version", () => getCoreManifest().version);
27045
+ capabilitiesRegistry.register("plugins", () => getPlugins().map((p) => p.code));
27046
+ capabilitiesRegistry.register("locales", () => platformLocales);
27088
27047
  return ctx;
27089
27048
  }
27090
27049
  var platformLocales, sharedCacheManager, sharedTempAdapter;
@@ -27100,6 +27059,7 @@ var init_context = __esm({
27100
27059
  init_plugin_ops();
27101
27060
  init_load_config();
27102
27061
  init_events_api();
27062
+ init_capabilities_registry();
27103
27063
  init_seed_runner();
27104
27064
  init_cache_manager();
27105
27065
  platformLocales = DEFAULT_LOCALES;
@@ -27119,6 +27079,7 @@ var init_engine = __esm({
27119
27079
  init_subject_extractor();
27120
27080
  init_definition_extractors();
27121
27081
  init_context();
27082
+ init_capabilities_registry();
27122
27083
  }
27123
27084
  });
27124
27085
 
@@ -27855,7 +27816,7 @@ var init_port_check = __esm({
27855
27816
  // src/core/tunnel.ts
27856
27817
  import { spawn, execSync as execSync2 } from "child_process";
27857
27818
  import { writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
27858
- import { join as join13 } from "path";
27819
+ import { join as join14 } from "path";
27859
27820
  import { tmpdir } from "os";
27860
27821
  function buildFrpcConfig(config3) {
27861
27822
  const lines = [
@@ -27893,7 +27854,7 @@ async function startTunnel(config3) {
27893
27854
  return false;
27894
27855
  }
27895
27856
  const toml = buildFrpcConfig(config3);
27896
- tmpConfigPath = join13(tmpdir(), `nexus-frpc-${process.pid}.toml`);
27857
+ tmpConfigPath = join14(tmpdir(), `nexus-frpc-${process.pid}.toml`);
27897
27858
  writeFileSync2(tmpConfigPath, toml);
27898
27859
  tunnelProcess = spawn("frpc", ["-c", tmpConfigPath], {
27899
27860
  stdio: ["ignore", "pipe", "pipe"]
@@ -27984,22 +27945,22 @@ var init_shared = __esm({
27984
27945
  });
27985
27946
 
27986
27947
  // src/cli/migrate-commands.ts
27987
- import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
27988
- import { join as join14 } from "path";
27948
+ import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
27949
+ import { join as join15 } from "path";
27989
27950
  import { pathToFileURL as pathToFileURL3 } from "url";
27990
27951
  import Table from "cli-table3";
27991
27952
  import { consola as consola2 } from "consola";
27992
27953
  async function loadSelfPlugin() {
27993
27954
  const projectPath2 = getProjectPath();
27994
- const pkgPath = join14(projectPath2, "package.json");
27995
- if (!existsSync12(pkgPath)) return;
27955
+ const pkgPath = join15(projectPath2, "package.json");
27956
+ if (!existsSync13(pkgPath)) return;
27996
27957
  try {
27997
- const pkg2 = JSON.parse(readFileSync8(pkgPath, "utf-8"));
27958
+ const pkg2 = JSON.parse(readFileSync9(pkgPath, "utf-8"));
27998
27959
  const pkgName = pkg2?.name;
27999
27960
  if (!pkgName || !/nexus-plugin-/.test(pkgName)) return;
28000
- const srcEntry = join14(projectPath2, "src", "index.ts");
28001
- const distEntry = join14(projectPath2, "dist", "index.js");
28002
- if (existsSync12(srcEntry)) {
27961
+ const srcEntry = join15(projectPath2, "src", "index.ts");
27962
+ const distEntry = join15(projectPath2, "dist", "index.js");
27963
+ if (existsSync13(srcEntry)) {
28003
27964
  try {
28004
27965
  const { tsImport } = await import("tsx/esm/api");
28005
27966
  const mod = await tsImport(
@@ -28009,7 +27970,7 @@ async function loadSelfPlugin() {
28009
27970
  const manifest = extractPluginManifest(mod);
28010
27971
  if (manifest) {
28011
27972
  if (!manifest.migrationsDir) {
28012
- manifest.migrationsDir = join14(projectPath2, "migrations");
27973
+ manifest.migrationsDir = join15(projectPath2, "migrations");
28013
27974
  }
28014
27975
  registerPlugin(manifest);
28015
27976
  return;
@@ -28018,13 +27979,13 @@ async function loadSelfPlugin() {
28018
27979
  console.error(` \u26A0 Failed to load plugin src/index.ts: ${err.message}`);
28019
27980
  }
28020
27981
  }
28021
- if (existsSync12(distEntry)) {
27982
+ if (existsSync13(distEntry)) {
28022
27983
  try {
28023
27984
  const mod = await import(pathToFileURL3(distEntry).href);
28024
27985
  const manifest = extractPluginManifest(mod);
28025
27986
  if (manifest) {
28026
27987
  if (!manifest.migrationsDir) {
28027
- manifest.migrationsDir = join14(projectPath2, "migrations");
27988
+ manifest.migrationsDir = join15(projectPath2, "migrations");
28028
27989
  }
28029
27990
  registerPlugin(manifest);
28030
27991
  return;
@@ -28074,8 +28035,8 @@ __export(seed_commands_exports, {
28074
28035
  handleSeedExport: () => handleSeedExport,
28075
28036
  importSeedFiles: () => importSeedFiles
28076
28037
  });
28077
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
28078
- import { join as join15, basename as basename4 } from "path";
28038
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
28039
+ import { join as join16, basename as basename4 } from "path";
28079
28040
  import { consola as consola3 } from "consola";
28080
28041
  function deserializeJsonFields(record, fields) {
28081
28042
  const result = { ...record };
@@ -28104,7 +28065,7 @@ async function handleSeedExport(entity) {
28104
28065
  const db2 = getDb();
28105
28066
  try {
28106
28067
  const modules = getOrderedModules();
28107
- const seedDir = join15(getProjectPath(), "data", "seeds");
28068
+ const seedDir = join16(getProjectPath(), "data", "seeds");
28108
28069
  const seedableEntities = [];
28109
28070
  for (const mod of modules) {
28110
28071
  for (const def of mod.definitions ?? []) {
@@ -28125,7 +28086,7 @@ async function handleSeedExport(entity) {
28125
28086
  }
28126
28087
  return;
28127
28088
  }
28128
- if (!existsSync13(seedDir)) {
28089
+ if (!existsSync14(seedDir)) {
28129
28090
  mkdirSync6(seedDir, { recursive: true });
28130
28091
  }
28131
28092
  for (const { module: modName, table, fields } of seedableEntities) {
@@ -28137,7 +28098,7 @@ async function handleSeedExport(entity) {
28137
28098
  const exported = rows.map(
28138
28099
  (row) => deserializeJsonFields(row, fields)
28139
28100
  );
28140
- const filePath = join15(seedDir, `${table}.json`);
28101
+ const filePath = join16(seedDir, `${table}.json`);
28141
28102
  writeFileSync3(filePath, JSON.stringify(exported, null, 2) + "\n", "utf-8");
28142
28103
  consola3.success(`${table}: exported ${rows.length} records to data/seeds/${table}.json (module: ${modName})`);
28143
28104
  }
@@ -28149,8 +28110,8 @@ async function handleSeedExport(entity) {
28149
28110
  }
28150
28111
  }
28151
28112
  async function importSeedFiles(db2, modules, logger2) {
28152
- const seedDir = join15(getProjectPath(), "data", "seeds");
28153
- if (!existsSync13(seedDir)) return;
28113
+ const seedDir = join16(getProjectPath(), "data", "seeds");
28114
+ if (!existsSync14(seedDir)) return;
28154
28115
  const files = readdirSync2(seedDir).filter((f) => f.endsWith(".json"));
28155
28116
  if (files.length === 0) return;
28156
28117
  const seedableDefs = /* @__PURE__ */ new Map();
@@ -28171,8 +28132,8 @@ async function importSeedFiles(db2, modules, logger2) {
28171
28132
  logger2.debug(`data/seeds/${file}: skipped (entity "${table}" is not seedable)`);
28172
28133
  continue;
28173
28134
  }
28174
- const filePath = join15(seedDir, file);
28175
- const raw = readFileSync9(filePath, "utf-8");
28135
+ const filePath = join16(seedDir, file);
28136
+ const raw = readFileSync10(filePath, "utf-8");
28176
28137
  let records;
28177
28138
  try {
28178
28139
  records = JSON.parse(raw);
@@ -28362,6 +28323,12 @@ async function runMigrationsAndSeeds(config3) {
28362
28323
  loadCoreModules();
28363
28324
  logger.info({ type: getDatabaseType(), path: getDatabasePath() }, "Database ready");
28364
28325
  await ensureSystemTables(getDb());
28326
+ setSecretResolverDb(getDb());
28327
+ await initAuthSecret(logger);
28328
+ const isPM2Cluster = !!(process.env["PM2_HOME"] && process.env["NODE_APP_INSTANCE"]);
28329
+ if (isPM2Cluster && getDatabaseType() === "sqlite") {
28330
+ logger.warn("SQLite detected with PM2 cluster mode. SQLite does not support concurrent writes reliably. Use PostgreSQL for multi-process deployments, or set PM2 instances to 1.");
28331
+ }
28365
28332
  const fileConfig = await loadNexusConfig();
28366
28333
  const effectiveConfig = { ...config3 };
28367
28334
  const programmaticPluginNames = new Set((config3?.plugins ?? []).map((p) => p.name));
@@ -28616,6 +28583,7 @@ async function stop() {
28616
28583
  resetConfigCache();
28617
28584
  clearCustomCaslRules();
28618
28585
  clearSeedPermissions();
28586
+ _resetSecretResolver();
28619
28587
  await resetServeSPA();
28620
28588
  return;
28621
28589
  }
@@ -28651,6 +28619,7 @@ async function stop() {
28651
28619
  resetConfigCache();
28652
28620
  clearCustomCaslRules();
28653
28621
  clearSeedPermissions();
28622
+ _resetSecretResolver();
28654
28623
  await resetServeSPA();
28655
28624
  currentConfig = void 0;
28656
28625
  server = null;
@@ -28720,6 +28689,7 @@ var init_server = __esm({
28720
28689
  init_ensure_system_tables();
28721
28690
  init_load_config();
28722
28691
  init_tunnel();
28692
+ init_secret_resolver();
28723
28693
  server = null;
28724
28694
  gracefulShutdownRegistered = false;
28725
28695
  setupGracefulShutdown();