@mantiq/core 0.5.6 → 0.5.8
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/package.json +1 -1
- package/src/application/Application.ts +5 -3
- package/src/discovery/Discoverer.ts +26 -4
- package/src/helpers/dd.ts +52 -0
- package/src/helpers/paths.ts +71 -0
- package/src/http/Kernel.ts +2 -2
- package/src/index.ts +2 -0
- package/src/middleware/Cors.ts +10 -5
- package/src/middleware/EncryptCookies.ts +1 -1
- package/src/middleware/VerifyCsrfToken.ts +5 -2
- package/src/providers/CoreServiceProvider.ts +15 -5
package/package.json
CHANGED
|
@@ -188,12 +188,14 @@ export class Application extends ContainerImpl {
|
|
|
188
188
|
if (mod[providerName] && typeof mod[providerName] === 'function') {
|
|
189
189
|
providers.push(mod[providerName])
|
|
190
190
|
}
|
|
191
|
-
} catch {
|
|
192
|
-
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (process.env.APP_DEBUG === 'true') {
|
|
193
|
+
console.warn(`[Mantiq] Failed to load provider from ${file}:`, (e as Error)?.message ?? e)
|
|
194
|
+
}
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
} catch {
|
|
196
|
-
// node_modules/@mantiq doesn't exist
|
|
198
|
+
// node_modules/@mantiq doesn't exist — expected when no @mantiq packages installed
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
return providers
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
2
|
-
import { dirname, join, relative } from 'node:path'
|
|
2
|
+
import { basename, dirname, join, relative } from 'node:path'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Auto-discovers application classes by scanning conventional directories.
|
|
@@ -126,17 +126,39 @@ export class Discoverer {
|
|
|
126
126
|
/**
|
|
127
127
|
* Load and register all discovered route files.
|
|
128
128
|
* Each route file should export a default function: (router) => void
|
|
129
|
+
*
|
|
130
|
+
* Routes are auto-wrapped in middleware groups by filename convention:
|
|
131
|
+
* - web.ts → router.group({ middleware: ['web'] })
|
|
132
|
+
* - api.ts → router.group({ middleware: ['api'], prefix: '/api' })
|
|
133
|
+
* - Other files (console.ts, channels.ts) → loaded as-is
|
|
129
134
|
*/
|
|
130
135
|
async loadRoutes(manifest: DiscoveryManifest, router: any): Promise<void> {
|
|
136
|
+
const groupMap: Record<string, { prefix: string }> = {
|
|
137
|
+
web: { prefix: '' },
|
|
138
|
+
api: { prefix: '/api' },
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
for (const file of manifest.routes) {
|
|
132
142
|
const fullPath = join(this.basePath, file)
|
|
133
143
|
try {
|
|
134
144
|
const mod = await import(fullPath)
|
|
135
|
-
if (typeof mod.default
|
|
145
|
+
if (typeof mod.default !== 'function') continue
|
|
146
|
+
|
|
147
|
+
const stem = basename(file, '.ts')
|
|
148
|
+
const group = groupMap[stem]
|
|
149
|
+
|
|
150
|
+
if (group) {
|
|
151
|
+
router.group(
|
|
152
|
+
{ middleware: [stem], prefix: group.prefix },
|
|
153
|
+
(r: any) => mod.default(r),
|
|
154
|
+
)
|
|
155
|
+
} else {
|
|
136
156
|
mod.default(router)
|
|
137
157
|
}
|
|
138
|
-
} catch {
|
|
139
|
-
|
|
158
|
+
} catch (e) {
|
|
159
|
+
if (process.env.APP_DEBUG === 'true') {
|
|
160
|
+
console.warn(`[Mantiq] Failed to load route file ${file}:`, (e as Error)?.message ?? e)
|
|
161
|
+
}
|
|
140
162
|
}
|
|
141
163
|
}
|
|
142
164
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dump values to console and die (exit process).
|
|
3
|
+
* Laravel's dd() equivalent.
|
|
4
|
+
*
|
|
5
|
+
* @example dd(user)
|
|
6
|
+
* @example dd(user, request.query(), 'debug')
|
|
7
|
+
*/
|
|
8
|
+
export function dd(...args: any[]): never {
|
|
9
|
+
dump(...args)
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dump values to console with colorized, inspected output.
|
|
15
|
+
* Like dd() but doesn't exit.
|
|
16
|
+
*
|
|
17
|
+
* @example dump(user)
|
|
18
|
+
* @example dump(user, request.query())
|
|
19
|
+
*/
|
|
20
|
+
export function dump(...args: any[]): void {
|
|
21
|
+
for (const arg of args) {
|
|
22
|
+
if (arg === null) {
|
|
23
|
+
console.log('\x1b[2mnull\x1b[0m')
|
|
24
|
+
} else if (arg === undefined) {
|
|
25
|
+
console.log('\x1b[2mundefined\x1b[0m')
|
|
26
|
+
} else if (typeof arg === 'string') {
|
|
27
|
+
console.log(`\x1b[32m"${arg}"\x1b[0m`)
|
|
28
|
+
} else if (typeof arg === 'number' || typeof arg === 'bigint') {
|
|
29
|
+
console.log(`\x1b[33m${arg}\x1b[0m`)
|
|
30
|
+
} else if (typeof arg === 'boolean') {
|
|
31
|
+
console.log(`\x1b[35m${arg}\x1b[0m`)
|
|
32
|
+
} else if (arg instanceof Error) {
|
|
33
|
+
console.log(`\x1b[31m${arg.constructor.name}: ${arg.message}\x1b[0m`)
|
|
34
|
+
if (arg.stack) {
|
|
35
|
+
const frames = arg.stack.split('\n').slice(1, 6).map(l => ` \x1b[2m${l.trim()}\x1b[0m`)
|
|
36
|
+
console.log(frames.join('\n'))
|
|
37
|
+
}
|
|
38
|
+
} else if (Array.isArray(arg)) {
|
|
39
|
+
console.dir(arg, { depth: 4, colors: true })
|
|
40
|
+
} else if (typeof arg === 'object') {
|
|
41
|
+
// Model instances — use toObject() if available
|
|
42
|
+
if (typeof arg.toObject === 'function') {
|
|
43
|
+
console.log(`\x1b[36m${arg.constructor?.name ?? 'Object'}\x1b[0m`)
|
|
44
|
+
console.dir(arg.toObject(), { depth: 4, colors: true })
|
|
45
|
+
} else {
|
|
46
|
+
console.dir(arg, { depth: 4, colors: true })
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
console.log(arg)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Application } from '../application/Application.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get the application base path or a path relative to it.
|
|
5
|
+
*
|
|
6
|
+
* @example base_path() // '/Users/you/my-app'
|
|
7
|
+
* @example base_path('config') // '/Users/you/my-app/config'
|
|
8
|
+
*/
|
|
9
|
+
export function base_path(path: string = ''): string {
|
|
10
|
+
return Application.getInstance().basePath_(path)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the app directory path.
|
|
15
|
+
*
|
|
16
|
+
* @example app_path() // '/Users/you/my-app/app'
|
|
17
|
+
* @example app_path('Models') // '/Users/you/my-app/app/Models'
|
|
18
|
+
*/
|
|
19
|
+
export function app_path(path: string = ''): string {
|
|
20
|
+
return base_path(path ? `app/${path}` : 'app')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the config directory path.
|
|
25
|
+
*
|
|
26
|
+
* @example config_path() // '/Users/you/my-app/config'
|
|
27
|
+
* @example config_path('app.ts') // '/Users/you/my-app/config/app.ts'
|
|
28
|
+
*/
|
|
29
|
+
export function config_path(path: string = ''): string {
|
|
30
|
+
return Application.getInstance().configPath(path)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the database directory path.
|
|
35
|
+
*
|
|
36
|
+
* @example database_path() // '/Users/you/my-app/database'
|
|
37
|
+
* @example database_path('migrations') // '/Users/you/my-app/database/migrations'
|
|
38
|
+
*/
|
|
39
|
+
export function database_path(path: string = ''): string {
|
|
40
|
+
return base_path(path ? `database/${path}` : 'database')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the storage directory path.
|
|
45
|
+
*
|
|
46
|
+
* @example storage_path() // '/Users/you/my-app/storage'
|
|
47
|
+
* @example storage_path('logs') // '/Users/you/my-app/storage/logs'
|
|
48
|
+
*/
|
|
49
|
+
export function storage_path(path: string = ''): string {
|
|
50
|
+
return Application.getInstance().storagePath(path)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the public directory path.
|
|
55
|
+
*
|
|
56
|
+
* @example public_path() // '/Users/you/my-app/public'
|
|
57
|
+
* @example public_path('build') // '/Users/you/my-app/public/build'
|
|
58
|
+
*/
|
|
59
|
+
export function public_path(path: string = ''): string {
|
|
60
|
+
return base_path(path ? `public/${path}` : 'public')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the resources directory path.
|
|
65
|
+
*
|
|
66
|
+
* @example resource_path() // '/Users/you/my-app/resources'
|
|
67
|
+
* @example resource_path('views') // '/Users/you/my-app/resources/views'
|
|
68
|
+
*/
|
|
69
|
+
export function resource_path(path: string = ''): string {
|
|
70
|
+
return base_path(path ? `resources/${path}` : 'resources')
|
|
71
|
+
}
|
package/src/http/Kernel.ts
CHANGED
|
@@ -100,8 +100,8 @@ export class HttpKernel {
|
|
|
100
100
|
const request = MantiqRequest.fromBun(bunRequest)
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
|
-
// Combine prepend + global + append middleware
|
|
104
|
-
const allMiddleware = [...this.prependMiddleware, ...this.globalMiddleware, ...this.appendMiddleware]
|
|
103
|
+
// Combine prepend + global + append middleware (deduplicated, preserving order)
|
|
104
|
+
const allMiddleware = [...new Set([...this.prependMiddleware, ...this.globalMiddleware, ...this.appendMiddleware])]
|
|
105
105
|
const globalClasses = this.resolveMiddlewareList(allMiddleware)
|
|
106
106
|
|
|
107
107
|
const response = await new Pipeline(this.container)
|
package/src/index.ts
CHANGED
|
@@ -100,3 +100,5 @@ export { response, json, html, redirect, noContent, stream, download } from './h
|
|
|
100
100
|
export { hash, hashCheck } from './helpers/hash.ts'
|
|
101
101
|
export { cache } from './helpers/cache.ts'
|
|
102
102
|
export { session } from './helpers/session.ts'
|
|
103
|
+
export { dd, dump } from './helpers/dd.ts'
|
|
104
|
+
export { base_path, app_path, config_path, database_path, storage_path, public_path, resource_path } from './helpers/paths.ts'
|
package/src/middleware/Cors.ts
CHANGED
|
@@ -19,13 +19,18 @@ export class CorsMiddleware implements Middleware {
|
|
|
19
19
|
private config: CorsConfig
|
|
20
20
|
|
|
21
21
|
constructor(configRepo?: ConfigRepository) {
|
|
22
|
+
// Smart default: use APP_URL as origin with credentials when available
|
|
23
|
+
const appUrl = configRepo?.get('app.url', '') as string
|
|
24
|
+
const defaultOrigin = appUrl || '*'
|
|
25
|
+
const defaultCredentials = !!appUrl
|
|
26
|
+
|
|
22
27
|
this.config = {
|
|
23
|
-
origin: configRepo?.get('cors.origin',
|
|
28
|
+
origin: configRepo?.get('cors.origin', defaultOrigin) ?? defaultOrigin,
|
|
24
29
|
methods: configRepo?.get('cors.methods', ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
25
|
-
allowedHeaders: configRepo?.get('cors.allowedHeaders', ['Content-Type', 'Authorization', 'X-Requested-With']) ?? ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
26
|
-
exposedHeaders: configRepo?.get('cors.exposedHeaders', []) ?? [],
|
|
27
|
-
credentials: configRepo?.get('cors.credentials',
|
|
28
|
-
maxAge: configRepo?.get('cors.maxAge',
|
|
30
|
+
allowedHeaders: configRepo?.get('cors.allowedHeaders', ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-TOKEN', 'X-XSRF-TOKEN', 'X-Mantiq']) ?? ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-TOKEN', 'X-XSRF-TOKEN', 'X-Mantiq'],
|
|
31
|
+
exposedHeaders: configRepo?.get('cors.exposedHeaders', ['X-Heartbeat']) ?? ['X-Heartbeat'],
|
|
32
|
+
credentials: configRepo?.get('cors.credentials', defaultCredentials) ?? defaultCredentials,
|
|
33
|
+
maxAge: configRepo?.get('cors.maxAge', 7200) ?? 7200,
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -11,7 +11,7 @@ import { parseCookies, serializeCookie } from '../http/Cookie.ts'
|
|
|
11
11
|
*/
|
|
12
12
|
export class EncryptCookies implements Middleware {
|
|
13
13
|
/** Cookie names that should NOT be encrypted/decrypted. */
|
|
14
|
-
protected except: string[] = []
|
|
14
|
+
protected except: string[] = ['XSRF-TOKEN']
|
|
15
15
|
|
|
16
16
|
constructor(private readonly encrypter: AesEncrypter) {}
|
|
17
17
|
|
|
@@ -39,8 +39,9 @@ export class VerifyCsrfToken implements Middleware {
|
|
|
39
39
|
// Read-only methods don't need CSRF
|
|
40
40
|
if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return false
|
|
41
41
|
|
|
42
|
-
// Check exclusions
|
|
43
42
|
const path = request.path()
|
|
43
|
+
|
|
44
|
+
// Check user-defined exclusions
|
|
44
45
|
return !this.except.some((pattern) => {
|
|
45
46
|
if (pattern.endsWith('*')) {
|
|
46
47
|
return path.startsWith(pattern.slice(0, -1))
|
|
@@ -77,7 +78,9 @@ export class VerifyCsrfToken implements Middleware {
|
|
|
77
78
|
const xsrfHeader = request.header('x-xsrf-token')
|
|
78
79
|
if (xsrfHeader) {
|
|
79
80
|
try {
|
|
80
|
-
|
|
81
|
+
// Cookie values may be URL-encoded — decode before decrypting
|
|
82
|
+
const decoded = decodeURIComponent(xsrfHeader)
|
|
83
|
+
return await this.encrypter.decrypt(decoded)
|
|
81
84
|
} catch {
|
|
82
85
|
return null
|
|
83
86
|
}
|
|
@@ -107,11 +107,21 @@ export class CoreServiceProvider extends ServiceProvider {
|
|
|
107
107
|
kernel.registerMiddleware('session', StartSession)
|
|
108
108
|
kernel.registerMiddleware('csrf', VerifyCsrfToken)
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// Register middleware groups from config
|
|
111
111
|
const configRepo = this.app.make(ConfigRepository)
|
|
112
|
-
const
|
|
113
|
-
'cors', 'encrypt.cookies', 'session',
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const middlewareGroups = configRepo.get('app.middlewareGroups', {
|
|
113
|
+
web: ['cors', 'encrypt.cookies', 'session', 'csrf'],
|
|
114
|
+
api: ['cors', 'throttle'],
|
|
115
|
+
}) as Record<string, string[]>
|
|
116
|
+
|
|
117
|
+
for (const [name, middleware] of Object.entries(middlewareGroups)) {
|
|
118
|
+
kernel.registerMiddlewareGroup(name, middleware)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Legacy: if app.middleware is set, apply as global middleware (backward compat)
|
|
122
|
+
const globalMiddleware = configRepo.get('app.middleware', []) as string[]
|
|
123
|
+
if (globalMiddleware.length > 0) {
|
|
124
|
+
kernel.setGlobalMiddleware(globalMiddleware)
|
|
125
|
+
}
|
|
116
126
|
}
|
|
117
127
|
}
|