@mantiq/core 0.5.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantiq/core",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Service container, router, middleware, HTTP kernel, config, and exception handler",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 === 'function') {
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
- // Skip unloadable routes
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
  }
@@ -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', false) ?? false,
28
- maxAge: configRepo?.get('cors.maxAge', 0) ?? 0,
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
 
@@ -19,7 +19,7 @@ import { serializeCookie } from '../http/Cookie.ts'
19
19
  */
20
20
  export class VerifyCsrfToken implements Middleware {
21
21
  /** URIs that should be excluded from CSRF verification. */
22
- protected except: string[] = ['/api/*']
22
+ protected except: string[] = []
23
23
 
24
24
  constructor(private readonly encrypter: AesEncrypter) {}
25
25
 
@@ -41,9 +41,6 @@ export class VerifyCsrfToken implements Middleware {
41
41
 
42
42
  const path = request.path()
43
43
 
44
- // API routes are always excluded — token auth is inherently CSRF-safe
45
- if (path.startsWith('/api/') || path === '/api') return false
46
-
47
44
  // Check user-defined exclusions
48
45
  return !this.except.some((pattern) => {
49
46
  if (pattern.endsWith('*')) {
@@ -107,11 +107,21 @@ export class CoreServiceProvider extends ServiceProvider {
107
107
  kernel.registerMiddleware('session', StartSession)
108
108
  kernel.registerMiddleware('csrf', VerifyCsrfToken)
109
109
 
110
- // Set default global middleware stack (can be overridden in config/app.ts)
110
+ // Register middleware groups from config
111
111
  const configRepo = this.app.make(ConfigRepository)
112
- const globalMiddleware = configRepo.get('app.middleware', [
113
- 'cors', 'encrypt.cookies', 'session', 'csrf',
114
- ]) as string[]
115
- kernel.setGlobalMiddleware(globalMiddleware)
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
  }