@mantiq/core 0.0.1
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/README.md +19 -0
- package/package.json +65 -0
- package/src/application/Application.ts +241 -0
- package/src/cache/CacheManager.ts +180 -0
- package/src/cache/FileCacheStore.ts +113 -0
- package/src/cache/MemcachedCacheStore.ts +115 -0
- package/src/cache/MemoryCacheStore.ts +62 -0
- package/src/cache/NullCacheStore.ts +39 -0
- package/src/cache/RedisCacheStore.ts +125 -0
- package/src/cache/events.ts +52 -0
- package/src/config/ConfigRepository.ts +115 -0
- package/src/config/env.ts +26 -0
- package/src/container/Container.ts +198 -0
- package/src/container/ContextualBindingBuilder.ts +21 -0
- package/src/contracts/Cache.ts +49 -0
- package/src/contracts/Config.ts +24 -0
- package/src/contracts/Container.ts +68 -0
- package/src/contracts/DriverManager.ts +16 -0
- package/src/contracts/Encrypter.ts +32 -0
- package/src/contracts/EventDispatcher.ts +32 -0
- package/src/contracts/ExceptionHandler.ts +20 -0
- package/src/contracts/Hasher.ts +19 -0
- package/src/contracts/Middleware.ts +23 -0
- package/src/contracts/Request.ts +54 -0
- package/src/contracts/Response.ts +19 -0
- package/src/contracts/Router.ts +62 -0
- package/src/contracts/ServiceProvider.ts +31 -0
- package/src/contracts/Session.ts +47 -0
- package/src/encryption/Encrypter.ts +197 -0
- package/src/encryption/errors.ts +30 -0
- package/src/errors/ConfigKeyNotFoundError.ts +7 -0
- package/src/errors/ContainerResolutionError.ts +13 -0
- package/src/errors/ForbiddenError.ts +7 -0
- package/src/errors/HttpError.ts +16 -0
- package/src/errors/MantiqError.ts +16 -0
- package/src/errors/NotFoundError.ts +7 -0
- package/src/errors/TokenMismatchError.ts +10 -0
- package/src/errors/TooManyRequestsError.ts +10 -0
- package/src/errors/UnauthorizedError.ts +7 -0
- package/src/errors/ValidationError.ts +10 -0
- package/src/exceptions/DevErrorPage.ts +564 -0
- package/src/exceptions/Handler.ts +118 -0
- package/src/hashing/Argon2Hasher.ts +46 -0
- package/src/hashing/BcryptHasher.ts +36 -0
- package/src/hashing/HashManager.ts +80 -0
- package/src/helpers/abort.ts +46 -0
- package/src/helpers/app.ts +17 -0
- package/src/helpers/cache.ts +12 -0
- package/src/helpers/config.ts +15 -0
- package/src/helpers/encrypt.ts +22 -0
- package/src/helpers/env.ts +1 -0
- package/src/helpers/hash.ts +20 -0
- package/src/helpers/response.ts +69 -0
- package/src/helpers/route.ts +24 -0
- package/src/helpers/session.ts +11 -0
- package/src/http/Cookie.ts +26 -0
- package/src/http/Kernel.ts +252 -0
- package/src/http/Request.ts +249 -0
- package/src/http/Response.ts +112 -0
- package/src/http/UploadedFile.ts +56 -0
- package/src/index.ts +97 -0
- package/src/macroable/Macroable.ts +174 -0
- package/src/middleware/Cors.ts +91 -0
- package/src/middleware/EncryptCookies.ts +101 -0
- package/src/middleware/Pipeline.ts +66 -0
- package/src/middleware/StartSession.ts +90 -0
- package/src/middleware/TrimStrings.ts +32 -0
- package/src/middleware/VerifyCsrfToken.ts +130 -0
- package/src/providers/CoreServiceProvider.ts +97 -0
- package/src/routing/ResourceRegistrar.ts +64 -0
- package/src/routing/Route.ts +40 -0
- package/src/routing/RouteCollection.ts +50 -0
- package/src/routing/RouteMatcher.ts +92 -0
- package/src/routing/Router.ts +280 -0
- package/src/routing/events.ts +19 -0
- package/src/session/SessionManager.ts +75 -0
- package/src/session/Store.ts +192 -0
- package/src/session/handlers/CookieSessionHandler.ts +42 -0
- package/src/session/handlers/FileSessionHandler.ts +79 -0
- package/src/session/handlers/MemorySessionHandler.ts +35 -0
- package/src/websocket/WebSocketContext.ts +20 -0
- package/src/websocket/WebSocketKernel.ts +60 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ── Application ───────────────────────────────────────────────────────────────
|
|
2
|
+
export { Application } from './application/Application.ts'
|
|
3
|
+
|
|
4
|
+
// ── Macroable ────────────────────────────────────────────────────────────────
|
|
5
|
+
export { Macroable, applyMacros } from './macroable/Macroable.ts'
|
|
6
|
+
export type { MacroableStatic, MacroableInstance } from './macroable/Macroable.ts'
|
|
7
|
+
|
|
8
|
+
// ── Contracts (interfaces + base classes) ────────────────────────────────────
|
|
9
|
+
export type { Container, Bindable, Resolvable, Constructor, ContextualBindingBuilder } from './contracts/Container.ts'
|
|
10
|
+
export type { Config } from './contracts/Config.ts'
|
|
11
|
+
export type { Middleware, NextFunction } from './contracts/Middleware.ts'
|
|
12
|
+
export type { MantiqRequest } from './contracts/Request.ts'
|
|
13
|
+
export type { MantiqResponseBuilder, CookieOptions } from './contracts/Response.ts'
|
|
14
|
+
export type { Router, RouterRoute, RouteAction, RouteGroupOptions, RouteMatch, RouteDefinition, HttpMethod } from './contracts/Router.ts'
|
|
15
|
+
export type { ExceptionHandler } from './contracts/ExceptionHandler.ts'
|
|
16
|
+
export type { DriverManager } from './contracts/DriverManager.ts'
|
|
17
|
+
export type { Encrypter } from './contracts/Encrypter.ts'
|
|
18
|
+
export type { Hasher } from './contracts/Hasher.ts'
|
|
19
|
+
export type { CacheStore } from './contracts/Cache.ts'
|
|
20
|
+
export type { SessionHandler, SessionConfig } from './contracts/Session.ts'
|
|
21
|
+
export type { EventDispatcher, EventHandler } from './contracts/EventDispatcher.ts'
|
|
22
|
+
export { ServiceProvider } from './contracts/ServiceProvider.ts'
|
|
23
|
+
export { Event, Listener } from './contracts/EventDispatcher.ts'
|
|
24
|
+
export type { WebSocketHandler, WebSocketContext } from './websocket/WebSocketContext.ts'
|
|
25
|
+
|
|
26
|
+
// ── Errors ────────────────────────────────────────────────────────────────────
|
|
27
|
+
export { MantiqError } from './errors/MantiqError.ts'
|
|
28
|
+
export { HttpError } from './errors/HttpError.ts'
|
|
29
|
+
export { NotFoundError } from './errors/NotFoundError.ts'
|
|
30
|
+
export { UnauthorizedError } from './errors/UnauthorizedError.ts'
|
|
31
|
+
export { ForbiddenError } from './errors/ForbiddenError.ts'
|
|
32
|
+
export { ValidationError } from './errors/ValidationError.ts'
|
|
33
|
+
export { TooManyRequestsError } from './errors/TooManyRequestsError.ts'
|
|
34
|
+
export { ContainerResolutionError } from './errors/ContainerResolutionError.ts'
|
|
35
|
+
export { ConfigKeyNotFoundError } from './errors/ConfigKeyNotFoundError.ts'
|
|
36
|
+
export { TokenMismatchError } from './errors/TokenMismatchError.ts'
|
|
37
|
+
export { EncryptionError, DecryptionError, MissingAppKeyError } from './encryption/errors.ts'
|
|
38
|
+
|
|
39
|
+
// ── Implementations ───────────────────────────────────────────────────────────
|
|
40
|
+
export { ContainerImpl } from './container/Container.ts'
|
|
41
|
+
export { ConfigRepository } from './config/ConfigRepository.ts'
|
|
42
|
+
export { MantiqRequest as MantiqRequestImpl } from './http/Request.ts'
|
|
43
|
+
export { MantiqResponse, ResponseBuilder } from './http/Response.ts'
|
|
44
|
+
export { serializeCookie, parseCookies } from './http/Cookie.ts'
|
|
45
|
+
export { UploadedFile } from './http/UploadedFile.ts'
|
|
46
|
+
export { HttpKernel } from './http/Kernel.ts'
|
|
47
|
+
export { RouterImpl } from './routing/Router.ts'
|
|
48
|
+
export { Route } from './routing/Route.ts'
|
|
49
|
+
export { RouteMatched } from './routing/events.ts'
|
|
50
|
+
export { Pipeline } from './middleware/Pipeline.ts'
|
|
51
|
+
export { CorsMiddleware } from './middleware/Cors.ts'
|
|
52
|
+
export { TrimStringsMiddleware } from './middleware/TrimStrings.ts'
|
|
53
|
+
export { StartSession } from './middleware/StartSession.ts'
|
|
54
|
+
export { EncryptCookies } from './middleware/EncryptCookies.ts'
|
|
55
|
+
export { VerifyCsrfToken } from './middleware/VerifyCsrfToken.ts'
|
|
56
|
+
export { WebSocketKernel } from './websocket/WebSocketKernel.ts'
|
|
57
|
+
export { DefaultExceptionHandler } from './exceptions/Handler.ts'
|
|
58
|
+
export { CoreServiceProvider } from './providers/CoreServiceProvider.ts'
|
|
59
|
+
|
|
60
|
+
// ── Encryption ────────────────────────────────────────────────────────────────
|
|
61
|
+
export { AesEncrypter } from './encryption/Encrypter.ts'
|
|
62
|
+
|
|
63
|
+
// ── Hashing ───────────────────────────────────────────────────────────────────
|
|
64
|
+
export { HashManager } from './hashing/HashManager.ts'
|
|
65
|
+
export { BcryptHasher } from './hashing/BcryptHasher.ts'
|
|
66
|
+
export { Argon2Hasher } from './hashing/Argon2Hasher.ts'
|
|
67
|
+
|
|
68
|
+
// ── Cache ─────────────────────────────────────────────────────────────────────
|
|
69
|
+
export { CacheManager } from './cache/CacheManager.ts'
|
|
70
|
+
export type { CacheConfig } from './cache/CacheManager.ts'
|
|
71
|
+
export { MemoryCacheStore } from './cache/MemoryCacheStore.ts'
|
|
72
|
+
export { FileCacheStore } from './cache/FileCacheStore.ts'
|
|
73
|
+
export { RedisCacheStore } from './cache/RedisCacheStore.ts'
|
|
74
|
+
export type { RedisCacheConfig } from './cache/RedisCacheStore.ts'
|
|
75
|
+
export { MemcachedCacheStore } from './cache/MemcachedCacheStore.ts'
|
|
76
|
+
export type { MemcachedCacheConfig } from './cache/MemcachedCacheStore.ts'
|
|
77
|
+
export { NullCacheStore } from './cache/NullCacheStore.ts'
|
|
78
|
+
export { CacheHit, CacheMissed, KeyWritten, KeyForgotten } from './cache/events.ts'
|
|
79
|
+
|
|
80
|
+
// ── Session ───────────────────────────────────────────────────────────────────
|
|
81
|
+
export { SessionManager } from './session/SessionManager.ts'
|
|
82
|
+
export { SessionStore } from './session/Store.ts'
|
|
83
|
+
export { MemorySessionHandler } from './session/handlers/MemorySessionHandler.ts'
|
|
84
|
+
export { FileSessionHandler } from './session/handlers/FileSessionHandler.ts'
|
|
85
|
+
export { CookieSessionHandler } from './session/handlers/CookieSessionHandler.ts'
|
|
86
|
+
|
|
87
|
+
// ── Global helpers ────────────────────────────────────────────────────────────
|
|
88
|
+
export { config } from './helpers/config.ts'
|
|
89
|
+
export { env } from './helpers/env.ts'
|
|
90
|
+
export { app } from './helpers/app.ts'
|
|
91
|
+
export { route, ROUTER } from './helpers/route.ts'
|
|
92
|
+
export { abort } from './helpers/abort.ts'
|
|
93
|
+
export { encrypt, decrypt, ENCRYPTER } from './helpers/encrypt.ts'
|
|
94
|
+
export { response, json, html, redirect, noContent, stream, download } from './helpers/response.ts'
|
|
95
|
+
export { hash, hashCheck } from './helpers/hash.ts'
|
|
96
|
+
export { cache } from './helpers/cache.ts'
|
|
97
|
+
export { session } from './helpers/session.ts'
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macroable — allows dynamically adding methods to classes at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Laravel equivalent: `Illuminate\Support\Traits\Macroable`
|
|
5
|
+
*
|
|
6
|
+
* Two usage patterns:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Mixin function** — wrap a class to get full Proxy-based macro support:
|
|
9
|
+
* ```ts
|
|
10
|
+
* const MyClass = Macroable(BaseClass)
|
|
11
|
+
* MyClass.macro('custom', function() { return this.value })
|
|
12
|
+
* new MyClass().custom() // works transparently
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* 2. **Apply to existing class** — add macro support without changing class identity:
|
|
16
|
+
* ```ts
|
|
17
|
+
* applyMacros(QueryBuilder)
|
|
18
|
+
* QueryBuilder.macro('toCsv', function() { ... })
|
|
19
|
+
* // Call via __macro() or extend the prototype
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* For TypeScript type safety, users can augment the interface:
|
|
23
|
+
* declare module '@mantiq/database' {
|
|
24
|
+
* interface QueryBuilder { toCsv(): Promise<string> }
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type Constructor<T = object> = new (...args: any[]) => T
|
|
29
|
+
|
|
30
|
+
export interface MacroableStatic {
|
|
31
|
+
macro(name: string, fn: Function): void
|
|
32
|
+
mixin(mixins: Record<string, Function>, replace?: boolean): void
|
|
33
|
+
hasMacro(name: string): boolean
|
|
34
|
+
flushMacros(): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MacroableInstance {
|
|
38
|
+
__macro(name: string, ...args: any[]): any
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Mixin function that adds macro support to a class.
|
|
43
|
+
*
|
|
44
|
+
* Returns a Proxy-wrapped class where:
|
|
45
|
+
* - Static macro/mixin/hasMacro/flushMacros methods are available
|
|
46
|
+
* - Instance method calls fall through to registered macros
|
|
47
|
+
* - Each subclass inherits parent macros but gets its own macro registry
|
|
48
|
+
*/
|
|
49
|
+
export function Macroable<T extends Constructor>(Base: T): T & MacroableStatic {
|
|
50
|
+
class MacroableClass extends (Base as Constructor) {
|
|
51
|
+
private static _macros = new Map<string, Function>()
|
|
52
|
+
|
|
53
|
+
static macro(name: string, fn: Function): void {
|
|
54
|
+
this._ensureOwnMacros()
|
|
55
|
+
this._macros.set(name, fn)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static mixin(mixins: Record<string, Function>, replace = true): void {
|
|
59
|
+
for (const [name, fn] of Object.entries(mixins)) {
|
|
60
|
+
if (replace || !this.hasMacro(name)) {
|
|
61
|
+
this.macro(name, fn)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static hasMacro(name: string): boolean {
|
|
67
|
+
return this._macros.has(name)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static flushMacros(): void {
|
|
71
|
+
this._ensureOwnMacros()
|
|
72
|
+
this._macros.clear()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
__macro(name: string, ...args: any[]): any {
|
|
76
|
+
const fn = (this.constructor as typeof MacroableClass)._macros?.get(name)
|
|
77
|
+
if (!fn) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Method ${name} does not exist on ${this.constructor.name}. Did you forget to register it with ${this.constructor.name}.macro()?`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return fn.apply(this, args)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static _ensureOwnMacros(): void {
|
|
86
|
+
if (!Object.prototype.hasOwnProperty.call(this, '_macros')) {
|
|
87
|
+
this._macros = new Map(this._macros)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Proxy for transparent macro calls on instances
|
|
93
|
+
return new Proxy(MacroableClass, {
|
|
94
|
+
construct(target, args, newTarget) {
|
|
95
|
+
const instance = Reflect.construct(target, args, newTarget)
|
|
96
|
+
return new Proxy(instance, {
|
|
97
|
+
get(obj, prop, receiver) {
|
|
98
|
+
const value = Reflect.get(obj, prop, receiver)
|
|
99
|
+
if (value !== undefined) return value
|
|
100
|
+
|
|
101
|
+
if (typeof prop === 'string') {
|
|
102
|
+
const macroFn = (obj.constructor as typeof MacroableClass)._macros?.get(prop)
|
|
103
|
+
if (macroFn) {
|
|
104
|
+
return (...callArgs: any[]) => macroFn.apply(obj, callArgs)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return value
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
}) as unknown as T & MacroableStatic
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Apply macro support to an existing class without wrapping it in a Proxy.
|
|
117
|
+
*
|
|
118
|
+
* This is the non-destructive approach — it doesn't change the class identity,
|
|
119
|
+
* preserving `instanceof` checks and existing imports. Macros are called via
|
|
120
|
+
* `instance.__macro('name', ...args)`.
|
|
121
|
+
*
|
|
122
|
+
* Use this for framework classes (QueryBuilder, Collection, etc.) that are
|
|
123
|
+
* already widely exported.
|
|
124
|
+
*/
|
|
125
|
+
export function applyMacros<T extends Constructor>(Target: T): void {
|
|
126
|
+
const macros = new Map<string, Function>()
|
|
127
|
+
|
|
128
|
+
Object.defineProperties(Target, {
|
|
129
|
+
_macros: { value: macros, writable: true, configurable: true },
|
|
130
|
+
|
|
131
|
+
macro: {
|
|
132
|
+
value(name: string, fn: Function): void {
|
|
133
|
+
macros.set(name, fn)
|
|
134
|
+
},
|
|
135
|
+
configurable: true,
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
mixin: {
|
|
139
|
+
value(mixins: Record<string, Function>, replace = true): void {
|
|
140
|
+
for (const [name, fn] of Object.entries(mixins)) {
|
|
141
|
+
if (replace || !macros.has(name)) {
|
|
142
|
+
macros.set(name, fn)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
configurable: true,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
hasMacro: {
|
|
150
|
+
value(name: string): boolean {
|
|
151
|
+
return macros.has(name)
|
|
152
|
+
},
|
|
153
|
+
configurable: true,
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
flushMacros: {
|
|
157
|
+
value(): void {
|
|
158
|
+
macros.clear()
|
|
159
|
+
},
|
|
160
|
+
configurable: true,
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Add __macro to prototype
|
|
165
|
+
Target.prototype.__macro = function (name: string, ...args: any[]): any {
|
|
166
|
+
const fn = macros.get(name)
|
|
167
|
+
if (!fn) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Method ${name} does not exist on ${Target.name}. Did you forget to register it with ${Target.name}.macro()?`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return fn.apply(this, args)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Middleware, NextFunction } from '../contracts/Middleware.ts'
|
|
2
|
+
import type { MantiqRequest } from '../contracts/Request.ts'
|
|
3
|
+
import { ConfigRepository } from '../config/ConfigRepository.ts'
|
|
4
|
+
|
|
5
|
+
interface CorsConfig {
|
|
6
|
+
origin: string | string[] | '*'
|
|
7
|
+
methods: string[]
|
|
8
|
+
allowedHeaders: string[]
|
|
9
|
+
exposedHeaders: string[]
|
|
10
|
+
credentials: boolean
|
|
11
|
+
maxAge: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Cross-Origin Resource Sharing middleware.
|
|
16
|
+
* Configure via config/cors.ts.
|
|
17
|
+
*/
|
|
18
|
+
export class CorsMiddleware implements Middleware {
|
|
19
|
+
private config: CorsConfig
|
|
20
|
+
|
|
21
|
+
constructor(configRepo?: ConfigRepository) {
|
|
22
|
+
this.config = {
|
|
23
|
+
origin: configRepo?.get('cors.origin', '*') ?? '*',
|
|
24
|
+
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,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async handle(request: MantiqRequest, next: NextFunction): Promise<Response> {
|
|
33
|
+
const origin = request.header('origin') ?? ''
|
|
34
|
+
|
|
35
|
+
// Handle preflight
|
|
36
|
+
if (request.method() === 'OPTIONS') {
|
|
37
|
+
return this.buildPreflightResponse(origin)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = await next()
|
|
41
|
+
return this.addCorsHeaders(response, origin)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private buildPreflightResponse(origin: string): Response {
|
|
45
|
+
const headers: Record<string, string> = {
|
|
46
|
+
'Access-Control-Allow-Methods': this.config.methods.join(', '),
|
|
47
|
+
'Access-Control-Allow-Headers': this.config.allowedHeaders.join(', '),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.applyOriginHeader(headers, origin)
|
|
51
|
+
|
|
52
|
+
if (this.config.credentials) headers['Access-Control-Allow-Credentials'] = 'true'
|
|
53
|
+
if (this.config.maxAge > 0) headers['Access-Control-Max-Age'] = String(this.config.maxAge)
|
|
54
|
+
if (this.config.exposedHeaders.length > 0) {
|
|
55
|
+
headers['Access-Control-Expose-Headers'] = this.config.exposedHeaders.join(', ')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Response(null, { status: 204, headers })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private addCorsHeaders(response: Response, origin: string): Response {
|
|
62
|
+
const headers = new Headers(response.headers)
|
|
63
|
+
this.setOriginHeader(headers, origin)
|
|
64
|
+
if (this.config.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
65
|
+
if (this.config.exposedHeaders.length > 0) {
|
|
66
|
+
headers.set('Access-Control-Expose-Headers', this.config.exposedHeaders.join(', '))
|
|
67
|
+
}
|
|
68
|
+
return new Response(response.body, { status: response.status, headers })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private setOriginHeader(headers: Headers, requestOrigin: string): void {
|
|
72
|
+
const { origin } = this.config
|
|
73
|
+
if (origin === '*') {
|
|
74
|
+
headers.set('Access-Control-Allow-Origin', '*')
|
|
75
|
+
} else if (Array.isArray(origin)) {
|
|
76
|
+
if (origin.includes(requestOrigin)) {
|
|
77
|
+
headers.set('Access-Control-Allow-Origin', requestOrigin)
|
|
78
|
+
headers.set('Vary', 'Origin')
|
|
79
|
+
}
|
|
80
|
+
} else if (origin === requestOrigin) {
|
|
81
|
+
headers.set('Access-Control-Allow-Origin', requestOrigin)
|
|
82
|
+
headers.set('Vary', 'Origin')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private applyOriginHeader(headers: Record<string, string>, requestOrigin: string): void {
|
|
87
|
+
const tmp = new Headers()
|
|
88
|
+
this.setOriginHeader(tmp, requestOrigin)
|
|
89
|
+
tmp.forEach((v, k) => { headers[k] = v })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Middleware, NextFunction } from '../contracts/Middleware.ts'
|
|
2
|
+
import type { MantiqRequest } from '../contracts/Request.ts'
|
|
3
|
+
import { AesEncrypter } from '../encryption/Encrypter.ts'
|
|
4
|
+
import { parseCookies, serializeCookie } from '../http/Cookie.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Middleware that encrypts outgoing cookies and decrypts incoming cookies.
|
|
8
|
+
*
|
|
9
|
+
* Cookies listed in `except` are left unencrypted (e.g. XSRF-TOKEN must be
|
|
10
|
+
* readable by JavaScript for the double-submit pattern).
|
|
11
|
+
*/
|
|
12
|
+
export class EncryptCookies implements Middleware {
|
|
13
|
+
/** Cookie names that should NOT be encrypted/decrypted. */
|
|
14
|
+
protected except: string[] = []
|
|
15
|
+
|
|
16
|
+
constructor(private readonly encrypter: AesEncrypter) {}
|
|
17
|
+
|
|
18
|
+
async handle(request: MantiqRequest, next: NextFunction): Promise<Response> {
|
|
19
|
+
// Decrypt incoming cookies by replacing the raw Request with decrypted values
|
|
20
|
+
const decryptedRequest = await this.decryptRequest(request)
|
|
21
|
+
|
|
22
|
+
const response = await next()
|
|
23
|
+
|
|
24
|
+
return this.encryptResponse(response)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decrypt cookies on the incoming request.
|
|
29
|
+
* We patch the raw Request's cookie header with decrypted values.
|
|
30
|
+
*/
|
|
31
|
+
private async decryptRequest(request: MantiqRequest): Promise<MantiqRequest> {
|
|
32
|
+
const raw = request.raw()
|
|
33
|
+
const cookieHeader = raw.headers.get('cookie')
|
|
34
|
+
if (!cookieHeader) return request
|
|
35
|
+
|
|
36
|
+
const cookies = parseCookies(cookieHeader)
|
|
37
|
+
const decrypted: Record<string, string> = {}
|
|
38
|
+
|
|
39
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
40
|
+
if (this.isExcluded(name)) {
|
|
41
|
+
decrypted[name] = value
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
decrypted[name] = await this.encrypter.decrypt(value)
|
|
47
|
+
} catch {
|
|
48
|
+
// Can't decrypt — skip this cookie (expired key, tampered, etc.)
|
|
49
|
+
decrypted[name] = value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Inject the decrypted cookies so subsequent middleware/handlers see plain values
|
|
54
|
+
request.setCookies(decrypted)
|
|
55
|
+
return request
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Encrypt cookies on the outgoing response.
|
|
60
|
+
*/
|
|
61
|
+
private async encryptResponse(response: Response): Promise<Response> {
|
|
62
|
+
const headers = new Headers(response.headers)
|
|
63
|
+
const setCookies = headers.getSetCookie()
|
|
64
|
+
|
|
65
|
+
if (setCookies.length === 0) return response
|
|
66
|
+
|
|
67
|
+
// Remove existing Set-Cookie headers
|
|
68
|
+
headers.delete('Set-Cookie')
|
|
69
|
+
|
|
70
|
+
for (const setCookie of setCookies) {
|
|
71
|
+
const [nameValue, ...parts] = setCookie.split('; ')
|
|
72
|
+
const eqIdx = nameValue!.indexOf('=')
|
|
73
|
+
const name = decodeURIComponent(nameValue!.slice(0, eqIdx))
|
|
74
|
+
const value = decodeURIComponent(nameValue!.slice(eqIdx + 1))
|
|
75
|
+
|
|
76
|
+
if (this.isExcluded(name)) {
|
|
77
|
+
headers.append('Set-Cookie', setCookie)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const encrypted = await this.encrypter.encrypt(value)
|
|
83
|
+
const newSetCookie = `${encodeURIComponent(name)}=${encodeURIComponent(encrypted)}; ${parts.join('; ')}`
|
|
84
|
+
headers.append('Set-Cookie', newSetCookie)
|
|
85
|
+
} catch {
|
|
86
|
+
// If encryption fails, send unencrypted (shouldn't happen with valid key)
|
|
87
|
+
headers.append('Set-Cookie', setCookie)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(response.body, {
|
|
92
|
+
status: response.status,
|
|
93
|
+
statusText: response.statusText,
|
|
94
|
+
headers,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private isExcluded(name: string): boolean {
|
|
99
|
+
return this.except.includes(name)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Container, Constructor } from '../contracts/Container.ts'
|
|
2
|
+
import type { Middleware, NextFunction } from '../contracts/Middleware.ts'
|
|
3
|
+
import type { MantiqRequest } from '../contracts/Request.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes a chain of middleware around a destination handler.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const response = await new Pipeline(container)
|
|
10
|
+
* .send(request)
|
|
11
|
+
* .through([AuthMiddleware, ThrottleMiddleware])
|
|
12
|
+
* .then(async (req) => controller.handle(req))
|
|
13
|
+
*/
|
|
14
|
+
export class Pipeline {
|
|
15
|
+
private passable!: MantiqRequest
|
|
16
|
+
private pipes: Constructor<Middleware>[] = []
|
|
17
|
+
/** Track middleware instances for terminable cleanup */
|
|
18
|
+
private resolved: Middleware[] = []
|
|
19
|
+
|
|
20
|
+
constructor(private readonly container: Container) {}
|
|
21
|
+
|
|
22
|
+
send(request: MantiqRequest): this {
|
|
23
|
+
this.passable = request
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
through(middleware: Constructor<Middleware>[]): this {
|
|
28
|
+
this.pipes = middleware
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async then(destination: (request: MantiqRequest) => Promise<Response>): Promise<Response> {
|
|
33
|
+
const pipeline = this.build(destination)
|
|
34
|
+
return pipeline()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run terminable middleware after the response has been sent.
|
|
39
|
+
* Call this after the response is returned from then().
|
|
40
|
+
*/
|
|
41
|
+
async terminate(response: Response): Promise<void> {
|
|
42
|
+
for (const middleware of this.resolved) {
|
|
43
|
+
if (middleware.terminate) {
|
|
44
|
+
await middleware.terminate(this.passable, response)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
private build(
|
|
52
|
+
destination: (request: MantiqRequest) => Promise<Response>,
|
|
53
|
+
): NextFunction {
|
|
54
|
+
// Build from the inside out: last middleware wraps the destination
|
|
55
|
+
return this.pipes.reduceRight(
|
|
56
|
+
(next: NextFunction, MiddlewareClass: Constructor<Middleware>): NextFunction => {
|
|
57
|
+
return async (): Promise<Response> => {
|
|
58
|
+
const middleware = this.container.make(MiddlewareClass)
|
|
59
|
+
this.resolved.push(middleware)
|
|
60
|
+
return middleware.handle(this.passable, next)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
() => destination(this.passable),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Middleware, NextFunction } from '../contracts/Middleware.ts'
|
|
2
|
+
import type { MantiqRequest } from '../contracts/Request.ts'
|
|
3
|
+
import { SessionManager } from '../session/SessionManager.ts'
|
|
4
|
+
import { SessionStore } from '../session/Store.ts'
|
|
5
|
+
import { CookieSessionHandler } from '../session/handlers/CookieSessionHandler.ts'
|
|
6
|
+
import { serializeCookie } from '../http/Cookie.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Middleware that starts a session on each request.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle:
|
|
12
|
+
* 1. Read session ID from cookie (or generate a new one)
|
|
13
|
+
* 2. Load session data from the handler
|
|
14
|
+
* 3. Attach session to request
|
|
15
|
+
* 4. Call next()
|
|
16
|
+
* 5. Age flash data
|
|
17
|
+
* 6. Save session
|
|
18
|
+
* 7. Attach session cookie to response
|
|
19
|
+
*/
|
|
20
|
+
export class StartSession implements Middleware {
|
|
21
|
+
constructor(private readonly manager: SessionManager) {}
|
|
22
|
+
|
|
23
|
+
async handle(request: MantiqRequest, next: NextFunction): Promise<Response> {
|
|
24
|
+
const config = this.manager.getConfig()
|
|
25
|
+
const handler = this.manager.driver()
|
|
26
|
+
|
|
27
|
+
// Read session ID from cookie
|
|
28
|
+
const sessionId = request.cookie(config.cookie) ?? SessionStore.generateId()
|
|
29
|
+
|
|
30
|
+
// For cookie driver: seed data from cookie before starting
|
|
31
|
+
if (handler instanceof CookieSessionHandler) {
|
|
32
|
+
const cookieData = request.cookie(config.cookie + '_data') ?? ''
|
|
33
|
+
if (cookieData) {
|
|
34
|
+
try {
|
|
35
|
+
handler.setDataFromCookie(sessionId, decodeURIComponent(cookieData))
|
|
36
|
+
} catch {
|
|
37
|
+
// Invalid cookie data — will start fresh
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create and start session
|
|
43
|
+
const session = new SessionStore(config.cookie, handler, sessionId)
|
|
44
|
+
await session.start()
|
|
45
|
+
|
|
46
|
+
// Attach to request
|
|
47
|
+
request.setSession(session)
|
|
48
|
+
|
|
49
|
+
// Process request
|
|
50
|
+
const response = await next()
|
|
51
|
+
|
|
52
|
+
// Age flash data
|
|
53
|
+
session.ageFlashData()
|
|
54
|
+
|
|
55
|
+
// Save session
|
|
56
|
+
await session.save()
|
|
57
|
+
|
|
58
|
+
// Attach cookie to response
|
|
59
|
+
return this.addCookieToResponse(response, session, config)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private addCookieToResponse(
|
|
63
|
+
response: Response,
|
|
64
|
+
session: SessionStore,
|
|
65
|
+
config: ReturnType<SessionManager['getConfig']>,
|
|
66
|
+
): Response {
|
|
67
|
+
const headers = new Headers(response.headers)
|
|
68
|
+
|
|
69
|
+
// Session ID cookie
|
|
70
|
+
const cookieOpts: Record<string, any> = {
|
|
71
|
+
path: config.path,
|
|
72
|
+
secure: config.secure,
|
|
73
|
+
httpOnly: config.httpOnly,
|
|
74
|
+
sameSite: config.sameSite,
|
|
75
|
+
maxAge: config.lifetime * 60,
|
|
76
|
+
}
|
|
77
|
+
if (config.domain) cookieOpts.domain = config.domain
|
|
78
|
+
|
|
79
|
+
headers.append(
|
|
80
|
+
'Set-Cookie',
|
|
81
|
+
serializeCookie(session.getName(), session.getId(), cookieOpts),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return new Response(response.body, {
|
|
85
|
+
status: response.status,
|
|
86
|
+
statusText: response.statusText,
|
|
87
|
+
headers,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Middleware, NextFunction } from '../contracts/Middleware.ts'
|
|
2
|
+
import type { MantiqRequest } from '../contracts/Request.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Trims whitespace from all string input values.
|
|
6
|
+
* Applied globally in the web middleware group.
|
|
7
|
+
*/
|
|
8
|
+
export class TrimStringsMiddleware implements Middleware {
|
|
9
|
+
/** Input keys to skip (e.g., passwords). */
|
|
10
|
+
protected except: string[] = ['password', 'password_confirmation', 'current_password']
|
|
11
|
+
|
|
12
|
+
async handle(request: MantiqRequest, next: NextFunction): Promise<Response> {
|
|
13
|
+
// @internal: Trimming happens lazily when input() is accessed.
|
|
14
|
+
// We wrap the request's input by patching parsedBody after first access.
|
|
15
|
+
// Since MantiqRequest.input() is async and parses lazily, we can intercept
|
|
16
|
+
// by using a Proxy or post-parse hook. For simplicity we pre-parse here.
|
|
17
|
+
try {
|
|
18
|
+
const body = await (request as any).parseBodyForTrimming?.()
|
|
19
|
+
if (body && typeof body === 'object') {
|
|
20
|
+
for (const key of Object.keys(body)) {
|
|
21
|
+
if (!this.except.includes(key) && typeof body[key] === 'string') {
|
|
22
|
+
body[key] = (body[key] as string).trim()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore parse errors — next middleware will handle them
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return next()
|
|
31
|
+
}
|
|
32
|
+
}
|