@strav/kernel 0.1.0

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 (64) hide show
  1. package/package.json +59 -0
  2. package/src/cache/cache_manager.ts +60 -0
  3. package/src/cache/cache_store.ts +31 -0
  4. package/src/cache/helpers.ts +74 -0
  5. package/src/cache/index.ts +4 -0
  6. package/src/cache/memory_store.ts +63 -0
  7. package/src/config/configuration.ts +105 -0
  8. package/src/config/index.ts +2 -0
  9. package/src/config/loaders/base_loader.ts +69 -0
  10. package/src/config/loaders/env_loader.ts +112 -0
  11. package/src/config/loaders/typescript_loader.ts +56 -0
  12. package/src/config/types.ts +8 -0
  13. package/src/core/application.ts +241 -0
  14. package/src/core/container.ts +113 -0
  15. package/src/core/index.ts +4 -0
  16. package/src/core/inject.ts +39 -0
  17. package/src/core/service_provider.ts +44 -0
  18. package/src/encryption/encryption_manager.ts +215 -0
  19. package/src/encryption/helpers.ts +158 -0
  20. package/src/encryption/index.ts +3 -0
  21. package/src/encryption/types.ts +6 -0
  22. package/src/events/emitter.ts +101 -0
  23. package/src/events/index.ts +2 -0
  24. package/src/exceptions/errors.ts +71 -0
  25. package/src/exceptions/exception_handler.ts +140 -0
  26. package/src/exceptions/helpers.ts +25 -0
  27. package/src/exceptions/http_exception.ts +132 -0
  28. package/src/exceptions/index.ts +23 -0
  29. package/src/exceptions/strav_error.ts +11 -0
  30. package/src/helpers/compose.ts +104 -0
  31. package/src/helpers/crypto.ts +4 -0
  32. package/src/helpers/env.ts +50 -0
  33. package/src/helpers/index.ts +6 -0
  34. package/src/helpers/strings.ts +67 -0
  35. package/src/helpers/ulid.ts +28 -0
  36. package/src/i18n/defaults/en/validation.json +20 -0
  37. package/src/i18n/helpers.ts +76 -0
  38. package/src/i18n/i18n_manager.ts +157 -0
  39. package/src/i18n/index.ts +3 -0
  40. package/src/i18n/translator.ts +96 -0
  41. package/src/i18n/types.ts +17 -0
  42. package/src/index.ts +11 -0
  43. package/src/logger/index.ts +5 -0
  44. package/src/logger/logger.ts +113 -0
  45. package/src/logger/sinks/console_sink.ts +24 -0
  46. package/src/logger/sinks/file_sink.ts +24 -0
  47. package/src/logger/sinks/sink.ts +36 -0
  48. package/src/providers/cache_provider.ts +16 -0
  49. package/src/providers/config_provider.ts +26 -0
  50. package/src/providers/encryption_provider.ts +16 -0
  51. package/src/providers/i18n_provider.ts +17 -0
  52. package/src/providers/index.ts +8 -0
  53. package/src/providers/logger_provider.ts +16 -0
  54. package/src/providers/storage_provider.ts +16 -0
  55. package/src/storage/index.ts +32 -0
  56. package/src/storage/local_driver.ts +46 -0
  57. package/src/storage/ostra_client.ts +432 -0
  58. package/src/storage/ostra_driver.ts +58 -0
  59. package/src/storage/s3_driver.ts +51 -0
  60. package/src/storage/storage.ts +43 -0
  61. package/src/storage/storage_manager.ts +70 -0
  62. package/src/storage/types.ts +49 -0
  63. package/src/storage/upload.ts +91 -0
  64. package/tsconfig.json +5 -0
