@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.
Files changed (82) hide show
  1. package/README.md +19 -0
  2. package/package.json +65 -0
  3. package/src/application/Application.ts +241 -0
  4. package/src/cache/CacheManager.ts +180 -0
  5. package/src/cache/FileCacheStore.ts +113 -0
  6. package/src/cache/MemcachedCacheStore.ts +115 -0
  7. package/src/cache/MemoryCacheStore.ts +62 -0
  8. package/src/cache/NullCacheStore.ts +39 -0
  9. package/src/cache/RedisCacheStore.ts +125 -0
  10. package/src/cache/events.ts +52 -0
  11. package/src/config/ConfigRepository.ts +115 -0
  12. package/src/config/env.ts +26 -0
  13. package/src/container/Container.ts +198 -0
  14. package/src/container/ContextualBindingBuilder.ts +21 -0
  15. package/src/contracts/Cache.ts +49 -0
  16. package/src/contracts/Config.ts +24 -0
  17. package/src/contracts/Container.ts +68 -0
  18. package/src/contracts/DriverManager.ts +16 -0
  19. package/src/contracts/Encrypter.ts +32 -0
  20. package/src/contracts/EventDispatcher.ts +32 -0
  21. package/src/contracts/ExceptionHandler.ts +20 -0
  22. package/src/contracts/Hasher.ts +19 -0
  23. package/src/contracts/Middleware.ts +23 -0
  24. package/src/contracts/Request.ts +54 -0
  25. package/src/contracts/Response.ts +19 -0
  26. package/src/contracts/Router.ts +62 -0
  27. package/src/contracts/ServiceProvider.ts +31 -0
  28. package/src/contracts/Session.ts +47 -0
  29. package/src/encryption/Encrypter.ts +197 -0
  30. package/src/encryption/errors.ts +30 -0
  31. package/src/errors/ConfigKeyNotFoundError.ts +7 -0
  32. package/src/errors/ContainerResolutionError.ts +13 -0
  33. package/src/errors/ForbiddenError.ts +7 -0
  34. package/src/errors/HttpError.ts +16 -0
  35. package/src/errors/MantiqError.ts +16 -0
  36. package/src/errors/NotFoundError.ts +7 -0
  37. package/src/errors/TokenMismatchError.ts +10 -0
  38. package/src/errors/TooManyRequestsError.ts +10 -0
  39. package/src/errors/UnauthorizedError.ts +7 -0
  40. package/src/errors/ValidationError.ts +10 -0
  41. package/src/exceptions/DevErrorPage.ts +564 -0
  42. package/src/exceptions/Handler.ts +118 -0
  43. package/src/hashing/Argon2Hasher.ts +46 -0
  44. package/src/hashing/BcryptHasher.ts +36 -0
  45. package/src/hashing/HashManager.ts +80 -0
  46. package/src/helpers/abort.ts +46 -0
  47. package/src/helpers/app.ts +17 -0
  48. package/src/helpers/cache.ts +12 -0
  49. package/src/helpers/config.ts +15 -0
  50. package/src/helpers/encrypt.ts +22 -0
  51. package/src/helpers/env.ts +1 -0
  52. package/src/helpers/hash.ts +20 -0
  53. package/src/helpers/response.ts +69 -0
  54. package/src/helpers/route.ts +24 -0
  55. package/src/helpers/session.ts +11 -0
  56. package/src/http/Cookie.ts +26 -0
  57. package/src/http/Kernel.ts +252 -0
  58. package/src/http/Request.ts +249 -0
  59. package/src/http/Response.ts +112 -0
  60. package/src/http/UploadedFile.ts +56 -0
  61. package/src/index.ts +97 -0
  62. package/src/macroable/Macroable.ts +174 -0
  63. package/src/middleware/Cors.ts +91 -0
  64. package/src/middleware/EncryptCookies.ts +101 -0
  65. package/src/middleware/Pipeline.ts +66 -0
  66. package/src/middleware/StartSession.ts +90 -0
  67. package/src/middleware/TrimStrings.ts +32 -0
  68. package/src/middleware/VerifyCsrfToken.ts +130 -0
  69. package/src/providers/CoreServiceProvider.ts +97 -0
  70. package/src/routing/ResourceRegistrar.ts +64 -0
  71. package/src/routing/Route.ts +40 -0
  72. package/src/routing/RouteCollection.ts +50 -0
  73. package/src/routing/RouteMatcher.ts +92 -0
  74. package/src/routing/Router.ts +280 -0
  75. package/src/routing/events.ts +19 -0
  76. package/src/session/SessionManager.ts +75 -0
  77. package/src/session/Store.ts +192 -0
  78. package/src/session/handlers/CookieSessionHandler.ts +42 -0
  79. package/src/session/handlers/FileSessionHandler.ts +79 -0
  80. package/src/session/handlers/MemorySessionHandler.ts +35 -0
  81. package/src/websocket/WebSocketContext.ts +20 -0
  82. 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
+ }