@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
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Contract for session handler (persistence layer).
3
+ * Each driver (memory, file, cookie) implements this.
4
+ */
5
+ export interface SessionHandler {
6
+ /**
7
+ * Read the session data for the given ID.
8
+ * Returns a serialized string, or empty string if not found.
9
+ */
10
+ read(sessionId: string): Promise<string>
11
+
12
+ /**
13
+ * Write session data for the given ID.
14
+ */
15
+ write(sessionId: string, data: string): Promise<void>
16
+
17
+ /**
18
+ * Destroy the session with the given ID.
19
+ */
20
+ destroy(sessionId: string): Promise<void>
21
+
22
+ /**
23
+ * Garbage-collect expired sessions older than `maxLifetimeSeconds`.
24
+ */
25
+ gc(maxLifetimeSeconds: number): Promise<void>
26
+ }
27
+
28
+ export interface SessionConfig {
29
+ /** Driver name: 'memory' | 'file' | 'cookie' */
30
+ driver: string
31
+ /** Session lifetime in minutes. */
32
+ lifetime: number
33
+ /** Cookie name. */
34
+ cookie: string
35
+ /** Cookie path. */
36
+ path: string
37
+ /** Cookie domain. */
38
+ domain?: string
39
+ /** Send only over HTTPS. */
40
+ secure: boolean
41
+ /** HTTP-only (no JS access). */
42
+ httpOnly: boolean
43
+ /** SameSite attribute. */
44
+ sameSite: 'Lax' | 'Strict' | 'None'
45
+ /** File driver: storage directory. */
46
+ files?: string
47
+ }
@@ -0,0 +1,197 @@
1
+ import type { Encrypter as EncrypterContract } from '../contracts/Encrypter.ts'
2
+ import { EncryptionError, DecryptionError, MissingAppKeyError } from './errors.ts'
3
+
4
+ /**
5
+ * AES-256-GCM encrypter using the Web Crypto API.
6
+ *
7
+ * Payload format (base64-encoded JSON):
8
+ * {
9
+ * iv: string // base64 12-byte IV
10
+ * value: string // base64 ciphertext (includes GCM auth tag)
11
+ * }
12
+ *
13
+ * This matches a Laravel-compatible envelope so payloads are inspectable
14
+ * (though not interchangeable due to algorithm differences).
15
+ */
16
+ export class AesEncrypter implements EncrypterContract {
17
+ private cryptoKey: CryptoKey | null = null
18
+ private readonly rawKey: ArrayBuffer
19
+
20
+ private static readonly ALGORITHM = 'AES-GCM'
21
+ private static readonly KEY_LENGTH = 256
22
+ private static readonly IV_LENGTH = 12 // 96 bits recommended for GCM
23
+
24
+ private constructor(rawKey: ArrayBuffer) {
25
+ this.rawKey = rawKey
26
+ }
27
+
28
+ /**
29
+ * Create an AesEncrypter from the APP_KEY environment variable.
30
+ *
31
+ * Accepts:
32
+ * - `base64:<base64-encoded-32-byte-key>`
33
+ * - A raw 32-byte base64 string
34
+ */
35
+ static async fromAppKey(appKey: string | undefined): Promise<AesEncrypter> {
36
+ if (!appKey) throw new MissingAppKeyError()
37
+
38
+ let keyData: ArrayBuffer
39
+
40
+ if (appKey.startsWith('base64:')) {
41
+ const b64 = appKey.slice(7)
42
+ keyData = decodeBase64(b64)
43
+ } else {
44
+ // Try to decode as raw base64
45
+ keyData = decodeBase64(appKey)
46
+ }
47
+
48
+ if (keyData.byteLength !== 32) {
49
+ throw new EncryptionError(
50
+ `Invalid key length: expected 32 bytes (256 bits), got ${keyData.byteLength} bytes. ` +
51
+ 'Generate a key with: openssl rand -base64 32',
52
+ )
53
+ }
54
+
55
+ const encrypter = new AesEncrypter(keyData)
56
+ await encrypter.importKey()
57
+ return encrypter
58
+ }
59
+
60
+ /**
61
+ * Create an AesEncrypter from raw key bytes (for testing or programmatic use).
62
+ */
63
+ static async fromRawKey(key: ArrayBuffer): Promise<AesEncrypter> {
64
+ if (key.byteLength !== 32) {
65
+ throw new EncryptionError(
66
+ `Invalid key length: expected 32 bytes (256 bits), got ${key.byteLength} bytes.`,
67
+ )
68
+ }
69
+ const encrypter = new AesEncrypter(key)
70
+ await encrypter.importKey()
71
+ return encrypter
72
+ }
73
+
74
+ /**
75
+ * Generate a new random key suitable for AES-256-GCM.
76
+ * Returns as `base64:<key>` format.
77
+ */
78
+ static generateKey(): string {
79
+ const key = new Uint8Array(32)
80
+ crypto.getRandomValues(key)
81
+ return 'base64:' + encodeBase64(key.buffer as ArrayBuffer)
82
+ }
83
+
84
+ // ── Encrypter contract ──────────────────────────────────────────────────
85
+
86
+ async encrypt(value: string): Promise<string> {
87
+ return this.encryptPayload(new TextEncoder().encode(value))
88
+ }
89
+
90
+ async decrypt(encrypted: string): Promise<string> {
91
+ const plaintext = await this.decryptPayload(encrypted)
92
+ return new TextDecoder().decode(plaintext)
93
+ }
94
+
95
+ async encryptObject(value: unknown): Promise<string> {
96
+ const json = JSON.stringify(value)
97
+ return this.encrypt(json)
98
+ }
99
+
100
+ async decryptObject<T = unknown>(encrypted: string): Promise<T> {
101
+ const json = await this.decrypt(encrypted)
102
+ try {
103
+ return JSON.parse(json) as T
104
+ } catch {
105
+ throw new DecryptionError('Decrypted payload is not valid JSON.')
106
+ }
107
+ }
108
+
109
+ getKey(): ArrayBuffer {
110
+ return this.rawKey
111
+ }
112
+
113
+ // ── Internal ────────────────────────────────────────────────────────────
114
+
115
+ private async importKey(): Promise<void> {
116
+ this.cryptoKey = await crypto.subtle.importKey(
117
+ 'raw',
118
+ this.rawKey,
119
+ { name: AesEncrypter.ALGORITHM, length: AesEncrypter.KEY_LENGTH },
120
+ false,
121
+ ['encrypt', 'decrypt'],
122
+ )
123
+ }
124
+
125
+ private async encryptPayload(data: Uint8Array): Promise<string> {
126
+ if (!this.cryptoKey) throw new EncryptionError('Encryption key not initialized.')
127
+
128
+ const iv = new Uint8Array(AesEncrypter.IV_LENGTH)
129
+ crypto.getRandomValues(iv)
130
+
131
+ try {
132
+ const ciphertext = await crypto.subtle.encrypt(
133
+ { name: AesEncrypter.ALGORITHM, iv },
134
+ this.cryptoKey,
135
+ data.buffer as ArrayBuffer,
136
+ )
137
+
138
+ const payload = {
139
+ iv: encodeBase64(iv.buffer as ArrayBuffer),
140
+ value: encodeBase64(ciphertext),
141
+ }
142
+
143
+ return btoa(JSON.stringify(payload))
144
+ } catch (err: any) {
145
+ throw new EncryptionError('Encryption failed.', { cause: err?.message })
146
+ }
147
+ }
148
+
149
+ private async decryptPayload(encrypted: string): Promise<ArrayBuffer> {
150
+ if (!this.cryptoKey) throw new DecryptionError('Encryption key not initialized.')
151
+
152
+ let payload: { iv?: string; value?: string }
153
+
154
+ try {
155
+ payload = JSON.parse(atob(encrypted))
156
+ } catch {
157
+ throw new DecryptionError('The payload is not valid base64-encoded JSON.')
158
+ }
159
+
160
+ if (!payload.iv || !payload.value) {
161
+ throw new DecryptionError('The payload is missing required fields (iv, value).')
162
+ }
163
+
164
+ const iv = decodeBase64(payload.iv)
165
+ const ciphertext = decodeBase64(payload.value)
166
+
167
+ try {
168
+ return await crypto.subtle.decrypt(
169
+ { name: AesEncrypter.ALGORITHM, iv: new Uint8Array(iv) },
170
+ this.cryptoKey,
171
+ ciphertext,
172
+ )
173
+ } catch {
174
+ throw new DecryptionError('The MAC is invalid — the payload may have been tampered with.')
175
+ }
176
+ }
177
+ }
178
+
179
+ // ── Base64 helpers (Uint8Array ↔ string) ──────────────────────────────────────
180
+
181
+ function encodeBase64(buffer: ArrayBuffer): string {
182
+ const bytes = new Uint8Array(buffer)
183
+ let binary = ''
184
+ for (let i = 0; i < bytes.byteLength; i++) {
185
+ binary += String.fromCharCode(bytes[i]!)
186
+ }
187
+ return btoa(binary)
188
+ }
189
+
190
+ function decodeBase64(b64: string): ArrayBuffer {
191
+ const binary = atob(b64)
192
+ const bytes = new Uint8Array(binary.length)
193
+ for (let i = 0; i < binary.length; i++) {
194
+ bytes[i] = binary.charCodeAt(i)
195
+ }
196
+ return bytes.buffer as ArrayBuffer
197
+ }
@@ -0,0 +1,30 @@
1
+ import { MantiqError } from '../errors/MantiqError.ts'
2
+
3
+ /**
4
+ * Thrown when encryption fails (e.g. invalid key, algorithm error).
5
+ */
6
+ export class EncryptionError extends MantiqError {
7
+ constructor(message = 'Could not encrypt the data.', context?: Record<string, any>) {
8
+ super(message, context)
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Thrown when decryption fails (e.g. tampered payload, wrong key).
14
+ */
15
+ export class DecryptionError extends MantiqError {
16
+ constructor(message = 'Could not decrypt the data.', context?: Record<string, any>) {
17
+ super(message, context)
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Thrown when APP_KEY is missing or invalid.
23
+ */
24
+ export class MissingAppKeyError extends MantiqError {
25
+ constructor() {
26
+ super(
27
+ 'No application encryption key has been specified. Set the APP_KEY environment variable (use "base64:<key>" format for raw keys).',
28
+ )
29
+ }
30
+ }
@@ -0,0 +1,7 @@
1
+ import { MantiqError } from './MantiqError.ts'
2
+
3
+ export class ConfigKeyNotFoundError extends MantiqError {
4
+ constructor(public readonly key: string) {
5
+ super(`Config key '${key}' not found and no default value provided.`)
6
+ }
7
+ }
@@ -0,0 +1,13 @@
1
+ import { MantiqError } from './MantiqError.ts'
2
+
3
+ export class ContainerResolutionError extends MantiqError {
4
+ constructor(
5
+ public readonly abstract: unknown,
6
+ public readonly reason: 'not_bound' | 'circular_dependency' | 'unresolvable_parameter',
7
+ public readonly details?: string,
8
+ ) {
9
+ super(
10
+ `Cannot resolve ${String(abstract)}: ${reason}.${details ? ' ' + details : ''}`,
11
+ )
12
+ }
13
+ }
@@ -0,0 +1,7 @@
1
+ import { HttpError } from './HttpError.ts'
2
+
3
+ export class ForbiddenError extends HttpError {
4
+ constructor(message = 'Forbidden', headers?: Record<string, string>) {
5
+ super(403, message, headers)
6
+ }
7
+ }
@@ -0,0 +1,16 @@
1
+ import { MantiqError } from './MantiqError.ts'
2
+
3
+ /**
4
+ * Base class for all HTTP-facing errors.
5
+ * Automatically converted to an HTTP response by the exception handler.
6
+ */
7
+ export class HttpError extends MantiqError {
8
+ constructor(
9
+ public readonly statusCode: number,
10
+ message: string,
11
+ public readonly headers?: Record<string, string>,
12
+ context?: Record<string, any>,
13
+ ) {
14
+ super(message, context)
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Base error class for all MantiqJS errors.
3
+ * All packages must throw subclasses of this — never raw Error.
4
+ */
5
+ export class MantiqError extends Error {
6
+ constructor(
7
+ message: string,
8
+ public readonly context?: Record<string, any>,
9
+ ) {
10
+ super(message)
11
+ this.name = this.constructor.name
12
+ if (Error.captureStackTrace) {
13
+ Error.captureStackTrace(this, this.constructor)
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ import { HttpError } from './HttpError.ts'
2
+
3
+ export class NotFoundError extends HttpError {
4
+ constructor(message = 'Not Found', headers?: Record<string, string>) {
5
+ super(404, message, headers)
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ import { HttpError } from './HttpError.ts'
2
+
3
+ /**
4
+ * Thrown when the CSRF token is missing or invalid.
5
+ */
6
+ export class TokenMismatchError extends HttpError {
7
+ constructor(message = 'CSRF token mismatch.') {
8
+ super(419, message)
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ import { HttpError } from './HttpError.ts'
2
+
3
+ export class TooManyRequestsError extends HttpError {
4
+ constructor(
5
+ message = 'Too Many Requests',
6
+ public readonly retryAfter?: number,
7
+ ) {
8
+ super(429, message, retryAfter ? { 'Retry-After': String(retryAfter) } : undefined)
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ import { HttpError } from './HttpError.ts'
2
+
3
+ export class UnauthorizedError extends HttpError {
4
+ constructor(message = 'Unauthorized', headers?: Record<string, string>) {
5
+ super(401, message, headers)
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ import { HttpError } from './HttpError.ts'
2
+
3
+ export class ValidationError extends HttpError {
4
+ constructor(
5
+ public readonly errors: Record<string, string[]>,
6
+ message = 'The given data was invalid.',
7
+ ) {
8
+ super(422, message, undefined, { errors })
9
+ }
10
+ }