webspresso 0.0.95 → 0.0.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  23. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  24. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  25. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  26. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  27. package/.cursorrules +14 -0
  28. package/AGENTS.md +175 -0
  29. package/CLAUDE.md +54 -0
  30. package/README.md +18 -14
  31. package/bin/commands/admin-password.js +2 -1
  32. package/bin/commands/admin-setup.js +1 -1
  33. package/bin/commands/agents-init.js +209 -0
  34. package/bin/commands/api.js +114 -48
  35. package/bin/commands/audit-prune.js +1 -1
  36. package/bin/commands/doctor.js +35 -0
  37. package/bin/commands/favicon-generate.js +1 -1
  38. package/bin/commands/new.js +13 -2
  39. package/bin/commands/orm-map.js +5 -4
  40. package/bin/commands/page.js +75 -89
  41. package/bin/commands/polar-migrate.js +1 -1
  42. package/bin/commands/seed.js +1 -1
  43. package/bin/commands/service.js +77 -0
  44. package/bin/webspresso.js +2 -2
  45. package/core/auth/hash.js +10 -5
  46. package/core/compileSchema.js +1 -1
  47. package/core/errors/middleware.js +22 -2
  48. package/core/errors/normalize.js +29 -2
  49. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  50. package/index.d.ts +306 -0
  51. package/index.js +35 -1
  52. package/package.json +36 -14
  53. package/plugins/admin-panel/app.js +10 -9
  54. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  55. package/plugins/admin-panel/core/api-extensions.js +4 -0
  56. package/plugins/basic-auth/index.js +3 -1
  57. package/plugins/content/client/inline-edit.js +12 -12
  58. package/plugins/cors.js +6 -7
  59. package/plugins/csrf/index.js +3 -2
  60. package/plugins/csv/engine.js +447 -0
  61. package/plugins/csv/index.js +94 -0
  62. package/plugins/csv/middleware.js +80 -0
  63. package/plugins/csv/services.js +138 -0
  64. package/plugins/file-manager/admin-component.js +613 -0
  65. package/plugins/file-manager/api-handlers.js +261 -0
  66. package/plugins/file-manager/core.js +601 -0
  67. package/plugins/file-manager/index.js +139 -0
  68. package/plugins/file-manager/security.js +217 -0
  69. package/plugins/index.js +45 -79
  70. package/plugins/mcp/transports/sse.js +4 -2
  71. package/plugins/site-analytics/admin-component.js +7 -8
  72. package/plugins/xlsx/engine.js +397 -0
  73. package/plugins/xlsx/index.js +91 -0
  74. package/plugins/xlsx/middleware.js +87 -0
  75. package/plugins/xlsx/services.js +118 -0
  76. package/src/api/api-loader.js +233 -0
  77. package/src/api/define-api.js +35 -0
  78. package/src/discovery/file-route-parser.js +201 -0
  79. package/src/discovery/scan-routes.js +265 -0
  80. package/src/file-router.js +9 -1
  81. package/src/modules/define-module.js +99 -0
  82. package/src/modules/module-discovery.js +117 -0
  83. package/src/pages/define-page.js +36 -0
  84. package/src/pages/page-loader.js +214 -0
  85. package/src/routing/mount-discovered-routes.js +78 -0
  86. package/src/routing/route-table.js +191 -0
  87. package/src/server.js +112 -15
  88. package/src/services/builtins/file-manager.js +178 -0
  89. package/src/services/discovery.js +6 -1
  90. package/src/services/index.js +6 -11
  91. package/bin/commands/skill.js +0 -187
  92. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  93. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  94. package/templates/skills/webspresso-usage/SKILL.md +0 -48
package/index.d.ts CHANGED
@@ -222,6 +222,11 @@ export function createMediaServices(options?: {
222
222
  publicBasePath?: string;
223
223
  }): Record<string, ServiceDefinition>;
224
224
 
