@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantiq/core",
3
- "version": "0.5.6",
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",
@@ -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
- // Skip packages that can't be loaded
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 === '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
  }
@@ -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
+ }
@@ -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'
@@ -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
 
@@ -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
- return await this.encrypter.decrypt(xsrfHeader)
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
- // 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',
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
  }