@mantiq/core 0.3.0 → 0.4.0-rc.2

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.3.0",
3
+ "version": "0.4.0-rc.2",
4
4
  "description": "Service container, router, middleware, HTTP kernel, config, and exception handler",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@ export class Application extends ContainerImpl {
30
30
  private constructor(private readonly basePath: string = process.cwd()) {
31
31
  super()
32
32
  // Register the application itself so it can be resolved from the container
33
- this.instance(Application, this)
33
+ this.instance(Application as any, this)
34
34
  }
35
35
 
36
36
  // ── Singleton access ──────────────────────────────────────────────────────
@@ -133,6 +133,61 @@ export class Application extends ContainerImpl {
133
133
  return this.basePath_(path ? `storage/${path}` : 'storage')
134
134
  }
135
135
 
136
+ // ── Package provider discovery ──────────────────────────────────────────
137
+
138
+ /**
139
+ * Discover service providers from installed @mantiq/* packages.
140
+ * Each package declares its provider in package.json:
141
+ * { "mantiq": { "provider": "AuthServiceProvider" } }
142
+ *
143
+ * Install a package → provider auto-discovered. Uninstall → gone.
144
+ */
145
+ async discoverPackageProviders(): Promise<Constructor<ServiceProvider>[]> {
146
+ const providers: Constructor<ServiceProvider>[] = []
147
+ const nodeModulesDir = this.basePath_('node_modules/@mantiq')
148
+
149
+ try {
150
+ const glob = new Bun.Glob('*/package.json')
151
+ for await (const file of glob.scan({ cwd: nodeModulesDir, absolute: false })) {
152
+ try {
153
+ const pkgJson = JSON.parse(
154
+ await Bun.file(`${nodeModulesDir}/${file}`).text()
155
+ )
156
+ const providerName = pkgJson?.mantiq?.provider
157
+ if (!providerName) continue
158
+
159
+ const pkgName = pkgJson.name as string
160
+ const mod = await import(pkgName)
161
+ if (mod[providerName] && typeof mod[providerName] === 'function') {
162
+ providers.push(mod[providerName])
163
+ }
164
+ } catch {
165
+ // Skip packages that can't be loaded
166
+ }
167
+ }
168
+ } catch {
169
+ // node_modules/@mantiq doesn't exist
170
+ }
171
+
172
+ return providers
173
+ }
174
+
175
+ /**
176
+ * One-call bootstrap: discover package providers, register, boot.
177
+ * Equivalent to:
178
+ * const providers = await app.discoverPackageProviders()
179
+ * await app.registerProviders([CoreServiceProvider, ...providers, ...userProviders])
180
+ * await app.bootProviders()
181
+ */
182
+ async bootstrap(
183
+ coreProviders: Constructor<ServiceProvider>[] = [],
184
+ userProviders: Constructor<ServiceProvider>[] = [],
185
+ ): Promise<void> {
186
+ const packageProviders = await this.discoverPackageProviders()
187
+ await this.registerProviders([...coreProviders, ...packageProviders, ...userProviders])
188
+ await this.bootProviders()
189
+ }
190
+
136
191
  // ── Provider lifecycle ────────────────────────────────────────────────────
137
192
 
138
193
  /**
@@ -191,7 +246,7 @@ export class Application extends ContainerImpl {
191
246
  * Override make() to handle deferred provider loading.
192
247
  * If a binding isn't found in the container, check deferred providers.
193
248
  */
194
- make<T>(abstract: Bindable<T>): T {
249
+ override make<T>(abstract: Bindable<T>): T {
195
250
  try {
196
251
  return super.make(abstract)
197
252
  } catch (err) {
@@ -19,14 +19,14 @@ export interface RouteMatch {
19
19
  action: RouteAction
20
20
  params: Record<string, any>
21
21
  middleware: string[]
22
- routeName?: string
22
+ routeName?: string | undefined
23
23
  }
24
24
 
25
25
  export interface RouteDefinition {
26
26
  method: HttpMethod | HttpMethod[]
27
27
  path: string
28
28
  action: RouteAction
29
- name?: string
29
+ name?: string | undefined
30
30
  middleware: string[]
31
31
  wheres: Record<string, RegExp>
32
32
  }
@@ -0,0 +1,200 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
2
+ import { dirname, join, relative } from 'node:path'
3
+
4
+ /**
5
+ * Auto-discovers application classes by scanning conventional directories.
6
+ *
7
+ * In development: scans the filesystem and rebuilds the manifest.
8
+ * In production: reads a cached manifest from bootstrap/manifest.json.
9
+ *
10
+ * Usage:
11
+ * const discoverer = new Discoverer(app.basePath)
12
+ * const manifest = await discoverer.build() // scan + cache
13
+ * const manifest = discoverer.cached() // read cache only
14
+ */
15
+
16
+ export interface DiscoveryManifest {
17
+ providers: string[]
18
+ commands: string[]
19
+ routes: string[]
20
+ models: string[]
21
+ policies: string[]
22
+ middleware: string[]
23
+ observers: string[]
24
+ listeners: string[]
25
+ jobs: string[]
26
+ timestamp: number
27
+ }
28
+
29
+ const EMPTY_MANIFEST: DiscoveryManifest = {
30
+ providers: [],
31
+ commands: [],
32
+ routes: [],
33
+ models: [],
34
+ policies: [],
35
+ middleware: [],
36
+ observers: [],
37
+ listeners: [],
38
+ jobs: [],
39
+ timestamp: 0,
40
+ }
41
+
42
+ /** Directories to scan, relative to basePath. */
43
+ const DISCOVERY_MAP: Array<{ key: keyof DiscoveryManifest; dir: string; pattern: string }> = [
44
+ { key: 'providers', dir: 'app/Providers', pattern: '*ServiceProvider.ts' },
45
+ { key: 'commands', dir: 'app/Console/Commands', pattern: '*Command.ts' },
46
+ { key: 'routes', dir: 'routes', pattern: '*.ts' },
47
+ { key: 'models', dir: 'app/Models', pattern: '*.ts' },
48
+ { key: 'policies', dir: 'app/Policies', pattern: '*Policy.ts' },
49
+ { key: 'middleware', dir: 'app/Http/Middleware', pattern: '*.ts' },
50
+ { key: 'observers', dir: 'app/Observers', pattern: '*Observer.ts' },
51
+ { key: 'listeners', dir: 'app/Listeners', pattern: '*Listener.ts' },
52
+ { key: 'jobs', dir: 'app/Jobs', pattern: '*.ts' },
53
+ ]
54
+
55
+ export class Discoverer {
56
+ private manifestPath: string
57
+
58
+ constructor(private basePath: string) {
59
+ this.manifestPath = join(basePath, 'bootstrap', 'manifest.json')
60
+ }
61
+
62
+ /**
63
+ * Scan all directories and build a fresh manifest.
64
+ * Writes to bootstrap/manifest.json for caching.
65
+ */
66
+ async build(): Promise<DiscoveryManifest> {
67
+ const manifest: DiscoveryManifest = { ...EMPTY_MANIFEST, timestamp: Date.now() }
68
+
69
+ for (const { key, dir, pattern } of DISCOVERY_MAP) {
70
+ if (key === 'timestamp') continue
71
+ const fullDir = join(this.basePath, dir)
72
+ const files = await this.scanDirectory(fullDir, pattern)
73
+ ;(manifest[key] as string[]) = files.map((f) => join(dir, f))
74
+ }
75
+
76
+ // Write cache
77
+ this.writeManifest(manifest)
78
+ return manifest
79
+ }
80
+
81
+ /**
82
+ * Read the cached manifest. Returns null if no cache exists.
83
+ */
84
+ cached(): DiscoveryManifest | null {
85
+ if (!existsSync(this.manifestPath)) return null
86
+ try {
87
+ const raw = readFileSync(this.manifestPath, 'utf-8')
88
+ return JSON.parse(raw) as DiscoveryManifest
89
+ } catch {
90
+ return null
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get the manifest — cached in production, fresh in development.
96
+ */
97
+ async resolve(isDev = true): Promise<DiscoveryManifest> {
98
+ if (!isDev) {
99
+ const cached = this.cached()
100
+ if (cached) return cached
101
+ }
102
+ return this.build()
103
+ }
104
+
105
+ /**
106
+ * Load and instantiate all discovered service providers.
107
+ * Returns provider instances ready for registration.
108
+ */
109
+ async loadProviders(manifest: DiscoveryManifest): Promise<any[]> {
110
+ const providers: any[] = []
111
+ for (const file of manifest.providers) {
112
+ const fullPath = join(this.basePath, file)
113
+ try {
114
+ const mod = await import(fullPath)
115
+ const ProviderClass = this.findExport(mod, (v) =>
116
+ typeof v === 'function' && v.prototype?.register && v.prototype?.boot
117
+ )
118
+ if (ProviderClass) providers.push(ProviderClass)
119
+ } catch {
120
+ // Skip unloadable providers
121
+ }
122
+ }
123
+ return providers
124
+ }
125
+
126
+ /**
127
+ * Load and register all discovered route files.
128
+ * Each route file should export a default function: (router) => void
129
+ */
130
+ async loadRoutes(manifest: DiscoveryManifest, router: any): Promise<void> {
131
+ for (const file of manifest.routes) {
132
+ const fullPath = join(this.basePath, file)
133
+ try {
134
+ const mod = await import(fullPath)
135
+ if (typeof mod.default === 'function') {
136
+ mod.default(router)
137
+ }
138
+ } catch {
139
+ // Skip unloadable routes
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Load all discovered command classes for CLI kernel.
146
+ */
147
+ async loadCommands(manifest: DiscoveryManifest): Promise<any[]> {
148
+ const commands: any[] = []
149
+ for (const file of manifest.commands) {
150
+ const fullPath = join(this.basePath, file)
151
+ try {
152
+ const mod = await import(fullPath)
153
+ for (const exported of Object.values(mod)) {
154
+ if (typeof exported !== 'function') continue
155
+ try {
156
+ const instance = new (exported as any)()
157
+ if (instance.name && typeof instance.handle === 'function') {
158
+ commands.push(instance)
159
+ }
160
+ } catch {
161
+ // Not a command
162
+ }
163
+ }
164
+ } catch {
165
+ // Skip
166
+ }
167
+ }
168
+ return commands
169
+ }
170
+
171
+ // ── Internal ──────────────────────────────────────────────────────────────
172
+
173
+ private async scanDirectory(dir: string, pattern: string): Promise<string[]> {
174
+ const files: string[] = []
175
+ try {
176
+ const glob = new Bun.Glob(pattern)
177
+ for await (const file of glob.scan({ cwd: dir, absolute: false })) {
178
+ // Skip dotfiles and test files
179
+ if (file.startsWith('.') || file.includes('.test.') || file.includes('.spec.')) continue
180
+ files.push(file)
181
+ }
182
+ } catch {
183
+ // Directory doesn't exist
184
+ }
185
+ return files.sort()
186
+ }
187
+
188
+ private writeManifest(manifest: DiscoveryManifest): void {
189
+ const dir = dirname(this.manifestPath)
190
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
191
+ writeFileSync(this.manifestPath, JSON.stringify(manifest, null, 2))
192
+ }
193
+
194
+ private findExport(mod: any, predicate: (v: any) => boolean): any {
195
+ for (const exported of Object.values(mod)) {
196
+ if (predicate(exported)) return exported
197
+ }
198
+ return null
199
+ }
200
+ }
@@ -2,6 +2,7 @@ import type { Container, Constructor } from '../contracts/Container.ts'
2
2
  import type { ExceptionHandler } from '../contracts/ExceptionHandler.ts'
3
3
  import type { Middleware } from '../contracts/Middleware.ts'
4
4
  import type { Router, RouteMatch } from '../contracts/Router.ts'
5
+ import type { MantiqRequest as MantiqRequestContract } from '../contracts/Request.ts'
5
6
  import { MantiqRequest } from './Request.ts'
6
7
  import { MantiqResponse } from './Response.ts'
7
8
  import { Pipeline } from '../middleware/Pipeline.ts'
@@ -90,7 +91,7 @@ export class HttpKernel {
90
91
  /**
91
92
  * Main entry point. Passed to Bun.serve() as the fetch handler.
92
93
  */
93
- async handle(bunRequest: Request, server: Server): Promise<Response> {
94
+ async handle(bunRequest: Request, server: Bun.Server<any>): Promise<Response> {
94
95
  // WebSocket upgrade
95
96
  if (bunRequest.headers.get('upgrade')?.toLowerCase() === 'websocket') {
96
97
  return this.wsKernel.handleUpgrade(bunRequest, server)
@@ -145,7 +146,7 @@ export class HttpKernel {
145
146
  port,
146
147
  hostname,
147
148
  fetch: (req, server) => this.handle(req, server),
148
- websocket: this.wsKernel.getBunHandlers(),
149
+ websocket: this.wsKernel.getBunHandlers() as Bun.WebSocketHandler<any>,
149
150
  })
150
151
 
151
152
  const display = hostname === '0.0.0.0' ? 'localhost' : hostname
@@ -172,7 +173,7 @@ export class HttpKernel {
172
173
  * Call the route action (controller method or closure).
173
174
  * Converts the return value to a Response.
174
175
  */
175
- private async callAction(match: RouteMatch, request: MantiqRequest): Promise<Response> {
176
+ private async callAction(match: RouteMatch, request: MantiqRequestContract): Promise<Response> {
176
177
  const action = match.action
177
178
 
178
179
  let result: any
@@ -228,7 +228,7 @@ export class MantiqRequest implements MantiqRequestContract {
228
228
 
229
229
  try {
230
230
  if (contentType.includes('application/json')) {
231
- this.parsedBody = await this.bunRequest.clone().json()
231
+ this.parsedBody = await this.bunRequest.clone().json() as Record<string, any>
232
232
  } else if (contentType.includes('application/x-www-form-urlencoded')) {
233
233
  const text = await this.bunRequest.clone().text()
234
234
  this.parsedBody = Object.fromEntries(new URLSearchParams(text).entries())
package/src/index.ts CHANGED
@@ -55,10 +55,12 @@ export { EncryptCookies } from './middleware/EncryptCookies.ts'
55
55
  export { VerifyCsrfToken } from './middleware/VerifyCsrfToken.ts'
56
56
  export { RateLimiter, MemoryStore } from './rateLimit/RateLimiter.ts'
57
57
  export type { RateLimitConfig, RateLimitStore, LimiterResolver } from './rateLimit/RateLimiter.ts'
58
- export { ThrottleRequests } from './rateLimit/ThrottleRequests.ts'
58
+ export { ThrottleRequests, getDefaultRateLimiter, setDefaultRateLimiter } from './rateLimit/ThrottleRequests.ts'
59
59
  export { WebSocketKernel } from './websocket/WebSocketKernel.ts'
60
60
  export { DefaultExceptionHandler } from './exceptions/Handler.ts'
61
61
  export { CoreServiceProvider } from './providers/CoreServiceProvider.ts'
62
+ export { Discoverer } from './discovery/Discoverer.ts'
63
+ export type { DiscoveryManifest } from './discovery/Discoverer.ts'
62
64
 
63
65
  // ── Encryption ────────────────────────────────────────────────────────────────
64
66
  export { AesEncrypter } from './encryption/Encrypter.ts'
@@ -48,7 +48,7 @@ export interface MacroableInstance {
48
48
  */
49
49
  export function Macroable<T extends Constructor>(Base: T): T & MacroableStatic {
50
50
  class MacroableClass extends (Base as Constructor) {
51
- private static _macros = new Map<string, Function>()
51
+ static _macros = new Map<string, Function>()
52
52
 
53
53
  static macro(name: string, fn: Function): void {
54
54
  this._ensureOwnMacros()
@@ -9,6 +9,7 @@ import { TrimStringsMiddleware } from '../middleware/TrimStrings.ts'
9
9
  import { StartSession } from '../middleware/StartSession.ts'
10
10
  import { EncryptCookies } from '../middleware/EncryptCookies.ts'
11
11
  import { VerifyCsrfToken } from '../middleware/VerifyCsrfToken.ts'
12
+ import { ThrottleRequests } from '../rateLimit/ThrottleRequests.ts'
12
13
  import { ROUTER } from '../helpers/route.ts'
13
14
  import { ENCRYPTER } from '../helpers/encrypt.ts'
14
15
  import { AesEncrypter } from '../encryption/Encrypter.ts'
@@ -77,6 +78,9 @@ export class CoreServiceProvider extends ServiceProvider {
77
78
  this.app.bind(EncryptCookies, (c) => new EncryptCookies(c.make<AesEncrypter>(ENCRYPTER)))
78
79
  this.app.bind(VerifyCsrfToken, (c) => new VerifyCsrfToken(c.make<AesEncrypter>(ENCRYPTER)))
79
80
 
81
+ // Rate limiting — zero-config, uses shared in-memory store
82
+ this.app.singleton(ThrottleRequests, () => new ThrottleRequests())
83
+
80
84
  // HTTP kernel — singleton, depends on Router + ExceptionHandler + WsKernel
81
85
  this.app.singleton(HttpKernel, (c) => {
82
86
  const router = c.make(RouterImpl)
@@ -93,5 +97,14 @@ export class CoreServiceProvider extends ServiceProvider {
93
97
  const encrypter = await AesEncrypter.fromAppKey(appKey)
94
98
  this.app.instance(ENCRYPTER, encrypter)
95
99
  }
100
+
101
+ // ── Auto-register middleware aliases on HttpKernel ─────────────────────
102
+ const kernel = this.app.make(HttpKernel)
103
+ kernel.registerMiddleware('throttle', ThrottleRequests)
104
+ kernel.registerMiddleware('cors', CorsMiddleware)
105
+ kernel.registerMiddleware('trim', TrimStringsMiddleware)
106
+ kernel.registerMiddleware('encrypt.cookies', EncryptCookies)
107
+ kernel.registerMiddleware('session', StartSession)
108
+ kernel.registerMiddleware('csrf', VerifyCsrfToken)
96
109
  }
97
110
  }
@@ -1,6 +1,18 @@
1
1
  import type { MantiqRequest } from '../contracts/Request.ts'
2
+ import type { Middleware } from '../contracts/Middleware.ts'
2
3
  import { HttpError } from '../errors/HttpError.ts'
3
- import type { RateLimiter, RateLimitConfig } from './RateLimiter.ts'
4
+ import { RateLimiter } from './RateLimiter.ts'
5
+ import type { RateLimitConfig } from './RateLimiter.ts'
6
+
7
+ /** Shared default RateLimiter instance. */
8
+ let _defaultLimiter: RateLimiter | null = null
9
+ export function getDefaultRateLimiter(): RateLimiter {
10
+ if (!_defaultLimiter) _defaultLimiter = new RateLimiter()
11
+ return _defaultLimiter
12
+ }
13
+ export function setDefaultRateLimiter(limiter: RateLimiter): void {
14
+ _defaultLimiter = limiter
15
+ }
4
16
 
5
17
  /**
6
18
  * Middleware that throttles requests using the RateLimiter.
@@ -17,12 +29,19 @@ import type { RateLimiter, RateLimitConfig } from './RateLimiter.ts'
17
29
  * X-RateLimit-Remaining: 45
18
30
  * Retry-After: 30 (only when rate limited)
19
31
  */
20
- export class ThrottleRequests {
32
+ export class ThrottleRequests implements Middleware {
21
33
  private params: string[] = []
34
+ private rateLimiter: RateLimiter = getDefaultRateLimiter()
22
35
 
23
- constructor(private readonly rateLimiter: RateLimiter) {}
36
+ constructor() {}
37
+
38
+ /** Use a custom RateLimiter instead of the default shared instance. */
39
+ useRateLimiter(limiter: RateLimiter): this {
40
+ this.rateLimiter = limiter
41
+ return this
42
+ }
24
43
 
25
- setParameters(...params: string[]): void {
44
+ setParameters(params: string[]): void {
26
45
  this.params = params
27
46
  }
28
47
 
package/src/types.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Optional peer dependencies
2
+ declare module 'ioredis' { const x: any; export = x; export default x; }
3
+ declare module 'memjs' { const x: any; export = x; export default x; }
@@ -1,7 +1,7 @@
1
1
  import type { MantiqRequest } from '../contracts/Request.ts'
2
2
 
3
3
  export interface WebSocketContext {
4
- userId?: string | number
4
+ userId?: string | number | undefined
5
5
  channels: Set<string>
6
6
  metadata: Record<string, any>
7
7
  }