@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
|
@@ -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,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,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,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,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
|
+
}
|