@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,115 @@
1
+ import type { CacheStore } from '../contracts/Cache.ts'
2
+
3
+ export interface MemcachedCacheConfig {
4
+ /** Comma-separated server list, e.g. "host1:11211,host2:11211" */
5
+ servers?: string
6
+ prefix?: string
7
+ username?: string
8
+ password?: string
9
+ }
10
+
11
+ /**
12
+ * Memcached-backed cache store using memjs.
13
+ *
14
+ * Requires `memjs` as a peer dependency:
15
+ * bun add memjs
16
+ */
17
+ export class MemcachedCacheStore implements CacheStore {
18
+ private client: any
19
+ private readonly prefix: string
20
+
21
+ constructor(config: MemcachedCacheConfig = {}) {
22
+ this.prefix = config.prefix ?? 'mantiq_cache:'
23
+
24
+ try {
25
+ const memjs = require('memjs')
26
+ this.client = memjs.Client.create(config.servers ?? '127.0.0.1:11211', {
27
+ username: config.username,
28
+ password: config.password,
29
+ })
30
+ } catch {
31
+ throw new Error(
32
+ 'memjs is required for the Memcached cache driver. Install it with: bun add memjs',
33
+ )
34
+ }
35
+ }
36
+
37
+ async get<T = unknown>(key: string): Promise<T | undefined> {
38
+ const result = await this.client.get(this.prefixed(key))
39
+ if (!result.value) return undefined
40
+
41
+ const raw = result.value.toString()
42
+ try {
43
+ return JSON.parse(raw) as T
44
+ } catch {
45
+ return raw as T
46
+ }
47
+ }
48
+
49
+ async put(key: string, value: unknown, ttl?: number): Promise<void> {
50
+ const serialized = JSON.stringify(value)
51
+ await this.client.set(this.prefixed(key), serialized, { expires: ttl ?? 0 })
52
+ }
53
+
54
+ async forget(key: string): Promise<boolean> {
55
+ return this.client.delete(this.prefixed(key))
56
+ }
57
+
58
+ async has(key: string): Promise<boolean> {
59
+ const result = await this.client.get(this.prefixed(key))
60
+ return result.value !== null
61
+ }
62
+
63
+ async flush(): Promise<void> {
64
+ await this.client.flush()
65
+ }
66
+
67
+ async increment(key: string, value = 1): Promise<number> {
68
+ // Memcached INCR requires the key to exist; if it doesn't, initialize it
69
+ try {
70
+ const result = await this.client.increment(this.prefixed(key), value, { initial: value })
71
+ return result.value ?? value
72
+ } catch {
73
+ await this.put(key, value)
74
+ return value
75
+ }
76
+ }
77
+
78
+ async decrement(key: string, value = 1): Promise<number> {
79
+ try {
80
+ const result = await this.client.decrement(this.prefixed(key), value, { initial: 0 })
81
+ return result.value ?? 0
82
+ } catch {
83
+ await this.put(key, 0)
84
+ return 0
85
+ }
86
+ }
87
+
88
+ async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
89
+ const serialized = JSON.stringify(value)
90
+ try {
91
+ await this.client.add(this.prefixed(key), serialized, { expires: ttl ?? 0 })
92
+ return true
93
+ } catch {
94
+ return false
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get the underlying memjs client.
100
+ */
101
+ getClient(): any {
102
+ return this.client
103
+ }
104
+
105
+ /**
106
+ * Close the Memcached connection.
107
+ */
108
+ async disconnect(): Promise<void> {
109
+ this.client.close()
110
+ }
111
+
112
+ private prefixed(key: string): string {
113
+ return this.prefix + key
114
+ }
115
+ }
@@ -0,0 +1,62 @@
1
+ import type { CacheStore } from '../contracts/Cache.ts'
2
+
3
+ interface CacheEntry {
4
+ value: unknown
5
+ expiresAt: number | null // timestamp in ms, null = forever
6
+ }
7
+
8
+ /**
9
+ * In-memory cache store. Fast, but lost on process restart.
10
+ * Ideal for dev, testing, and short-lived caches.
11
+ */
12
+ export class MemoryCacheStore implements CacheStore {
13
+ private store = new Map<string, CacheEntry>()
14
+
15
+ async get<T = unknown>(key: string): Promise<T | undefined> {
16
+ const entry = this.store.get(key)
17
+ if (!entry) return undefined
18
+
19
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
20
+ this.store.delete(key)
21
+ return undefined
22
+ }
23
+
24
+ return entry.value as T
25
+ }
26
+
27
+ async put(key: string, value: unknown, ttl?: number): Promise<void> {
28
+ this.store.set(key, {
29
+ value,
30
+ expiresAt: ttl != null ? Date.now() + ttl * 1000 : null,
31
+ })
32
+ }
33
+
34
+ async forget(key: string): Promise<boolean> {
35
+ return this.store.delete(key)
36
+ }
37
+
38
+ async has(key: string): Promise<boolean> {
39
+ return (await this.get(key)) !== undefined
40
+ }
41
+
42
+ async flush(): Promise<void> {
43
+ this.store.clear()
44
+ }
45
+
46
+ async increment(key: string, value = 1): Promise<number> {
47
+ const current = await this.get<number>(key)
48
+ const newValue = (current ?? 0) + value
49
+ await this.put(key, newValue)
50
+ return newValue
51
+ }
52
+
53
+ async decrement(key: string, value = 1): Promise<number> {
54
+ return this.increment(key, -value)
55
+ }
56
+
57
+ async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
58
+ if (await this.has(key)) return false
59
+ await this.put(key, value, ttl)
60
+ return true
61
+ }
62
+ }
@@ -0,0 +1,39 @@
1
+ import type { CacheStore } from '../contracts/Cache.ts'
2
+
3
+ /**
4
+ * Null cache store — never stores anything.
5
+ * Useful for disabling cache in testing or specific environments.
6
+ */
7
+ export class NullCacheStore implements CacheStore {
8
+ async get<T = unknown>(_key: string): Promise<T | undefined> {
9
+ return undefined
10
+ }
11
+
12
+ async put(_key: string, _value: unknown, _ttl?: number): Promise<void> {
13
+ // noop
14
+ }
15
+
16
+ async forget(_key: string): Promise<boolean> {
17
+ return false
18
+ }
19
+
20
+ async has(_key: string): Promise<boolean> {
21
+ return false
22
+ }
23
+
24
+ async flush(): Promise<void> {
25
+ // noop
26
+ }
27
+
28
+ async increment(_key: string, value = 1): Promise<number> {
29
+ return value
30
+ }
31
+
32
+ async decrement(_key: string, value = 1): Promise<number> {
33
+ return -value
34
+ }
35
+
36
+ async add(_key: string, _value: unknown, _ttl?: number): Promise<boolean> {
37
+ return false
38
+ }
39
+ }
@@ -0,0 +1,125 @@
1
+ import type { CacheStore } from '../contracts/Cache.ts'
2
+
3
+ export interface RedisCacheConfig {
4
+ host?: string
5
+ port?: number
6
+ password?: string
7
+ db?: number
8
+ prefix?: string
9
+ url?: string
10
+ }
11
+
12
+ /**
13
+ * Redis-backed cache store using ioredis.
14
+ *
15
+ * Requires `ioredis` as a peer dependency:
16
+ * bun add ioredis
17
+ */
18
+ export class RedisCacheStore implements CacheStore {
19
+ private client: any
20
+ private readonly prefix: string
21
+
22
+ constructor(config: RedisCacheConfig = {}) {
23
+ this.prefix = config.prefix ?? 'mantiq_cache:'
24
+
25
+ try {
26
+ const Redis = require('ioredis')
27
+ if (config.url) {
28
+ this.client = new Redis(config.url)
29
+ } else {
30
+ this.client = new Redis({
31
+ host: config.host ?? '127.0.0.1',
32
+ port: config.port ?? 6379,
33
+ password: config.password,
34
+ db: config.db ?? 0,
35
+ })
36
+ }
37
+ } catch {
38
+ throw new Error(
39
+ 'ioredis is required for the Redis cache driver. Install it with: bun add ioredis',
40
+ )
41
+ }
42
+ }
43
+
44
+ async get<T = unknown>(key: string): Promise<T | undefined> {
45
+ const raw = await this.client.get(this.prefixed(key))
46
+ if (raw === null) return undefined
47
+
48
+ try {
49
+ return JSON.parse(raw) as T
50
+ } catch {
51
+ return raw as T
52
+ }
53
+ }
54
+
55
+ async put(key: string, value: unknown, ttl?: number): Promise<void> {
56
+ const serialized = JSON.stringify(value)
57
+ if (ttl != null) {
58
+ await this.client.setex(this.prefixed(key), ttl, serialized)
59
+ } else {
60
+ await this.client.set(this.prefixed(key), serialized)
61
+ }
62
+ }
63
+
64
+ async forget(key: string): Promise<boolean> {
65
+ const count = await this.client.del(this.prefixed(key))
66
+ return count > 0
67
+ }
68
+
69
+ async has(key: string): Promise<boolean> {
70
+ const exists = await this.client.exists(this.prefixed(key))
71
+ return exists > 0
72
+ }
73
+
74
+ async flush(): Promise<void> {
75
+ const pattern = this.prefixed('*')
76
+ let cursor = '0'
77
+
78
+ do {
79
+ const [nextCursor, keys] = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', 1000)
80
+ cursor = nextCursor
81
+ if (keys.length > 0) {
82
+ await this.client.del(...keys)
83
+ }
84
+ } while (cursor !== '0')
85
+ }
86
+
87
+ async increment(key: string, value = 1): Promise<number> {
88
+ return this.client.incrby(this.prefixed(key), value)
89
+ }
90
+
91
+ async decrement(key: string, value = 1): Promise<number> {
92
+ return this.client.decrby(this.prefixed(key), value)
93
+ }
94
+
95
+ async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
96
+ const serialized = JSON.stringify(value)
97
+
98
+ if (ttl != null) {
99
+ // SET key value EX ttl NX — only set if not exists
100
+ const result = await this.client.set(this.prefixed(key), serialized, 'EX', ttl, 'NX')
101
+ return result === 'OK'
102
+ }
103
+
104
+ const result = await this.client.set(this.prefixed(key), serialized, 'NX')
105
+ return result === 'OK'
106
+ }
107
+
108
+ /**
109
+ * Get the underlying ioredis client for advanced operations.
110
+ */
111
+ getClient(): any {
112
+ return this.client
113
+ }
114
+
115
+ /**
116
+ * Disconnect the Redis client.
117
+ */
118
+ async disconnect(): Promise<void> {
119
+ await this.client.quit()
120
+ }
121
+
122
+ private prefixed(key: string): string {
123
+ return this.prefix + key
124
+ }
125
+ }
@@ -0,0 +1,52 @@
1
+ import { Event } from '../contracts/EventDispatcher.ts'
2
+
3
+ /**
4
+ * Fired when a cache key is found (cache hit).
5
+ */
6
+ export class CacheHit extends Event {
7
+ constructor(
8
+ public readonly key: string,
9
+ public readonly value: unknown,
10
+ public readonly store: string,
11
+ ) {
12
+ super()
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Fired when a cache key is not found (cache miss).
18
+ */
19
+ export class CacheMissed extends Event {
20
+ constructor(
21
+ public readonly key: string,
22
+ public readonly store: string,
23
+ ) {
24
+ super()
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Fired when a value is written to the cache.
30
+ */
31
+ export class KeyWritten extends Event {
32
+ constructor(
33
+ public readonly key: string,
34
+ public readonly value: unknown,
35
+ public readonly ttl: number | undefined,
36
+ public readonly store: string,
37
+ ) {
38
+ super()
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Fired when a cache key is removed.
44
+ */
45
+ export class KeyForgotten extends Event {
46
+ constructor(
47
+ public readonly key: string,
48
+ public readonly store: string,
49
+ ) {
50
+ super()
51
+ }
52
+ }
@@ -0,0 +1,115 @@
1
+ import type { Config } from '../contracts/Config.ts'
2
+ import { ConfigKeyNotFoundError } from '../errors/ConfigKeyNotFoundError.ts'
3
+
4
+ export class ConfigRepository implements Config {
5
+ private data: Record<string, any> = {}
6
+
7
+ constructor(data: Record<string, any> = {}) {
8
+ this.data = data
9
+ }
10
+
11
+ /**
12
+ * Get a config value using dot-notation.
13
+ * @throws ConfigKeyNotFoundError if key doesn't exist and no default is provided
14
+ */
15
+ get<T = any>(key: string, defaultValue?: T): T {
16
+ const value = this.getByDotNotation(this.data, key)
17
+
18
+ if (value === undefined) {
19
+ if (defaultValue !== undefined) return defaultValue
20
+ throw new ConfigKeyNotFoundError(key)
21
+ }
22
+
23
+ return value as T
24
+ }
25
+
26
+ set(key: string, value: any): void {
27
+ this.setByDotNotation(this.data, key, value)
28
+ }
29
+
30
+ has(key: string): boolean {
31
+ return this.getByDotNotation(this.data, key) !== undefined
32
+ }
33
+
34
+ all(): Record<string, any> {
35
+ return { ...this.data }
36
+ }
37
+
38
+ /**
39
+ * Load config from a directory of TypeScript/JS config files.
40
+ * Each file's default export becomes a top-level key named after the file.
41
+ */
42
+ static async fromDirectory(configPath: string): Promise<ConfigRepository> {
43
+ const data: Record<string, any> = {}
44
+
45
+ try {
46
+ const glob = new Bun.Glob('*.ts')
47
+ for await (const file of glob.scan(configPath)) {
48
+ const key = file.replace(/\.ts$/, '')
49
+ try {
50
+ const mod = await import(`${configPath}/${file}`)
51
+ data[key] = mod.default ?? mod
52
+ } catch {
53
+ // Skip files that fail to load
54
+ }
55
+ }
56
+ } catch {
57
+ // Config directory doesn't exist — use empty config
58
+ }
59
+
60
+ return new ConfigRepository(data)
61
+ }
62
+
63
+ /**
64
+ * Load from a cached JSON file (production optimization).
65
+ */
66
+ static fromCache(cachePath: string): ConfigRepository {
67
+ try {
68
+ const file = Bun.file(cachePath)
69
+ const json = JSON.parse(new TextDecoder().decode(file.arrayBuffer() as any))
70
+ return new ConfigRepository(json)
71
+ } catch {
72
+ return new ConfigRepository()
73
+ }
74
+ }
75
+
76
+ // ── Private ───────────────────────────────────────────────────────────────
77
+
78
+ private getByDotNotation(obj: Record<string, any>, key: string): any {
79
+ if (!key.includes('.')) {
80
+ return obj[key]
81
+ }
82
+
83
+ const parts = key.split('.')
84
+ let current: any = obj
85
+
86
+ for (const part of parts) {
87
+ if (current === null || current === undefined || typeof current !== 'object') {
88
+ return undefined
89
+ }
90
+ current = current[part]
91
+ }
92
+
93
+ return current
94
+ }
95
+
96
+ private setByDotNotation(obj: Record<string, any>, key: string, value: any): void {
97
+ if (!key.includes('.')) {
98
+ obj[key] = value
99
+ return
100
+ }
101
+
102
+ const parts = key.split('.')
103
+ let current = obj
104
+
105
+ for (let i = 0; i < parts.length - 1; i++) {
106
+ const part = parts[i]!
107
+ if (current[part] === undefined || typeof current[part] !== 'object') {
108
+ current[part] = {}
109
+ }
110
+ current = current[part]
111
+ }
112
+
113
+ current[parts[parts.length - 1]!] = value
114
+ }
115
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Read an environment variable with type coercion.
3
+ *
4
+ * Type coercion rules:
5
+ * - 'true' / 'false' → boolean
6
+ * - '' (empty string) → '' (not undefined)
7
+ * - undefined → defaultValue
8
+ * - All other strings remain strings
9
+ *
10
+ * @example env('APP_DEBUG', false)
11
+ * @example env('APP_NAME', 'MantiqJS')
12
+ */
13
+ export function env<T = string>(key: string, defaultValue?: T): T {
14
+ const raw = process.env[key]
15
+
16
+ if (raw === undefined) {
17
+ if (defaultValue !== undefined) return defaultValue
18
+ return undefined as unknown as T
19
+ }
20
+
21
+ // Boolean coercion
22
+ if (raw === 'true') return true as unknown as T
23
+ if (raw === 'false') return false as unknown as T
24
+
25
+ return raw as unknown as T
26
+ }