webspresso 0.0.106 → 0.0.107
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/bin/webspresso.js +17 -7
- package/core/app.js +38 -6
- package/core/auth/index.js +4 -1
- package/core/auth/jwt.js +85 -13
- package/core/auth/magic-link.js +23 -5
- package/core/auth/manager.js +7 -1
- package/core/auth/redis-session.js +62 -25
- package/core/auth/session-manager.js +28 -2
- package/core/auth/sessions.js +182 -52
- package/core/console/eval.js +4 -1
- package/core/content/service.js +1 -0
- package/core/errors/domain.js +20 -2
- package/core/errors/http.js +1 -0
- package/core/http/hono-adapter.js +1 -1
- package/core/http/pipeline.js +36 -2
- package/core/index.js +12 -1
- package/core/mail/mailer.js +1 -1
- package/core/orm/eager-loader.js +81 -1
- package/core/orm/index.js +27 -13
- package/core/orm/json-fields.js +44 -13
- package/core/orm/model.js +12 -0
- package/core/orm/query-builder.js +33 -15
- package/core/orm/repository.js +59 -12
- package/core/orm/scopes.js +54 -4
- package/core/services/index.js +9 -0
- package/core/shutdown/shutdown-manager.js +57 -38
- package/core/studio/inspector.js +9 -1
- package/core/studio/server.js +24 -8
- package/core/utils/string.js +77 -16
- package/core/validation/index.js +18 -0
- package/index.d.ts +172 -8
- package/index.js +4 -0
- package/package.json +18 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
- package/plugins/admin-panel/index.js +24 -11
- package/plugins/admin-panel/modules/user-management.js +3 -2
- package/plugins/csrf/index.js +2 -1
- package/plugins/dashboard/index.js +27 -7
- package/src/app-context.js +9 -1
- package/src/file-router.js +12 -3
- package/src/plugin-manager.js +273 -48
- package/src/server.js +26 -33
- package/src/services/discovery.js +9 -1
- package/src/services/executor.js +271 -58
- package/src/services/index.js +52 -3
- package/src/services/loader.js +120 -0
- package/src/services/memoize.js +223 -49
- package/src/services/registry.js +492 -94
- package/src/services/validator.js +164 -13
package/core/utils/string.js
CHANGED
|
@@ -18,10 +18,35 @@
|
|
|
18
18
|
*/
|
|
19
19
|
function toCamelCase(str) {
|
|
20
20
|
if (!str || typeof str !== 'string') return '';
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const s = str.trim();
|
|
22
|
+
if (!s) return '';
|
|
23
|
+
|
|
24
|
+
let start = 0;
|
|
25
|
+
let end = s.length;
|
|
26
|
+
while (start < end && (s[start] === '-' || s[start] === '_')) {
|
|
27
|
+
start++;
|
|
28
|
+
}
|
|
29
|
+
while (end > start && (s[end - 1] === '-' || s[end - 1] === '_')) {
|
|
30
|
+
end--;
|
|
31
|
+
}
|
|
32
|
+
if (start >= end) return '';
|
|
33
|
+
|
|
34
|
+
let result = '';
|
|
35
|
+
let capitalizeNext = false;
|
|
36
|
+
for (let i = start; i < end; i++) {
|
|
37
|
+
const ch = s[i];
|
|
38
|
+
if (ch === '-' || ch === '_' || ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
39
|
+
capitalizeNext = true;
|
|
40
|
+
} else {
|
|
41
|
+
if (capitalizeNext) {
|
|
42
|
+
result += result.length === 0 ? ch.toLowerCase() : ch.toUpperCase();
|
|
43
|
+
capitalizeNext = false;
|
|
44
|
+
} else {
|
|
45
|
+
result += result.length === 0 ? ch.toLowerCase() : ch;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
/**
|
|
@@ -35,12 +60,30 @@ function toCamelCase(str) {
|
|
|
35
60
|
*/
|
|
36
61
|
function toSnakeCase(str) {
|
|
37
62
|
if (!str || typeof str !== 'string') return '';
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
const s = str.trim();
|
|
64
|
+
if (!s) return '';
|
|
65
|
+
|
|
66
|
+
const expanded = s.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
|
|
67
|
+
let result = '';
|
|
68
|
+
let prevWasUnderscore = false;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < expanded.length; i++) {
|
|
71
|
+
const ch = expanded[i];
|
|
72
|
+
if (ch === '-' || ch === '_' || ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
73
|
+
if (!prevWasUnderscore && result.length > 0) {
|
|
74
|
+
result += '_';
|
|
75
|
+
prevWasUnderscore = true;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
result += ch.toLowerCase();
|
|
79
|
+
prevWasUnderscore = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
while (result.endsWith('_')) {
|
|
84
|
+
result = result.slice(0, -1);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
44
87
|
}
|
|
45
88
|
|
|
46
89
|
/**
|
|
@@ -54,12 +97,30 @@ function toSnakeCase(str) {
|
|
|
54
97
|
*/
|
|
55
98
|
function toKebabCase(str) {
|
|
56
99
|
if (!str || typeof str !== 'string') return '';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
const s = str.trim();
|
|
101
|
+
if (!s) return '';
|
|
102
|
+
|
|
103
|
+
const expanded = s.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
104
|
+
let result = '';
|
|
105
|
+
let prevWasDash = false;
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < expanded.length; i++) {
|
|
108
|
+
const ch = expanded[i];
|
|
109
|
+
if (ch === '-' || ch === '_' || ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
110
|
+
if (!prevWasDash && result.length > 0) {
|
|
111
|
+
result += '-';
|
|
112
|
+
prevWasDash = true;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
result += ch.toLowerCase();
|
|
116
|
+
prevWasDash = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
while (result.endsWith('-')) {
|
|
121
|
+
result = result.slice(0, -1);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
63
124
|
}
|
|
64
125
|
|
|
65
126
|
/**
|
package/core/validation/index.js
CHANGED
|
@@ -12,6 +12,24 @@ const { extendZ, generateNanoid, zodNanoid } = require('../orm/utils/nanoid');
|
|
|
12
12
|
*/
|
|
13
13
|
const zExtended = extendZ(z);
|
|
14
14
|
|
|
15
|
+
// Ensure asynchronous refinements and superRefinements never leak dangling unhandled rejections
|
|
16
|
+
// if evaluated during a synchronous parse attempt before falling back to async parsing.
|
|
17
|
+
if (z.ZodType && z.ZodType.prototype && typeof z.ZodType.prototype._refinement === 'function' && !z.ZodType.prototype._refinement.__webspresso_wrapped) {
|
|
18
|
+
const origRefinement = z.ZodType.prototype._refinement;
|
|
19
|
+
const wrappedRefinement = function (refinement) {
|
|
20
|
+
const wrapped = (val, ctx) => {
|
|
21
|
+
const res = refinement(val, ctx);
|
|
22
|
+
if (res && typeof res.catch === 'function') {
|
|
23
|
+
res.catch(() => {});
|
|
24
|
+
}
|
|
25
|
+
return res;
|
|
26
|
+
};
|
|
27
|
+
return origRefinement.call(this, wrapped);
|
|
28
|
+
};
|
|
29
|
+
wrappedRefinement.__webspresso_wrapped = true;
|
|
30
|
+
z.ZodType.prototype._refinement = wrappedRefinement;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
/**
|
|
16
34
|
* Check if an object contains dangerous prototype pollution keys
|
|
17
35
|
* @param {*} obj
|
package/index.d.ts
CHANGED
|
@@ -218,8 +218,14 @@ export interface ServiceContext {
|
|
|
218
218
|
t?: unknown;
|
|
219
219
|
service?: {
|
|
220
220
|
<T = unknown>(name: string, input?: unknown, options?: unknown): Promise<T>;
|
|
221
|
+
call?: <T = unknown>(name: string, input?: unknown, options?: unknown) => Promise<T>;
|
|
222
|
+
execute?: <T = unknown>(name: string, input?: unknown, options?: unknown) => Promise<T>;
|
|
221
223
|
invalidate?: (name: string, input?: unknown, ctx?: unknown) => boolean;
|
|
222
224
|
clearCache?: (name?: string) => boolean;
|
|
225
|
+
invalidateByTag?: (tag: string, serviceName?: string) => number | Promise<number>;
|
|
226
|
+
invalidateByTags?: (tags: string | string[], serviceName?: string) => number | Promise<number>;
|
|
227
|
+
invalidateTag?: (tag: string, serviceName?: string) => number | Promise<number>;
|
|
228
|
+
invalidateTags?: (tags: string | string[], serviceName?: string) => number | Promise<number>;
|
|
223
229
|
};
|
|
224
230
|
breaker?: (name: string, options?: CircuitBreakerOptions) => CircuitBreaker;
|
|
225
231
|
retry?: typeof retry;
|
|
@@ -231,6 +237,43 @@ export type ServiceHandler<TInput = unknown, TOutput = unknown> = (
|
|
|
231
237
|
ctx: ServiceContext
|
|
232
238
|
) => Promise<TOutput> | TOutput;
|
|
233
239
|
|
|
240
|
+
export interface ServiceMiddlewareContext<TInput = unknown> {
|
|
241
|
+
name: string;
|
|
242
|
+
input: TInput;
|
|
243
|
+
ctx: ServiceContext;
|
|
244
|
+
serviceDef: ServiceDefinition<TInput, any>;
|
|
245
|
+
options: unknown;
|
|
246
|
+
registry: ServiceRegistry;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface ServiceBeforeContext<TInput = unknown> {
|
|
250
|
+
name: string;
|
|
251
|
+
input: TInput;
|
|
252
|
+
ctx: ServiceContext;
|
|
253
|
+
serviceDef: ServiceDefinition<TInput, any>;
|
|
254
|
+
options: unknown;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface ServiceAfterContext<TInput = unknown, TOutput = unknown> {
|
|
258
|
+
name: string;
|
|
259
|
+
result: TOutput;
|
|
260
|
+
input: TInput;
|
|
261
|
+
ctx: ServiceContext;
|
|
262
|
+
serviceDef: ServiceDefinition<TInput, TOutput>;
|
|
263
|
+
duration: number;
|
|
264
|
+
options: unknown;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface ServiceErrorContext<TInput = unknown> {
|
|
268
|
+
name: string;
|
|
269
|
+
error: Error;
|
|
270
|
+
input: TInput;
|
|
271
|
+
ctx: ServiceContext;
|
|
272
|
+
serviceDef: ServiceDefinition<TInput, any>;
|
|
273
|
+
duration: number;
|
|
274
|
+
options: unknown;
|
|
275
|
+
}
|
|
276
|
+
|
|
234
277
|
export interface ServiceDefinition<TInput = unknown, TOutput = unknown> {
|
|
235
278
|
schema?: import('zod').ZodType<any> | Record<string, unknown> | ((input: unknown) => unknown) | null;
|
|
236
279
|
handler: ServiceHandler<TInput, TOutput>;
|
|
@@ -248,21 +291,66 @@ export interface ServiceDefinition<TInput = unknown, TOutput = unknown> {
|
|
|
248
291
|
transaction?: boolean;
|
|
249
292
|
circuitBreaker?: boolean | string | CircuitBreakerOptions | CircuitBreaker;
|
|
250
293
|
retry?: boolean | number | RetryOptions;
|
|
294
|
+
before?: ((input: TInput, ctx: ServiceContext) => unknown) | Array<(input: TInput, ctx: ServiceContext) => unknown>;
|
|
295
|
+
after?: ((result: TOutput, input: TInput, ctx: ServiceContext) => unknown) | Array<(result: TOutput, input: TInput, ctx: ServiceContext) => unknown>;
|
|
296
|
+
onError?: ((error: Error, input: TInput, ctx: ServiceContext) => unknown) | Array<(error: Error, input: TInput, ctx: ServiceContext) => unknown>;
|
|
297
|
+
middleware?: ((context: ServiceMiddlewareContext<TInput>, next: () => Promise<unknown>) => Promise<unknown>) | Array<(context: ServiceMiddlewareContext<TInput>, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
251
298
|
[key: string]: unknown;
|
|
252
299
|
}
|
|
253
300
|
|
|
254
301
|
export class ServiceRegistry {
|
|
255
|
-
constructor(options?: {
|
|
302
|
+
constructor(options?: {
|
|
303
|
+
servicesDir?: string;
|
|
304
|
+
isDev?: boolean;
|
|
305
|
+
logger?: unknown;
|
|
306
|
+
timeouts?: unknown;
|
|
307
|
+
timeout?: number | string;
|
|
308
|
+
lazy?: boolean;
|
|
309
|
+
middlewares?: Array<(context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
310
|
+
middleware?: (context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
|
|
311
|
+
before?: Array<(context: ServiceBeforeContext) => unknown> | ((context: ServiceBeforeContext) => unknown);
|
|
312
|
+
after?: Array<(context: ServiceAfterContext) => unknown> | ((context: ServiceAfterContext) => unknown);
|
|
313
|
+
onError?: Array<(context: ServiceErrorContext) => unknown> | ((context: ServiceErrorContext) => unknown);
|
|
314
|
+
events?: unknown;
|
|
315
|
+
cacheProvider?: unknown;
|
|
316
|
+
cache?: unknown;
|
|
317
|
+
});
|
|
256
318
|
readonly services: Map<string, ServiceDefinition>;
|
|
319
|
+
readonly lazyServices: Map<string, unknown>;
|
|
320
|
+
readonly aliases: Map<string, string>;
|
|
321
|
+
readonly middlewares: Array<(context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
322
|
+
readonly beforeHooks: Array<(context: ServiceBeforeContext) => unknown>;
|
|
323
|
+
readonly afterHooks: Array<(context: ServiceAfterContext) => unknown>;
|
|
324
|
+
readonly onErrorHooks: Array<(context: ServiceErrorContext) => unknown>;
|
|
257
325
|
load(): this;
|
|
258
|
-
|
|
326
|
+
loadAsync(): Promise<this>;
|
|
327
|
+
register(name: string, definition: ServiceDefinition | ServiceHandler, options?: { overwrite?: boolean; aliases?: string[] }): this;
|
|
328
|
+
registerLazy(
|
|
329
|
+
name: string,
|
|
330
|
+
loader: (() => Promise<unknown> | unknown) | string,
|
|
331
|
+
options?: { overwrite?: boolean; aliases?: string[]; filePath?: string }
|
|
332
|
+
): this;
|
|
333
|
+
use(middleware: (context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>): this;
|
|
334
|
+
before(hook: (context: ServiceBeforeContext) => unknown): this;
|
|
335
|
+
after(hook: (context: ServiceAfterContext) => unknown): this;
|
|
336
|
+
onError(hook: (context: ServiceErrorContext) => unknown): this;
|
|
337
|
+
setEventDispatcher(events: unknown): this;
|
|
259
338
|
get(name: string): ServiceDefinition | undefined;
|
|
339
|
+
getAsync(name: string): Promise<ServiceDefinition | undefined>;
|
|
260
340
|
has(name: string): boolean;
|
|
261
341
|
list(): string[];
|
|
342
|
+
unregister(name: string): boolean;
|
|
262
343
|
invalidate(name: string, input?: unknown, ctx?: unknown): boolean;
|
|
263
344
|
clearCache(name?: string): boolean;
|
|
345
|
+
clearAllCaches(): boolean;
|
|
346
|
+
invalidateByTag(tag: string, serviceName?: string): number | Promise<number>;
|
|
347
|
+
invalidateByTags(tags: string | string[], serviceName?: string): number | Promise<number>;
|
|
348
|
+
invalidateTag?(tag: string, serviceName?: string): number | Promise<number>;
|
|
349
|
+
invalidateTags?(tags: string | string[], serviceName?: string): number | Promise<number>;
|
|
264
350
|
call<T = unknown>(name: string, input?: unknown, ctx?: unknown, options?: unknown): Promise<T>;
|
|
351
|
+
execute<T = unknown>(name: string, input?: unknown, ctx?: unknown, options?: unknown): Promise<T>;
|
|
265
352
|
reload(): this;
|
|
353
|
+
reloadAsync(): Promise<this>;
|
|
266
354
|
}
|
|
267
355
|
|
|
268
356
|
export function createServiceRegistry(options?: {
|
|
@@ -270,8 +358,42 @@ export function createServiceRegistry(options?: {
|
|
|
270
358
|
isDev?: boolean;
|
|
271
359
|
logger?: unknown;
|
|
272
360
|
autoLoad?: boolean;
|
|
361
|
+
lazy?: boolean;
|
|
362
|
+
timeouts?: unknown;
|
|
363
|
+
timeout?: number | string;
|
|
364
|
+
middlewares?: Array<(context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
365
|
+
middleware?: (context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
|
|
366
|
+
before?: Array<(context: ServiceBeforeContext) => unknown> | ((context: ServiceBeforeContext) => unknown);
|
|
367
|
+
after?: Array<(context: ServiceAfterContext) => unknown> | ((context: ServiceAfterContext) => unknown);
|
|
368
|
+
onError?: Array<(context: ServiceErrorContext) => unknown> | ((context: ServiceErrorContext) => unknown);
|
|
369
|
+
events?: unknown;
|
|
370
|
+
cacheProvider?: unknown;
|
|
371
|
+
cache?: unknown;
|
|
273
372
|
}): ServiceRegistry;
|
|
274
373
|
|
|
374
|
+
export function createServiceRegistryAsync(options?: {
|
|
375
|
+
servicesDir?: string;
|
|
376
|
+
isDev?: boolean;
|
|
377
|
+
logger?: unknown;
|
|
378
|
+
autoLoad?: boolean;
|
|
379
|
+
lazy?: boolean;
|
|
380
|
+
timeouts?: unknown;
|
|
381
|
+
timeout?: number | string;
|
|
382
|
+
middlewares?: Array<(context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
383
|
+
middleware?: (context: ServiceMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
|
|
384
|
+
before?: Array<(context: ServiceBeforeContext) => unknown> | ((context: ServiceBeforeContext) => unknown);
|
|
385
|
+
after?: Array<(context: ServiceAfterContext) => unknown> | ((context: ServiceAfterContext) => unknown);
|
|
386
|
+
onError?: Array<(context: ServiceErrorContext) => unknown> | ((context: ServiceErrorContext) => unknown);
|
|
387
|
+
events?: unknown;
|
|
388
|
+
cacheProvider?: unknown;
|
|
389
|
+
cache?: unknown;
|
|
390
|
+
}): Promise<ServiceRegistry>;
|
|
391
|
+
|
|
392
|
+
export function composeMiddleware(middlewares: Function[]): (context: unknown, next?: Function) => Promise<unknown>;
|
|
393
|
+
export function loadModuleSync(filePath: string, options?: { isDev?: boolean }): unknown;
|
|
394
|
+
export function loadModuleAsync(filePath: string, options?: { isDev?: boolean }): Promise<unknown>;
|
|
395
|
+
export function extractModuleExport(mod: unknown): unknown;
|
|
396
|
+
|
|
275
397
|
export type InferServiceInput<T> = T extends ServiceDefinition<infer TInput, any>
|
|
276
398
|
? TInput
|
|
277
399
|
: T extends { schema: import('zod').ZodType<infer TInput> }
|
|
@@ -297,15 +419,22 @@ export const service: typeof defineService;
|
|
|
297
419
|
export interface MemoizeOptions {
|
|
298
420
|
ttl?: string | number | boolean;
|
|
299
421
|
maxSize?: number;
|
|
422
|
+
clone?: boolean;
|
|
300
423
|
key?: (input: unknown, ctx?: unknown) => string;
|
|
424
|
+
tags?: string | string[] | ((input: unknown, ctx?: unknown) => string | string[]);
|
|
425
|
+
provider?: unknown;
|
|
301
426
|
}
|
|
302
427
|
|
|
303
428
|
export interface MemoizedFunction<TArgs extends unknown[], TReturn> {
|
|
304
429
|
(...args: TArgs): Promise<TReturn>;
|
|
305
|
-
invalidate(...args: TArgs): boolean
|
|
306
|
-
delete(...args: TArgs): boolean
|
|
307
|
-
|
|
308
|
-
|
|
430
|
+
invalidate(...args: TArgs): boolean | Promise<boolean>;
|
|
431
|
+
delete(...args: TArgs): boolean | Promise<boolean>;
|
|
432
|
+
invalidateByTag(tag: string): number | Promise<number>;
|
|
433
|
+
invalidateByTags(tags: string | string[]): number | Promise<number>;
|
|
434
|
+
invalidateTag?(tag: string): number | Promise<number>;
|
|
435
|
+
invalidateTags?(tags: string | string[]): number | Promise<number>;
|
|
436
|
+
clear(): void | Promise<void>;
|
|
437
|
+
has(...args: TArgs): boolean | Promise<boolean>;
|
|
309
438
|
readonly size: number;
|
|
310
439
|
}
|
|
311
440
|
|
|
@@ -363,7 +492,28 @@ export function createExchangeServices(options?: {
|
|
|
363
492
|
db?: unknown;
|
|
364
493
|
}): Record<string, ServiceDefinition>;
|
|
365
494
|
|
|
366
|
-
export function validateServiceInput(
|
|
495
|
+
export function validateServiceInput(
|
|
496
|
+
schema: unknown,
|
|
497
|
+
input: unknown,
|
|
498
|
+
serviceName: string,
|
|
499
|
+
options?: { rejectDangerousKeys?: boolean }
|
|
500
|
+
): unknown;
|
|
501
|
+
|
|
502
|
+
export function createServiceValidationError(
|
|
503
|
+
message: string,
|
|
504
|
+
options?: {
|
|
505
|
+
serviceName: string;
|
|
506
|
+
fields?: Record<string, string>;
|
|
507
|
+
fieldErrors?: Record<string, string[]>;
|
|
508
|
+
details?: unknown[];
|
|
509
|
+
cause?: unknown;
|
|
510
|
+
status?: number;
|
|
511
|
+
}
|
|
512
|
+
): ValidationError;
|
|
513
|
+
|
|
514
|
+
export function normalizeValidationIssues(
|
|
515
|
+
issues: unknown[]
|
|
516
|
+
): Array<{ path: string; message: string; code?: string }>;
|
|
367
517
|
|
|
368
518
|
export function executeService(
|
|
369
519
|
registry: ServiceRegistry,
|
|
@@ -650,7 +800,19 @@ export class CircuitBreakerTimeoutError extends GatewayTimeoutError {
|
|
|
650
800
|
|
|
651
801
|
export class ValidationError extends HttpError {
|
|
652
802
|
fields: Record<string, string[] | string>;
|
|
653
|
-
|
|
803
|
+
fieldErrors?: Record<string, string[]>;
|
|
804
|
+
service?: string;
|
|
805
|
+
issues?: unknown[];
|
|
806
|
+
details?: unknown[];
|
|
807
|
+
normalizedIssues?: Array<{ path: string; message: string; code?: string }>;
|
|
808
|
+
constructor(message?: string, options?: ErrorOptions & {
|
|
809
|
+
fields?: Record<string, string[] | string>;
|
|
810
|
+
fieldErrors?: Record<string, string[]>;
|
|
811
|
+
service?: string;
|
|
812
|
+
statusCode?: number;
|
|
813
|
+
issues?: unknown[];
|
|
814
|
+
details?: unknown[];
|
|
815
|
+
});
|
|
654
816
|
}
|
|
655
817
|
|
|
656
818
|
export class ConfigurationError extends WebspressoError { constructor(message: string, options?: ErrorOptions); }
|
|
@@ -5076,6 +5238,7 @@ export function createUserSessionsTable(knex: any): Promise<void>;
|
|
|
5076
5238
|
export function dropUserSessionsTable(knex: any): Promise<void>;
|
|
5077
5239
|
export function createKnexSessionTracker(db: any, options?: { tableName?: string }): SessionTracker;
|
|
5078
5240
|
export function createMemorySessionTracker(): SessionTracker;
|
|
5241
|
+
export function createKvSessionTracker(kv: any, options?: { prefix?: string; ttlSeconds?: number }): SessionTracker;
|
|
5079
5242
|
export function createListSessionsHandler(authManager: any): (req: any, res: any) => Promise<any>;
|
|
5080
5243
|
export function createRevokeSessionHandler(authManager: any): (req: any, res: any) => Promise<any>;
|
|
5081
5244
|
|
|
@@ -5121,6 +5284,7 @@ export namespace sessions {
|
|
|
5121
5284
|
dropUserSessionsTable,
|
|
5122
5285
|
createKnexSessionTracker,
|
|
5123
5286
|
createMemorySessionTracker,
|
|
5287
|
+
createKvSessionTracker,
|
|
5124
5288
|
createRedisSessionStore,
|
|
5125
5289
|
createRedisSessionTracker,
|
|
5126
5290
|
RedisSessionStore,
|
package/index.js
CHANGED
|
@@ -117,6 +117,7 @@ module.exports = {
|
|
|
117
117
|
setAppContext,
|
|
118
118
|
|
|
119
119
|
// Services Layer
|
|
120
|
+
services: require('./core/services'),
|
|
120
121
|
createServiceRegistry,
|
|
121
122
|
defineService,
|
|
122
123
|
service: defineService,
|
|
@@ -541,6 +542,9 @@ Object.defineProperties(module.exports, {
|
|
|
541
542
|
parseUserAgent: { get: () => require('./core/auth').parseUserAgent, enumerable: true, configurable: true },
|
|
542
543
|
sessions: { get: () => require('./core/auth').sessions, enumerable: true, configurable: true },
|
|
543
544
|
SessionManager: { get: () => require('./core/auth').SessionManager, enumerable: true, configurable: true },
|
|
545
|
+
createKnexSessionTracker: { get: () => require('./core/auth').createKnexSessionTracker, enumerable: true, configurable: true },
|
|
546
|
+
createMemorySessionTracker: { get: () => require('./core/auth').createMemorySessionTracker, enumerable: true, configurable: true },
|
|
547
|
+
createKvSessionTracker: { get: () => require('./core/auth').createKvSessionTracker, enumerable: true, configurable: true },
|
|
544
548
|
createRedisSessionStore: { get: () => require('./core/auth').createRedisSessionStore, enumerable: true, configurable: true },
|
|
545
549
|
createRedisSessionTracker: { get: () => require('./core/auth').createRedisSessionTracker, enumerable: true, configurable: true },
|
|
546
550
|
RedisSessionStore: { get: () => require('./core/auth').RedisSessionStore, enumerable: true, configurable: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
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",
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
"./plugins/polar/src/*": "./plugins/polar/src/*.js",
|
|
19
19
|
"./plugins/*": "./plugins/*/index.js",
|
|
20
20
|
"./realtime": "./core/realtime/index.js",
|
|
21
|
+
"./services": {
|
|
22
|
+
"types": "./index.d.ts",
|
|
23
|
+
"default": "./core/services/index.js"
|
|
24
|
+
},
|
|
21
25
|
"./core": "./core/index.js",
|
|
26
|
+
"./core/services": {
|
|
27
|
+
"types": "./index.d.ts",
|
|
28
|
+
"default": "./core/services/index.js"
|
|
29
|
+
},
|
|
22
30
|
"./core/auth": "./core/auth/index.js",
|
|
23
31
|
"./core/auth/*": "./core/auth/*.js",
|
|
24
32
|
"./core/errors": "./core/errors/index.js",
|
|
@@ -39,17 +47,17 @@
|
|
|
39
47
|
},
|
|
40
48
|
"scripts": {
|
|
41
49
|
"test": "vitest run",
|
|
42
|
-
"test:unit": "vitest run tests/unit/",
|
|
43
|
-
"test:integration": "vitest run tests/integration/",
|
|
50
|
+
"test:unit": "vitest run tests/unit/ tests/plugins/unit/",
|
|
51
|
+
"test:integration": "vitest run tests/integration/ tests/plugins/integration/",
|
|
44
52
|
"test:security": "vitest run tests/security/",
|
|
45
53
|
"test:orm": "vitest run tests/**/orm* tests/**/database*",
|
|
46
54
|
"test:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js",
|
|
47
55
|
"test:admin": "vitest run tests/**/admin*",
|
|
48
|
-
"test:services": "vitest run tests/**/service*",
|
|
49
|
-
"test:plugins": "vitest run tests
|
|
56
|
+
"test:services": "vitest run tests/unit/services/ tests/**/service*",
|
|
57
|
+
"test:plugins": "vitest run tests/plugins/",
|
|
50
58
|
"test:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template*",
|
|
51
59
|
"test:branches": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js",
|
|
52
|
-
"test:fast": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js",
|
|
60
|
+
"test:fast": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js tests/unit/auth.test.js tests/unit/services/services.test.js tests/unit/server.test.js",
|
|
53
61
|
"test:watch": "vitest",
|
|
54
62
|
"test:coverage": "vitest run --coverage",
|
|
55
63
|
"test:coverage:quick": "vitest run --coverage",
|
|
@@ -129,7 +137,7 @@
|
|
|
129
137
|
"multer": "^2.4.0",
|
|
130
138
|
"nodemailer": "^10.0.10",
|
|
131
139
|
"nunjucks": "^3.2.4",
|
|
132
|
-
"sanitize-html": "^2.
|
|
140
|
+
"sanitize-html": "^2.18.1",
|
|
133
141
|
"sharp": "^0.35.5",
|
|
134
142
|
"swup": "^4.8.3",
|
|
135
143
|
"yaml": "^2.8.3",
|
|
@@ -194,7 +202,9 @@
|
|
|
194
202
|
"undici": "^6.28.1",
|
|
195
203
|
"uuid": "^11.1.1",
|
|
196
204
|
"js-yaml": "^5.4.2",
|
|
197
|
-
"qs": "^6.16.0"
|
|
205
|
+
"qs": "^6.16.0",
|
|
206
|
+
"sanitize-html": "^2.18.1",
|
|
207
|
+
"vite": "^8.3.5"
|
|
198
208
|
},
|
|
199
209
|
"allowScripts": {
|
|
200
210
|
"better-sqlite3@11.10.0": true
|
|
@@ -518,7 +518,7 @@ function parseFilterQuery(queryString) {
|
|
|
518
518
|
if (!queryString) return filters;
|
|
519
519
|
|
|
520
520
|
const params = new URLSearchParams(queryString);
|
|
521
|
-
const filterParams =
|
|
521
|
+
const filterParams = Object.create(null);
|
|
522
522
|
|
|
523
523
|
// Group filter parameters using simple string parsing (no regex needed)
|
|
524
524
|
for (const [key, value] of params.entries()) {
|
|
@@ -532,7 +532,10 @@ function parseFilterQuery(queryString) {
|
|
|
532
532
|
const col = key.substring(7, firstClose);
|
|
533
533
|
const prop = key.substring(secondOpen + 1, secondClose);
|
|
534
534
|
|
|
535
|
-
if (
|
|
535
|
+
if (col === '__proto__' || col === 'constructor' || col === 'prototype') continue;
|
|
536
|
+
if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') continue;
|
|
537
|
+
|
|
538
|
+
if (!filterParams[col]) filterParams[col] = Object.create(null);
|
|
536
539
|
if (prop === 'value' && filterParams[col].value) {
|
|
537
540
|
// Multiple values for 'in' operator
|
|
538
541
|
if (!Array.isArray(filterParams[col].value)) {
|
|
@@ -324,6 +324,7 @@ function adminPanelPlugin(options = {}) {
|
|
|
324
324
|
db,
|
|
325
325
|
auth,
|
|
326
326
|
config: userMgmtConfig,
|
|
327
|
+
hashPassword: (password, rounds) => bcrypt.hash(password, process.env.NODE_ENV === 'test' ? 4 : (rounds || 10)),
|
|
327
328
|
});
|
|
328
329
|
}
|
|
329
330
|
|
|
@@ -570,24 +571,36 @@ function adminPanelPlugin(options = {}) {
|
|
|
570
571
|
// Static Vendor Assets (Offline Zero-CDN)
|
|
571
572
|
// ==========================================
|
|
572
573
|
const vendorDir = path.join(__dirname, 'vendor');
|
|
574
|
+
const vendorAssetCache = new Map();
|
|
573
575
|
ctx.addRoute('get', `${adminPath}/vendor/:file`, (req, res) => {
|
|
574
576
|
const fileName = path.basename(req.params.file);
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
577
|
+
let cached = vendorAssetCache.get(fileName);
|
|
578
|
+
if (cached === undefined) {
|
|
579
|
+
const filePath = path.join(vendorDir, fileName);
|
|
580
|
+
if (!fs.existsSync(filePath)) {
|
|
581
|
+
vendorAssetCache.set(fileName, null);
|
|
582
|
+
return res.status(404).send('Vendor asset not found');
|
|
583
|
+
}
|
|
584
|
+
let contentType = 'application/octet-stream';
|
|
585
|
+
if (fileName.endsWith('.js')) {
|
|
586
|
+
contentType = 'application/javascript';
|
|
587
|
+
} else if (fileName.endsWith('.css')) {
|
|
588
|
+
contentType = 'text/css';
|
|
589
|
+
}
|
|
590
|
+
cached = {
|
|
591
|
+
buffer: fs.readFileSync(filePath),
|
|
592
|
+
contentType,
|
|
593
|
+
};
|
|
594
|
+
vendorAssetCache.set(fileName, cached);
|
|
579
595
|
}
|
|
580
596
|
|
|
581
|
-
if (
|
|
582
|
-
res.
|
|
583
|
-
} else if (fileName.endsWith('.css')) {
|
|
584
|
-
res.type('text/css');
|
|
585
|
-
} else {
|
|
586
|
-
res.type('application/octet-stream');
|
|
597
|
+
if (!cached) {
|
|
598
|
+
return res.status(404).send('Vendor asset not found');
|
|
587
599
|
}
|
|
588
600
|
|
|
601
|
+
res.type(cached.contentType);
|
|
589
602
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
590
|
-
res.
|
|
603
|
+
res.send(cached.buffer);
|
|
591
604
|
});
|
|
592
605
|
|
|
593
606
|
// ==========================================
|
|
@@ -50,7 +50,7 @@ function resolveUserManagementModelName(config = {}) {
|
|
|
50
50
|
* @param {Object} [options.config] - User management config
|
|
51
51
|
*/
|
|
52
52
|
function registerUserManagement(options) {
|
|
53
|
-
const { registry, db, auth, config = {} } = options;
|
|
53
|
+
const { registry, db, auth, config = {}, hashPassword: customHashPassword } = options;
|
|
54
54
|
|
|
55
55
|
const {
|
|
56
56
|
fields = {},
|
|
@@ -230,7 +230,8 @@ function registerUserManagement(options) {
|
|
|
230
230
|
const repo = db.getRepository(targetModel);
|
|
231
231
|
// Generate random password
|
|
232
232
|
const newPassword = generateRandomPassword(12);
|
|
233
|
-
const
|
|
233
|
+
const hashFn = customHashPassword || ((p) => hash(p, process.env.NODE_ENV === 'test' ? 4 : 10));
|
|
234
|
+
const hashedPassword = await hashFn(newPassword);
|
|
234
235
|
await repo.update(record.id, { [fieldMap.password]: hashedPassword });
|
|
235
236
|
return { message: 'Password reset', newPassword }; // Return new password to show in UI
|
|
236
237
|
},
|
package/plugins/csrf/index.js
CHANGED
|
@@ -171,13 +171,14 @@ function csrfPlugin(options = {}) {
|
|
|
171
171
|
if (activeMethods.includes(method) && !isIgnored(req, activeIgnorePaths)) {
|
|
172
172
|
const body = req.body && typeof req.body === 'object' ? req.body : EMPTY_OBJECT;
|
|
173
173
|
const query = req.query && typeof req.query === 'object' ? req.query : EMPTY_OBJECT;
|
|
174
|
-
const
|
|
174
|
+
const rawSubmitted = (
|
|
175
175
|
body._csrf ||
|
|
176
176
|
query._csrf ||
|
|
177
177
|
req.headers['x-csrf-token'] ||
|
|
178
178
|
req.headers['xsrf-token'] ||
|
|
179
179
|
req.headers['x-xsrf-token']
|
|
180
180
|
);
|
|
181
|
+
const submitted = typeof rawSubmitted === 'string' ? rawSubmitted : null;
|
|
181
182
|
|
|
182
183
|
if (!submitted || !safeCompare(submitted, csrfTokenValue)) {
|
|
183
184
|
res.status(errorStatus);
|