@@ -0,0 +1,113 @@
1
+ import pino from 'pino'
2
+ import Configuration from '../config/configuration.ts'
3
+ import { inject } from '../core/inject.ts'
4
+ import Emitter from '../events/emitter.ts'
5
+ import { LogSink, type SinkConfig } from './sinks/sink'
6
+ import { ConsoleSink } from './sinks/console_sink'
7
+ import { FileSink } from './sinks/file_sink'
8
+
9
+ type SinkConstructor = new (config: SinkConfig) => LogSink
10
+
11
+ /**
12
+ * Maps sink names used in `config/logging.ts` to their implementing classes.
13
+ * Register new sink types here so the Logger can instantiate them from config.
14
+ */
15
+ const sinkRegistry: Record<string, SinkConstructor> = {
16
+ console: ConsoleSink,
17
+ file: FileSink,
18
+ }
19
+
20
+ /**
21
+ * Structured logger backed by pino with configurable sinks.
22
+ *
23
+ * Sinks (console, file, …) are declared in `config/logging.ts` and combined
24
+ * at construction time via `pino.multistream`. Each sink can define its own
25
+ * minimum log level independently of the global level.
26
+ *
27
+ * @example
28
+ * // Resolve via DI (Configuration is injected automatically):
29
+ * container.singleton(Logger)
30
+ * const logger = container.resolve(Logger)
31
+ *
32
+ * logger.info('server started', { port: 3000 })
33
+ * logger.error('request failed', { statusCode: 500, path: '/api/users' })
34
+ */
35
+ @inject
36
+ export default class Logger {
37
+ private pino: pino.Logger
38
+
39
+ constructor(protected config: Configuration) {
40
+ const sinks = this.buildSinks()
41
+
42
+ const streams = sinks.map(sink => ({
43
+ stream: sink.createStream(),
44
+ level: sink.level as pino.Level,
45
+ }))
46
+
47
+ const level = this.config.get('logging.level', 'info') as string satisfies string
48
+
49
+ this.pino = streams.length > 0 ? pino({ level }, pino.multistream(streams)) : pino({ level })
50
+ }
51
+
52
+ /** Log at `trace` level (most verbose). */
53
+ trace(msg: string, context?: Record<string, unknown>): void {
54
+ context ? this.pino.trace(context, msg) : this.pino.trace(msg)
55
+ this.emitLog('trace', msg, context)
56
+ }
57
+
58
+ /** Log at `debug` level. */
59
+ debug(msg: string, context?: Record<string, unknown>): void {
60
+ context ? this.pino.debug(context, msg) : this.pino.debug(msg)
61
+ this.emitLog('debug', msg, context)
62
+ }
63
+
64
+ /** Log at `info` level. */
65
+ info(msg: string, context?: Record<string, unknown>): void {
66
+ context ? this.pino.info(context, msg) : this.pino.info(msg)
67
+ this.emitLog('info', msg, context)
68
+ }
69
+
70
+ /** Log at `warn` level. */
71
+ warn(msg: string, context?: Record<string, unknown>): void {
72
+ context ? this.pino.warn(context, msg) : this.pino.warn(msg)
73
+ this.emitLog('warn', msg, context)
74
+ }
75
+
76
+ /** Log at `error` level. */
77
+ error(msg: string, context?: Record<string, unknown>): void {
78
+ context ? this.pino.error(context, msg) : this.pino.error(msg)
79
+ this.emitLog('error', msg, context)
80
+ }
81
+
82
+ /** Log at `fatal` level (most severe). */
83
+ fatal(msg: string, context?: Record<string, unknown>): void {
84
+ context ? this.pino.fatal(context, msg) : this.pino.fatal(msg)
85
+ this.emitLog('fatal', msg, context)
86
+ }
87
+
88
+ /** Emit a log event for devtools/observability consumers. Fire-and-forget. */
89
+ private emitLog(level: string, msg: string, context?: Record<string, unknown>): void {
90
+ if (Emitter.listenerCount('log:entry') === 0) return
91
+ Emitter.emit('log:entry', { level, msg, context }).catch(() => {})
92
+ }
93
+
94
+ /**
95
+ * Read the `logging.sinks` configuration and instantiate every enabled sink
96
+ * whose name matches an entry in {@link sinkRegistry}.
97
+ */
98
+ private buildSinks(): LogSink[] {
99
+ const sinks: LogSink[] = []
100
+ const sinksConfig = this.config.get('logging.sinks', {}) as Record<string, SinkConfig>
101
+
102
+ for (const [name, sinkConfig] of Object.entries(sinksConfig)) {
103
+ if (!sinkConfig.enabled) continue
104
+
105
+ const SinkClass = sinkRegistry[name]
106
+ if (SinkClass) {
107
+ sinks.push(new SinkClass(sinkConfig))
108
+ }
109
+ }
110
+
111
+ return sinks
112
+ }
113
+ }
@@ -0,0 +1,24 @@
1
+ import build from 'pino-pretty'
2
+ import { LogSink, type SinkConfig } from './sink'
3
+
4
+ /** Configuration specific to the console sink. */
5
+ export interface ConsoleSinkConfig extends SinkConfig {
6
+ /** Enable ANSI colour output. Defaults to `true`. */
7
+ colorize?: boolean
8
+ }
9
+
10
+ /**
11
+ * Log sink that writes human-readable output to stdout via `pino-pretty`.
12
+ *
13
+ * Intended for local development; for structured JSON output in production
14
+ * consider disabling this sink and using the file sink instead.
15
+ */
16
+ export class ConsoleSink extends LogSink {
17
+ createStream() {
18
+ const config = this.config as ConsoleSinkConfig
19
+
20
+ return build({
21
+ colorize: config.colorize ?? true,
22
+ })
23
+ }
24
+ }
@@ -0,0 +1,24 @@
1
+ import { mkdirSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+ import pino from 'pino'
4
+ import { LogSink, type SinkConfig } from './sink'
5
+
6
+ /** Configuration specific to the file sink. */
7
+ export interface FileSinkConfig extends SinkConfig {
8
+ /** Path to the log file. Parent directories are created automatically. */
9
+ path: string
10
+ }
11
+
12
+ /**
13
+ * Log sink that writes structured JSON to a file using pino's optimised
14
+ * {@link pino.destination | SonicBoom} writer with asynchronous flushing.
15
+ */
16
+ export class FileSink extends LogSink {
17
+ createStream() {
18
+ const { path } = this.config as FileSinkConfig
19
+
20
+ mkdirSync(dirname(path), { recursive: true })
21
+
22
+ return pino.destination({ dest: path, sync: false })
23
+ }
24
+ }
@@ -0,0 +1,36 @@
1
+ import type { DestinationStream } from 'pino'
2
+
3
+ /** Supported log severity levels, from most to least verbose. */
4
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'
5
+
6
+ /** Configuration object for a single log sink. */
7
+ export interface SinkConfig {
8
+ /** Whether this sink is active. Disabled sinks are skipped by the Logger. */
9
+ enabled: boolean
10
+ /** Minimum severity level this sink will accept. Defaults to `"info"`. */
11
+ level?: LogLevel
12
+ [key: string]: unknown
13
+ }
14
+
15
+ /**
16
+ * Abstract base class for log sinks.
17
+ *
18
+ * Subclasses implement {@link createStream} to provide a writable destination
19
+ * (stdout, file, network, etc.) that the Logger combines via pino multistream.
20
+ *
21
+ * To add a new sink:
22
+ * 1. Extend this class and implement `createStream()`.
23
+ * 2. Register the class in the `sinkRegistry` inside `logger.ts`.
24
+ * 3. Add a corresponding section in `config/logging.ts`.
25
+ */
26
+ export abstract class LogSink {
27
+ constructor(protected config: SinkConfig) {}
28
+
29
+ /** Create the writable destination stream for this sink. */
30
+ abstract createStream(): DestinationStream
31
+
32
+ /** Minimum log level for this sink. Defaults to `"info"` when not configured. */
33
+ get level(): LogLevel {
34
+ return this.config.level ?? 'info'
35
+ }
36
+ }
@@ -0,0 +1,16 @@
1
+ import ServiceProvider from '../core/service_provider.ts'
2
+ import type Application from '../core/application.ts'
3
+ import CacheManager from '../cache/cache_manager.ts'
4
+
5
+ export default class CacheProvider extends ServiceProvider {
6
+ readonly name = 'cache'
7
+ override readonly dependencies = ['config']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(CacheManager)
11
+ }
12
+
13
+ override boot(app: Application): void {
14
+ app.resolve(CacheManager)
15
+ }
16
+ }
@@ -0,0 +1,26 @@
1
+ import ServiceProvider from '../core/service_provider.ts'
2
+ import type Application from '../core/application.ts'
3
+ import Configuration from '../config/configuration.ts'
4
+
5
+ export interface ConfigProviderOptions {
6
+ /** Path to the config directory. Default: `'./config'` */
7
+ directory?: string
8
+ }
9
+
10
+ export default class ConfigProvider extends ServiceProvider {
11
+ readonly name = 'config'
12
+
13
+ constructor(private options?: ConfigProviderOptions) {
14
+ super()
15
+ }
16
+
17
+ override register(app: Application): void {
18
+ const dir = this.options?.directory ?? './config'
19
+ app.singleton(Configuration, () => new Configuration(dir))
20
+ }
21
+
22
+ override async boot(app: Application): Promise<void> {
23
+ const config = app.resolve(Configuration)
24
+ await config.load()
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ import ServiceProvider from '../core/service_provider.ts'
2
+ import type Application from '../core/application.ts'
3
+ import EncryptionManager from '../encryption/encryption_manager.ts'
4
+
5
+ export default class EncryptionProvider extends ServiceProvider {
6
+ readonly name = 'encryption'
7
+ override readonly dependencies = ['config']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(EncryptionManager)
11
+ }
12
+
13
+ override boot(app: Application): void {
14
+ app.resolve(EncryptionManager)
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ import ServiceProvider from '../core/service_provider.ts'
2
+ import type Application from '../core/application.ts'
3
+ import I18nManager from '../i18n/i18n_manager.ts'
4
+
5
+ export default class I18nProvider extends ServiceProvider {
6
+ readonly name = 'i18n'
7
+ override readonly dependencies = ['config']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(I18nManager)
11
+ }
12
+
13
+ override async boot(app: Application): Promise<void> {
14
+ app.resolve(I18nManager)
15
+ await I18nManager.load()
16
+ }
17
+ }
@@ -0,0 +1,8 @@
1
+ export { default as ConfigProvider } from './config_provider.ts'
2
+ export { default as EncryptionProvider } from './encryption_provider.ts'
3
+ export { default as StorageProvider } from './storage_provider.ts'
4
+ export { default as CacheProvider } from './cache_provider.ts'
5
+ export { default as I18nProvider } from './i18n_provider.ts'
6
+ export { default as LoggerProvider } from './logger_provider.ts'
7
+
8
+ export type { ConfigProviderOptions } from './config_provider.ts'
@@ -0,0 +1,16 @@
1
+ import ServiceProvider from '../core/service_provider.ts'
2
+ import type Application from '../core/application.ts'
3
+ import Logger from '../logger/logger.ts'
4
+
5
+ export default class LoggerProvider extends ServiceProvider {
6
+ readonly name = 'logger'
7
+ override readonly dependencies = ['config']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(Logger)
11
+ }
12
+
13
+ override boot(app: Application): void {
14
+ app.resolve(Logger)
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import ServiceProvider from '../core/service_provider.ts'
2
+ import type Application from '../core/application.ts'
3
+ import StorageManager from '../storage/storage_manager.ts'
4
+
5
+ export default class StorageProvider extends ServiceProvider {
6
+ readonly name = 'storage'
7
+ override readonly dependencies = ['config']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(StorageManager)
11
+ }
12
+
13
+ override boot(app: Application): void {
14
+ app.resolve(StorageManager)
15
+ }
16
+ }
@@ -0,0 +1,32 @@
1
+ export { default as Storage } from './storage.ts'
2
+ export { Upload, FileTooLargeError, InvalidFileTypeError } from './upload.ts'
3
+ export { default as StorageManager } from './storage_manager.ts'
4
+ export { default as LocalDriver } from './local_driver.ts'
5
+ export { default as S3Driver } from './s3_driver.ts'
6
+ export { default as OstraDriver } from './ostra_driver.ts'
7
+ export { default as OstraClient, OstraBucket, OstraMultipart, OstraError } from './ostra_client.ts'
8
+ export type {
9
+ StorageDriver,
10
+ StorageConfig,
11
+ FileStats,
12
+ LocalDriverConfig,
13
+ S3DriverConfig,
14
+ OstraDriverConfig,
15
+ } from './types.ts'
16
+ export type { UploadResult } from './upload.ts'
17
+ export type {
18
+ OstraClientConfig,
19
+ BucketInfo,
20
+ BucketStats,
21
+ ObjectMeta,
22
+ ObjectHeaders,
23
+ ListResult,
24
+ VersionInfo,
25
+ VersionListResult,
26
+ BatchDeleteResult,
27
+ SignedUrlResult,
28
+ MultipartInfo,
29
+ PartInfo,
30
+ TokenInfo,
31
+ TokenRecord,
32
+ } from './ostra_client.ts'
@@ -0,0 +1,46 @@
1
+ import { join, extname, resolve } from 'node:path'
2
+ import { unlink } from 'node:fs/promises'
3
+ import { randomHex } from '../helpers/crypto.ts'
4
+ import type { StorageDriver, LocalDriverConfig } from './types.ts'
5
+
6
+ export default class LocalDriver implements StorageDriver {
7
+ private root: string
8
+ private baseUrl: string
9
+
10
+ constructor(config: LocalDriverConfig) {
11
+ this.root = resolve(config.root)
12
+ this.baseUrl = config.baseUrl
13
+ }
14
+
15
+ async put(directory: string, file: File, name?: string): Promise<string> {
16
+ const ext = extname(file.name)
17
+ const filename = name ?? `${randomHex(8)}${ext}`
18
+ const relativePath = join(directory, filename)
19
+ const fullPath = join(this.root, relativePath)
20
+
21
+ await Bun.write(fullPath, file)
22
+
23
+ return relativePath
24
+ }
25
+
26
+ async get(path: string): Promise<Blob | null> {
27
+ const file = Bun.file(join(this.root, path))
28
+ if (!(await file.exists())) return null
29
+ return file
30
+ }
31
+
32
+ async exists(path: string): Promise<boolean> {
33
+ return Bun.file(join(this.root, path)).exists()
34
+ }
35
+
36
+ async delete(path: string): Promise<void> {
37
+ const fullPath = join(this.root, path)
38
+ if (await Bun.file(fullPath).exists()) {
39
+ await unlink(fullPath)
40
+ }
41
+ }
42
+
43
+ url(path: string): string {
44
+ return `${this.baseUrl}/${path}`
45
+ }
46
+ }