225
+ export function createFileManagerServices(options?: {
226
+ baseDir?: string;
227
+ publicBasePath?: string;
228
+ }): Record<string, ServiceDefinition>;
229
+
225
230
  export function createSystemServices(options?: {
226
231
  db?: unknown;
227
232
  serviceRegistry?: unknown;
@@ -706,6 +711,7 @@ export type Zdb = Record<string, any> & {
706
711
  };
707
712
 
708
713
  export const zdb: Zdb;
714
+ export const z: typeof import('zod').z;
709
715
 
710
716
  export function createSchemaHelpers(z: typeof import('zod').z): Zdb;
711
717
 
@@ -1677,4 +1683,304 @@ export class McpServer {
1677
1683
  export function mcpPlugin(options?: McpPluginOptions): WebspressoPlugin;
1678
1684
  export function createMcpServer(options?: McpPluginOptions): McpServer;
1679
1685
 
1686
+ // --- File Manager Plugin ---
1687
+
1688
+ export interface FileManagerPluginOptions {
1689
+ baseDir?: string;
1690
+ publicBasePath?: string;
1691
+ apiPrefix?: string;
1692
+ pageTitle?: string;
1693
+ menuLabel?: string;
1694
+ menuOrder?: number;
1695
+ maxUploadBytes?: number;
1696
+ allowedExtensions?: string[] | null;
1697
+ allowedMimeTypes?: string[] | null;
1698
+ includePage?: boolean;
1699
+ }
1700
+
1701
+ export function fileManagerPlugin(options?: FileManagerPluginOptions): WebspressoPlugin;
1702
+
1703
+ // --- XLSX Plugin & Toolkit ---
1704
+
1705
+ export interface XlsxColumn {
1706
+ header?: string;
1707
+ key: string;
1708
+ width?: number;
1709
+ style?: Record<string, unknown>;
1710
+ }
1711
+
1712
+ export interface XlsxHeaderStyle {
1713
+ fill?: string;
1714
+ bg?: string;
1715
+ color?: string;
1716
+ textColor?: string;
1717
+ fontName?: string;
1718
+ fontSize?: number;
1719
+ bold?: boolean;
1720
+ align?: 'left' | 'center' | 'right';
1721
+ height?: number;
1722
+ }
1723
+
1724
+ export interface XlsxSheetConfig {
1725
+ name?: string;
1726
+ columns?: XlsxColumn[];
1727
+ rows?: Record<string, unknown>[];
1728
+ data?: Record<string, unknown>[];
1729
+ autoWidth?: boolean;
1730
+ striped?: boolean;
1731
+ headerStyle?: XlsxHeaderStyle;
1732
+ views?: Array<Record<string, unknown>>;
1733
+ }
1734
+
1735
+ export interface XlsxGenerateOptions {
1736
+ creator?: string;
1737
+ title?: string;
1738
+ created?: Date;
1739
+ sanitizeFormulas?: boolean;
1740
+ headerStyle?: XlsxHeaderStyle;
1741
+ sheets?: XlsxSheetConfig[] | XlsxSheetConfig;
1742
+ name?: string;
1743
+ columns?: XlsxColumn[];
1744
+ rows?: Record<string, unknown>[];
1745
+ data?: Record<string, unknown>[];
1746
+ }
1747
+
1748
+ export interface XlsxParseOptions {
1749
+ sheet?: string | number;
1750
+ headerRow?: number;
1751
+ dataStartRow?: number;
1752
+ raw?: boolean;
1753
+ transformRow?: (row: Record<string, unknown>, index: number) => Record<string, unknown>;
1754
+ }
1755
+
1756
+ export interface XlsxModelExportOptions {
1757
+ sheetName?: string;
1758
+ name?: string;
1759
+ creator?: string;
1760
+ title?: string;
1761
+ columns?: XlsxColumn[];
1762
+ headerStyle?: XlsxHeaderStyle;
1763
+ }
1764
+
1765
+ export interface XlsxPluginOptions {
1766
+ enabled?: boolean;
1767
+ sanitizeFormulas?: boolean;
1768
+ headerStyle?: XlsxHeaderStyle;
1769
+ db?: unknown;
1770
+ }
1771
+
1772
+ export interface XlsxToolkit {
1773
+ MIME_TYPE: string;
1774
+ generate(options: XlsxGenerateOptions): Promise<Buffer>;
1775
+ parse(input: Buffer | import('stream').Readable, options?: XlsxParseOptions): Promise<Record<string, unknown>[] | Record<string, Record<string, unknown>[]>>;
1776
+ fromModel(repository: unknown, filterOrQuery?: unknown, options?: XlsxModelExportOptions): Promise<Buffer>;
1777
+ createStreamWriter(outputStream: import('stream').Writable, options?: Record<string, unknown>): unknown;
1778
+ sanitizeFormula(value: unknown): unknown;
1779
+ applyHeaderStyle(worksheet: unknown, style?: XlsxHeaderStyle): void;
1780
+ applyAutoWidths(worksheet: unknown, columns: XlsxColumn[], rows: Record<string, unknown>[]): void;
1781
+ hexToArgb(hex: string): string;
1782
+ createMiddleware(globalOptions?: XlsxPluginOptions): import('express').RequestHandler;
1783
+ createServices(options?: Record<string, unknown>): Record<string, unknown>;
1784
+ }
1785
+
1786
+ export const xlsx: XlsxToolkit;
1787
+ export function xlsxPlugin(options?: XlsxPluginOptions): WebspressoPlugin;
1788
+
1789
+ // --- CSV Plugin & Toolkit ---
1790
+
1791
+ export interface CsvColumn {
1792
+ header?: string;
1793
+ key: string;
1794
+ }
1795
+
1796
+ export interface CsvGenerateOptions {
1797
+ rows?: Record<string, unknown>[];
1798
+ data?: Record<string, unknown>[];
1799
+ columns?: (CsvColumn | string)[];
1800
+ delimiter?: string;
1801
+ eol?: string;
1802
+ header?: boolean;
1803
+ bom?: boolean;
1804
+ sanitizeFormulas?: boolean;
1805
+ asBuffer?: boolean;
1806
+ }
1807
+
1808
+ export interface CsvParseOptions {
1809
+ delimiter?: string;
1810
+ columns?: boolean | string[];
1811
+ skip_empty_lines?: boolean;
1812
+ trim?: boolean;
1813
+ relax_column_count?: boolean;
1814
+ transformRow?: (row: Record<string, unknown>, index: number) => Record<string, unknown>;
1815
+ [key: string]: unknown;
1816
+ }
1817
+
1818
+ export interface CsvModelExportOptions {
1819
+ columns?: (CsvColumn | string)[];
1820
+ delimiter?: string;
1821
+ eol?: string;
1822
+ bom?: boolean;
1823
+ sanitizeFormulas?: boolean;
1824
+ asBuffer?: boolean;
1825
+ }
1826
+
1827
+ export interface CsvPluginOptions {
1828
+ enabled?: boolean;
1829
+ delimiter?: string;
1830
+ bom?: boolean;
1831
+ sanitizeFormulas?: boolean;
1832
+ db?: unknown;
1833
+ }
1834
+
1835
+ export interface CsvStreamWriter {
1836
+ writeHeader(columns?: (CsvColumn | string)[]): void;
1837
+ writeRow(row: Record<string, unknown>): void;
1838
+ writeRows(rows: Record<string, unknown>[]): void;
1839
+ end(): void;
1840
+ stream: import('stream').Writable;
1841
+ }
1842
+
1843
+ export interface CsvToolkit {
1844
+ MIME_TYPE: string;
1845
+ UTF8_BOM: string;
1846
+ generate(options?: CsvGenerateOptions): Buffer | string;
1847
+ parse(input: Buffer | string | import('stream').Readable, options?: CsvParseOptions): Promise<Record<string, unknown>[]>;
1848
+ fromModel(repository: unknown, filterOrQuery?: unknown, options?: CsvModelExportOptions): Promise<Buffer>;
1849
+ createStreamWriter(outputStream: import('stream').Writable, options?: Record<string, unknown>): CsvStreamWriter;
1850
+ sanitizeFormula(value: unknown): unknown;
1851
+ escapeCsvField(value: unknown, options?: Record<string, unknown>): string;
1852
+ createMiddleware(globalOptions?: CsvPluginOptions): import('express').RequestHandler;
1853
+ createServices(options?: Record<string, unknown>): Record<string, unknown>;
1854
+ }
1855
+
1856
+ export const csv: CsvToolkit;
1857
+ export function csvPlugin(options?: CsvPluginOptions): WebspressoPlugin;
1858
+
1859
+ // --- Discovery, Pages, API & Modules ---
1860
+
1861
+ export interface PageContext<TParams = Record<string, string>, TQuery = Record<string, string>> {
1862
+ req: import('express').Request;
1863
+ res: import('express').Response;
1864
+ params: TParams;
1865
+ query: TQuery;
1866
+ service: <TInput = unknown, TOutput = unknown>(name: string, input?: TInput, opts?: unknown) => Promise<TOutput>;
1867
+ module: string | null;
1868
+ config: Record<string, unknown>;
1869
+ logger: Console;
1870
+ redirect: (url: string, status?: number) => { __redirected: boolean };
1871
+ error: (status?: number, message?: string) => never;
1872
+ fsy: Record<string, unknown>;
1873
+ locale: string;
1874
+ t: (key: string, params?: Record<string, unknown>) => string;
1875
+ db: unknown;
1876
+ }
1877
+
1878
+ export interface DefinePageOptions<TData = any, TParams = Record<string, string>, TQuery = Record<string, string>> {
1879
+ middleware?: (string | import('express').RequestHandler | [string, Record<string, unknown>])[];
1880
+ load?: (ctx: PageContext<TParams, TQuery>) => Promise<TData> | TData;
1881
+ head?: (data: TData, ctx: PageContext<TParams, TQuery>) => Record<string, unknown>;
1882
+ render?: (data: TData, ctx: PageContext<TParams, TQuery>) => string | Promise<string>;
1883
+ layout?: string | boolean;
1884
+ hooks?: Record<string, unknown>;
1885
+ meta?: Record<string, unknown>;
1886
+ [key: string]: unknown;
1887
+ }
1888
+
1889
+ export function definePage<TData = any, TParams = Record<string, string>, TQuery = Record<string, string>>(
1890
+ config: DefinePageOptions<TData, TParams, TQuery> | ((ctx: PageContext<TParams, TQuery>) => Promise<TData> | TData)
1891
+ ): DefinePageOptions<TData, TParams, TQuery>;
1892
+
1893
+ export interface ApiContext<TBody = any, TParams = Record<string, string>, TQuery = Record<string, string>> {
1894
+ req: import('express').Request;
1895
+ res: import('express').Response;
1896
+ params: TParams;
1897
+ query: TQuery;
1898
+ body: TBody;
1899
+ input: { body?: TBody; params?: TParams; query?: TQuery; headers?: Record<string, string> };
1900
+ service: <TInput = unknown, TOutput = unknown>(name: string, input?: TInput, opts?: unknown) => Promise<TOutput>;
1901
+ module: string | null;
1902
+ config: Record<string, unknown>;
1903
+ logger: Console;
1904
+ db: unknown;
1905
+ }
1906
+
1907
+ export interface DefineApiOptions<TResult = any, TBody = any, TParams = Record<string, string>, TQuery = Record<string, string>> {
1908
+ description?: string;
1909
+ tags?: string[];
1910
+ middleware?: (string | import('express').RequestHandler | [string, Record<string, unknown>])[];
1911
+ schema?: {
1912
+ params?: unknown;
1913
+ query?: unknown;
1914
+ body?: unknown;
1915
+ headers?: unknown;
1916
+ } | ((helpers: { z: typeof import('zod').z }) => Record<string, unknown>);
1917
+ response?: Record<number | string, unknown>;
1918
+ handler: (req: import('express').Request, ctx: ApiContext<TBody, TParams, TQuery>) => Promise<TResult> | TResult;
1919
+ [key: string]: unknown;
1920
+ }
1921
+
1922
+ export function defineApi<TResult = any, TBody = any, TParams = Record<string, string>, TQuery = Record<string, string>>(
1923
+ config: DefineApiOptions<TResult, TBody, TParams, TQuery> | ((req: import('express').Request, ctx: ApiContext<TBody, TParams, TQuery>) => Promise<TResult> | TResult)
1924
+ ): DefineApiOptions<TResult, TBody, TParams, TQuery>;
1925
+
1926
+ export interface DefineModuleOptions {
1927
+ name: string;
1928
+ version?: string;
1929
+ description?: string;
1930
+ imports?: string[];
1931
+ dependencies?: string[];
1932
+ pages?: boolean | { dir?: string; prefix?: string };
1933
+ api?: boolean | { dir?: string; prefix?: string };
1934
+ services?: Record<string, unknown> | boolean | { dir?: string };
1935
+ middlewares?: Record<string, import('express').RequestHandler> | boolean | { dir?: string };
1936
+ exports?: Record<string, unknown>;
1937
+ apiExports?: Record<string, unknown>;
1938
+ onInit?: (ctx: unknown) => void | Promise<void>;
1939
+ onDestroy?: (ctx: unknown) => void | Promise<void>;
1940
+ [key: string]: unknown;
1941
+ }
1942
+
1943
+ export function defineModule(config: DefineModuleOptions): (userOptions?: Record<string, unknown>) => WebspressoPlugin;
1944
+
1945
+ export interface RouteDescriptor {
1946
+ type: 'page' | 'api';
1947
+ method: string;
1948
+ path: string;
1949
+ file: string;
1950
+ source?: string;
1951
+ module?: string | null;
1952
+ description?: string;
1953
+ tags?: string[];
1954
+ moduleConfig?: unknown;
1955
+ }
1956
+
1957
+ export class RouteTable {
1958
+ routes: ReadonlyArray<RouteDescriptor>;
1959
+ constructor(routes?: RouteDescriptor[]);
1960
+ list(): Array<{ method: string; path: string; type: string; module: string | null; source: string; description?: string; tags?: string[] }>;
1961
+ get(method: string, path: string): RouteDescriptor | undefined;
1962
+ has(method: string, path: string): boolean;
1963
+ get size(): number;
1964
+ }
1965
+
1966
+ export function scanRoutes(options?: {
1967
+ rootDir?: string;
1968
+ pages?: boolean | { dir?: string; prefix?: string };
1969
+ api?: boolean | { dir?: string; prefix?: string };
1970
+ modules?: boolean | { dir?: string };
1971
+ }): RouteDescriptor[];
1972
+
1973
+ export function parseFileRoute(
1974
+ relativePath: string,
1975
+ options?: { type?: 'page' | 'api'; prefix?: string }
1976
+ ): { path: string; method: string; isValid: boolean; isPrivate: boolean; originalMethodPart?: string };
1977
+
1978
+ export function compileRouteTable(
1979
+ descriptors?: RouteDescriptor[],
1980
+ options?: { explicitRoutes?: Array<{ method?: string; path: string }>; isDev?: boolean }
1981
+ ): RouteTable;
1982
+
1983
+
1984
+
1985
+
1680
1986
 
package/index.js CHANGED
@@ -52,6 +52,15 @@ const {
52
52
  resetPluginManager
53
53
  } = require('./src/plugin-manager');
54
54
 
55
+ // Discovery, Pages, API, Modules & Routing primitives
56
+ const { definePage } = require('./src/pages/define-page');
57
+ const { defineApi } = require('./src/api/define-api');
58
+ const { defineModule } = require('./src/modules/define-module');
59
+ const { scanRoutes } = require('./src/discovery/scan-routes');
60
+ const { parseFileRoute } = require('./src/discovery/file-route-parser');
61
+ const { compileRouteTable, RouteTable } = require('./src/routing/route-table');
62
+ const { mountDiscoveredRoutes } = require('./src/routing/mount-discovered-routes');
63
+
55
64
  // ORM exports (lazy loaded)
56
65
  const orm = require('./core/orm');
57
66
 
@@ -90,6 +99,11 @@ const {
90
99
  mcpPlugin,
91
100
  createMcpServer,
92
101
  McpServer,
102
+ fileManagerPlugin,
103
+ xlsxPlugin,
104
+ xlsx,
105
+ csvPlugin,
106
+ csv,
93
107
  } = require('./plugins');
94
108
 
95
109
  const content = require('./core/content');
@@ -127,6 +141,7 @@ module.exports = {
127
141
  createAuthServices: require('./src/services').createAuthServices,
128
142
  createMailServices: require('./src/services').createMailServices,
129
143
  createMediaServices: require('./src/services').createMediaServices,
144
+ createFileManagerServices: require('./src/services').createFileManagerServices,
130
145
  createSystemServices: require('./src/services').createSystemServices,
131
146
  createExchangeServices: require('./src/services').createExchangeServices,
132
147
  validateServiceInput: require('./src/services').validateServiceInput,
@@ -205,8 +220,9 @@ module.exports = {
205
220
  /** Event bus / plugin shell / view resolver / flows (`kernel.createApp` is distinct from SSR `createApp`) */
206
221
  kernel,
207
222
 
208
- // Direct zdb export (for convenience)
223
+ // Direct zdb & z export (for convenience)
209
224
  zdb: orm.zdb,
225
+ z: require('./core/validation').z,
210
226
 
211
227
  // Plugins
212
228
  schemaExplorerPlugin,
@@ -243,6 +259,24 @@ module.exports = {
243
259
  mcpPlugin,
244
260
  createMcpServer,
245
261
  McpServer,
262
+ fileManagerPlugin,
263
+ xlsxPlugin,
264
+ xlsx,
265
+ csvPlugin,
266
+ csv,
267
+
268
+ // Discovery, Pages, API, Modules & Routing primitives
269
+ definePage,
270
+ page: definePage,
271
+ defineApi,
272
+ api: defineApi,
273
+ defineModule,
274
+ module: defineModule,
275
+ scanRoutes,
276
+ parseFileRoute,
277
+ compileRouteTable,
278
+ RouteTable,
279
+ mountDiscoveredRoutes,
246
280
 
247
281
  // Schema-driven CMS core (framework-agnostic)
248
282
  content,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.95",
3
+ "version": "0.0.97",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -38,10 +38,27 @@
38
38
  },
39
39
  "scripts": {
40
40
  "test": "vitest run",
41
+ "test:unit": "vitest run tests/unit/",
42
+ "test:integration": "vitest run tests/integration/",
41
43
  "test:security": "vitest run tests/security/",
42
- "security": "npm run test:security && npm audit",
44
+ "test:orm": "vitest run tests/**/orm* tests/**/database*",
45
+ "test:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js",
46
+ "test:admin": "vitest run tests/**/admin*",
47
+ "test:services": "vitest run tests/**/service*",
48
+ "test:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js",
49
+ "test:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template*",
50
+ "test:branches": "vitest run tests/unit/*branches.test.js",
51
+ "test:fast": "vitest run tests/unit/*branches.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js",
43
52
  "test:watch": "vitest",
44
53
  "test:coverage": "vitest run --coverage",
54
+ "test:coverage:branches": "vitest run tests/unit/*branches.test.js --coverage",
55
+ "test:coverage:unit": "vitest run tests/unit/ --coverage",
56
+ "test:coverage:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template* --coverage",
57
+ "test:coverage:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js --coverage",
58
+ "test:coverage:orm": "vitest run tests/**/orm* tests/**/database* --coverage",
59
+ "test:coverage:services": "vitest run tests/**/service* --coverage",
60
+ "test:coverage:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js --coverage",
61
+ "test:coverage:fast": "vitest run tests/unit/*branches.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js --coverage",
45
62
  "test:e2e": "playwright test",
46
63
  "test:e2e:ui": "playwright test --ui",
47
64
  "test:e2e:debug": "playwright test --debug",
@@ -80,10 +97,12 @@
80
97
  "index.d.ts",
81
98
  "bin/",
82
99
  "src/",
83
- "utils/",
84
100
  "core/",
85
101
  "plugins/",
86
- "templates/",
102
+ ".agents/",
103
+ "AGENTS.md",
104
+ "CLAUDE.md",
105
+ ".cursorrules",
87
106
  "views/partials/webspresso-client-runtime.njk"
88
107
  ],
89
108
  "dependencies": {
@@ -94,7 +113,7 @@
94
113
  "commander": "^11.1.0",
95
114
  "connect-timeout": "^1.9.1",
96
115
  "cookie-parser": "^1.4.7",
97
- "csv-parse": "^5.6.0",
116
+ "csv-parse": "^7.0.2",
98
117
  "dayjs": "^1.11.19",
99
118
  "exceljs": "^4.4.0",
100
119
  "express": "^5.2.1",
@@ -103,11 +122,11 @@
103
122
  "inquirer": "^8.2.6",
104
123
  "knex": "^3.1.0",
105
124
  "mjml": "^5.4.0",
106
- "multer": "^2.1.1",
107
- "nodemailer": "^9.0.3",
125
+ "multer": "^2.4.0",
126
+ "nodemailer": "^10.0.10",
108
127
  "nunjucks": "^3.2.4",
109
- "sanitize-html": "^2.17.3",
110
- "sharp": "^0.35.3",
128
+ "sanitize-html": "^2.17.7",
129
+ "sharp": "^0.35.4",
111
130
  "swup": "^4.8.3",
112
131
  "yaml": "^2.8.3",
113
132
  "zod": "^3.23.0",
@@ -142,11 +161,11 @@
142
161
  }
143
162
  },
144
163
  "devDependencies": {
145
- "@faker-js/faker": "^9.9.0",
164
+ "@faker-js/faker": "^10.6.0",
146
165
  "@playwright/test": "^1.48.0",
147
166
  "@types/express": "^4.17.21",
148
167
  "@types/node": "^20.14.0",
149
- "@vitest/coverage-v8": "^3.0.0",
168
+ "@vitest/coverage-v8": "^4.1.11",
150
169
  "better-sqlite3": "^11.10.0",
151
170
  "chokidar": "^3.5.3",
152
171
  "docboot": "^0.3.0",
@@ -155,16 +174,19 @@
155
174
  "release-it": "^20.2.1",
156
175
  "supertest": "^6.3.4",
157
176
  "typescript": "~5.6.3",
158
- "vitest": "^3.0.0"
177
+ "vitest": "^4.1.11",
178
+ "vitest-doctor": "^0.3.0"
159
179
  },
160
180
  "engines": {
161
- "node": ">=18.0.0"
181
+ "node": ">=20.0.0"
162
182
  },
163
183
  "overrides": {
164
184
  "basic-ftp": "^5.3.0",
165
185
  "tar": "^7.5.11",
166
186
  "undici": "^6.24.0",
167
- "uuid": "^11.1.1"
187
+ "uuid": "^11.1.1",
188
+ "js-yaml": "^5.4.2",
189
+ "qs": "^6.16.0"
168
190
  },
169
191
  "allowScripts": {
170
192
  "better-sqlite3@11.10.0": true
@@ -248,9 +248,10 @@ function wrapCustomPageWithLayout(page, rawComp) {
248
248
  return {
249
249
  oninit: function(vnode) {
250
250
  try {
251
- vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
252
- if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
253
- return vnode.state.instance.oninit.call(this, vnode);
251
+ const instance = typeof rawComp === 'function' ? rawComp.call(rawComp, vnode) : rawComp;
252
+ vnode.state.instance = instance;
253
+ if (instance && typeof instance.oninit === 'function') {
254
+ return instance.oninit.call(instance, vnode);
254
255
  }
255
256
  } catch (err) {
256
257
  console.error('[Admin Panel] Error in oninit for custom page "' + page.id + '":', err);
@@ -260,7 +261,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
260
261
  oncreate: function(vnode) {
261
262
  try {
262
263
  const comp = vnode.state.instance || rawComp;
263
- if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
264
+ if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(comp, vnode);
264
265
  } catch (err) {
265
266
  console.error('[Admin Panel] Error in oncreate for custom page "' + page.id + '":', err);
266
267
  }
@@ -268,7 +269,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
268
269
  onupdate: function(vnode) {
269
270
  try {
270
271
  const comp = vnode.state.instance || rawComp;
271
- if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
272
+ if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(comp, vnode);
272
273
  } catch (err) {
273
274
  console.error('[Admin Panel] Error in onupdate for custom page "' + page.id + '":', err);
274
275
  }
@@ -276,7 +277,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
276
277
  onbeforeremove: function(vnode) {
277
278
  try {
278
279
  const comp = vnode.state.instance || rawComp;
279
- if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
280
+ if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(comp, vnode);
280
281
  } catch (err) {
281
282
  console.error('[Admin Panel] Error in onbeforeremove for custom page "' + page.id + '":', err);
282
283
  }
@@ -284,7 +285,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
284
285
  onremove: function(vnode) {
285
286
  try {
286
287
  const comp = vnode.state.instance || rawComp;
287
- if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
288
+ if (comp && typeof comp.onremove === 'function') return comp.onremove.call(comp, vnode);
288
289
  } catch (err) {
289
290
  console.error('[Admin Panel] Error in onremove for custom page "' + page.id + '":', err);
290
291
  }
@@ -305,9 +306,9 @@ function wrapCustomPageWithLayout(page, rawComp) {
305
306
  var content;
306
307
  try {
307
308
  if (comp && typeof comp.view === 'function') {
308
- content = comp.view.call(this, vnode);
309
+ content = comp.view.call(comp, vnode);
309
310
  } else if (typeof comp === 'function') {
310
- content = comp.call(this, vnode);
311
+ content = comp.call(comp, vnode);
311
312
  } else {
312
313
  content = comp;
313
314
  }
@@ -305,7 +305,28 @@ const FileUploadField = {
305
305
  }),
306
306
  m('div', [
307
307
  m('p.text-gray-600.dark:text-slate-400.mb-2', displayUrl ? 'Drag and drop to replace, or' : 'Drag and drop a file here, or'),
308
- m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer', { for: 'file-input-' + col.name }, displayUrl ? 'choose another file' : 'browse'),
308
+ m('div.flex.items-center.justify-center.gap-3.flex-wrap', [
309
+ m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer.font-medium', { for: 'file-input-' + col.name }, displayUrl ? 'choose another file' : 'browse'),
310
+ typeof window !== 'undefined' && window.__openFileManagerPicker
311
+ ? m('button.text-indigo-600.hover:text-indigo-800.dark:text-indigo-400.cursor-pointer.font-medium.flex.items-center.gap-1', {
312
+ type: 'button',
313
+ onclick: function(e) {
314
+ e.preventDefault();
315
+ e.stopPropagation();
316
+ window.__openFileManagerPicker({
317
+ accept: accept,
318
+ title: '📁 Ortam Kütüphanesinden Seç',
319
+ onSelect: function(pickedUrl) {
320
+ if (onChange) onChange(pickedUrl);
321
+ }
322
+ });
323
+ }
324
+ }, [
325
+ m('span', '•'),
326
+ m('span', 'Kütüphaneden Seç')
327
+ ])
328
+ : null
329
+ ])
309
330
  ]),
310
331
  ]),
311
332
  m('input[type=hidden]', { name: col.name, value: typeof value === 'string' ? value : '' }),
@@ -24,6 +24,10 @@ let npmVersionCache = {
24
24
  };
25
25
 
26
26
  function fetchLatestNpmVersion(force = false) {
27
+ if ((process.env.NODE_ENV === 'test' || process.env.VITEST) && !force) {
28
+ return Promise.resolve(npmVersionCache.latestVersion || getWebspressoVersion());
29
+ }
30
+
27
31
  const now = Date.now();
28
32
  if (!force && npmVersionCache.latestVersion && (now - npmVersionCache.checkedAt < 30 * 60 * 1000)) {
29
33
  return Promise.resolve(npmVersionCache.latestVersion);
@@ -117,7 +117,9 @@ function sendUnauthorized(req, res, config) {
117
117
 
118
118
  if (config.challenge !== false) {
119
119
  const realm = String(config.realm || 'Restricted Area')
120
- .split(/[\r\n]/)[0]
120
+ .split(/[\r\n\0]/)[0]
121
+ .replace(/[\t]/g, ' ')
122
+ .replace(/\\/g, '\\\\')
121
123
  .replace(/"/g, '\\"');
122
124
  res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
123
125
  }
@@ -248,9 +248,14 @@
248
248
  el.textContent = '';
249
249
  return;
250
250
  }
251
+ if (typeof window.DOMPurify !== 'undefined' && typeof window.DOMPurify.sanitize === 'function') {
252
+ el.innerHTML = window.DOMPurify.sanitize(htmlStr);
253
+ return;
254
+ }
251
255
  try {
252
- var parser = new DOMParser();
253
- var doc = parser.parseFromString(htmlStr, 'text/html');
256
+ var template = document.createElement('template');
257
+ template.innerHTML = htmlStr;
258
+ var content = template.content;
254
259
  var allowedTags = {
255
260
  p: 1, br: 1, b: 1, i: 1, strong: 1, em: 1, u: 1, s: 1, strike: 1,
256
261
  h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1,
@@ -260,14 +265,14 @@
260
265
  var allowedAttrs = {
261
266
  href: 1, title: 1, target: 1, rel: 1, class: 1, id: 1, src: 1, alt: 1, width: 1, height: 1
262
267
  };
263
- var allNodes = doc.body.querySelectorAll('*');
268
+ var allNodes = content.querySelectorAll('*');
264
269
  for (var i = allNodes.length - 1; i >= 0; i--) {
265
270
  var node = allNodes[i];
266
271
  var tag = node.tagName.toLowerCase();
267
272
  if (!allowedTags[tag]) {
268
273
  if (node.parentNode) {
269
274
  node.parentNode.removeChild(node);
270
- } else {
275
+ } else if (typeof node.remove === 'function') {
271
276
  node.remove();
272
277
  }
273
278
  continue;
@@ -275,7 +280,7 @@
275
280
  var attrs = Array.prototype.slice.call(node.attributes);
276
281
  for (var j = 0; j < attrs.length; j++) {
277
282
  var attrName = attrs[j].name.toLowerCase();
278
- var attrVal = attrs[j].value.toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
283
+ var attrVal = (attrs[j].value || '').toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
279
284
  if (attrName.indexOf('on') === 0 || !allowedAttrs[attrName]) {
280
285
  node.removeAttribute(attrs[j].name);
281
286
  } else if ((attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'formaction' || attrName === 'xlink:href') &&
@@ -287,13 +292,8 @@
287
292
  }
288
293
  }
289
294
 
290
- // Transfer DOM nodes directly to target element
291
- while (el.firstChild) {
292
- el.removeChild(el.firstChild);
293
- }
294
- while (doc.body.firstChild) {
295
- el.appendChild(doc.body.firstChild);
296
- }
295
+ el.textContent = '';
296
+ el.appendChild(content.cloneNode(true));
297
297
  } catch (e) {
298
298
  el.textContent = htmlStr;
299
299
  }