@arkstack/common 0.7.7 → 0.7.8
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 +735 -20
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,37 +1,752 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@arkstack/common`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Core utilities, primitives, and shared infrastructure for the Arkstack framework ecosystem. This package provides the building blocks used across all Arkstack packages — error handling, logging, hashing, encryption, lifecycle management, hooks, and more.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Configuration management
|
|
7
|
-
- `ErrorHandler` and shared exception classes
|
|
8
|
-
- Hashing and encryption helpers
|
|
9
|
-
- Typed model resolution
|
|
10
|
-
- Pagination helpers
|
|
5
|
+
---
|
|
11
6
|
|
|
12
|
-
##
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Modules](#modules)
|
|
11
|
+
- [System](#system)
|
|
12
|
+
- [Logger](#logger)
|
|
13
|
+
- [ErrorHandler](#errorhandler)
|
|
14
|
+
- [Exceptions](#exceptions)
|
|
15
|
+
- [Hook](#hook)
|
|
16
|
+
- [Encryption](#encryption)
|
|
17
|
+
- [Hash](#hash)
|
|
18
|
+
- [Network](#network)
|
|
19
|
+
- [Lifecycle](#lifecycle)
|
|
20
|
+
- [Prototypes](#prototypes)
|
|
21
|
+
- [Global Augmentations](#global-augmentations)
|
|
22
|
+
- [Types](#types)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @arkstack/common
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Modules
|
|
35
|
+
|
|
36
|
+
### System
|
|
37
|
+
|
|
38
|
+
**`src/system.ts`**
|
|
39
|
+
|
|
40
|
+
Provides core application-level utilities for environment variable access, configuration loading, path resolution, and dynamic file importing.
|
|
41
|
+
|
|
42
|
+
#### `env(key, defaultValue?)`
|
|
43
|
+
|
|
44
|
+
Reads a value from `process.env` with automatic type coercion. Booleans (`true`, `false`, `on`, `off`), numbers, `null`, and empty strings are all handled gracefully.
|
|
13
45
|
|
|
14
46
|
```ts
|
|
15
|
-
import {
|
|
16
|
-
import type User from './src/app/models/User';
|
|
47
|
+
import { env } from '@arkstack/common'
|
|
17
48
|
|
|
18
|
-
const
|
|
49
|
+
const port = env('PORT', 3000) // number
|
|
50
|
+
const debug = env('DEBUG', false) // boolean
|
|
51
|
+
const name = env('APP_NAME', 'App') // string
|
|
19
52
|
```
|
|
20
53
|
|
|
21
|
-
|
|
54
|
+
**Type coercion rules:**
|
|
55
|
+
|
|
56
|
+
| Raw value | Resolved type |
|
|
57
|
+
|-----------|---------------|
|
|
58
|
+
| `"true"` / `"on"` | `true` |
|
|
59
|
+
| `"false"` / `"off"` | `false` |
|
|
60
|
+
| Numeric string | `number` |
|
|
61
|
+
| `"null"` | `null` |
|
|
62
|
+
| `""` | `undefined` (falls back to `defaultValue`) |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
#### `config(key?, defaultValue?)`
|
|
67
|
+
|
|
68
|
+
Loads and merges all configuration files from the build output's `config/` directory. Supports dot-path key access with full TypeScript inference.
|
|
22
69
|
|
|
23
70
|
```ts
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
71
|
+
import { config } from '@arkstack/common'
|
|
72
|
+
|
|
73
|
+
// Get the full config object
|
|
74
|
+
const allConfig = config()
|
|
75
|
+
|
|
76
|
+
// Access a nested key
|
|
77
|
+
const dbHost = config('database.host', 'localhost')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Config files are loaded from the resolved `outputDir()` using `createRequire`. Middleware config files are skipped when running in CLI context.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
#### `appUrl(link?)`
|
|
85
|
+
|
|
86
|
+
Builds a fully-qualified application URL from `APP_URL` and `PORT` environment variables.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { appUrl } from '@arkstack/common'
|
|
90
|
+
|
|
91
|
+
appUrl() // "http://localhost:3000"
|
|
92
|
+
appUrl('/api/health') // "http://localhost:3000/api/health"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
#### `nodeEnv()`
|
|
98
|
+
|
|
99
|
+
Returns `'dev'` or `'prod'` based on the `NODE_ENV` environment variable. Defaults to `'dev'` for any unrecognised value.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { nodeEnv } from '@arkstack/common'
|
|
103
|
+
|
|
104
|
+
nodeEnv() // "dev" | "prod"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
#### `outputDir(cwd?)`
|
|
110
|
+
|
|
111
|
+
Resolves the build output directory. In development, defaults to `.arkstack/build`; in production, to `dist`. Both can be overridden via environment variables:
|
|
112
|
+
|
|
113
|
+
| Variable | Context | Default |
|
|
114
|
+
|----------|---------|---------|
|
|
115
|
+
| `OUTPUT_DIR_DEV` | Development | `.arkstack/build` |
|
|
116
|
+
| `OUTPUT_DIR` | Production | `dist` |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
#### `importFile<T>(filePath)`
|
|
121
|
+
|
|
122
|
+
Dynamically imports a file using [Jiti](https://github.com/unjs/jiti), with TypeScript and `tsconfig` path support. Useful for loading user-defined config or plugin files at runtime.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { importFile } from '@arkstack/common'
|
|
126
|
+
|
|
127
|
+
const module = await importFile<{ default: MyConfig }>('./config/app.ts')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Logger
|
|
133
|
+
|
|
134
|
+
**`src/Logger.ts`**
|
|
135
|
+
|
|
136
|
+
A structured, chalk-powered console logger with verbosity control, two-column formatting, and a composable parsing API.
|
|
137
|
+
|
|
138
|
+
#### Basic log levels
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { Logger } from '@arkstack/common'
|
|
142
|
+
|
|
143
|
+
Logger.success('Server started')
|
|
144
|
+
Logger.info('Listening on port 3000')
|
|
145
|
+
Logger.warn('Deprecated option used')
|
|
146
|
+
Logger.error('Something went wrong', false) // false = don't exit
|
|
147
|
+
Logger.debug('Internal state dump') // only shown at verbosity >= 3
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Each level uses a distinct icon and colour:
|
|
151
|
+
|
|
152
|
+
| Method | Icon | Colour |
|
|
153
|
+
|--------|------|--------|
|
|
154
|
+
| `success` | `✓` | Green |
|
|
155
|
+
| `info` | `ℹ` | Blue |
|
|
156
|
+
| `warn` | `⚠` | Yellow |
|
|
157
|
+
| `error` | `✖` | Red |
|
|
158
|
+
| `debug` | `🐛` | Gray |
|
|
159
|
+
|
|
160
|
+
The second argument for all level methods is `exit` (boolean). When `true`, the process exits after logging. `error` exits by default (`exit = true`); all others default to `false`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
#### `Logger.configure(options)`
|
|
165
|
+
|
|
166
|
+
Sets global verbosity and suppression behaviour.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
Logger.configure({
|
|
170
|
+
verbosity: 3, // enables debug output
|
|
171
|
+
quiet: true, // suppresses info and success
|
|
172
|
+
silent: true, // suppresses all output
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
#### `Logger.twoColumnDetail(name, value, log?, spacer?)`
|
|
179
|
+
|
|
180
|
+
Renders a right-aligned two-column layout padded to the terminal width.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
Logger.twoColumnDetail('Route', 'GET /api/users')
|
|
184
|
+
// "Route ......................................... GET /api/users"
|
|
185
|
+
|
|
186
|
+
const row = Logger.twoColumnDetail('Route', 'GET /api/users', false)
|
|
187
|
+
// returns [name, dots, value] without printing
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
#### `Logger.describe(name, desc, width?, log?)`
|
|
193
|
+
|
|
194
|
+
Similar to `twoColumnDetail`, but uses a fixed width with space padding rather than dots. Useful for command help listings.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
Logger.describe('--port', 'Port to listen on', 40)
|
|
198
|
+
// "--port Port to listen on"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
#### `Logger.split(name, value, status?, exit?, preserveCol?, spacer?)`
|
|
204
|
+
|
|
205
|
+
Like `twoColumnDetail`, but wraps the left column with a coloured background badge based on `status`.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
Logger.split('Database', 'Connected', 'success')
|
|
209
|
+
Logger.split('Migration', 'Failed', 'error', true) // exits after logging
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
#### `Logger.parse(config, joiner?, log?, sc?)`
|
|
215
|
+
|
|
216
|
+
Composes a styled string from a `[text, chalkStyle]` pair array.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
Logger.parse([
|
|
220
|
+
['Arkstack', 'bold'],
|
|
221
|
+
['v1.0.0', 'gray'],
|
|
222
|
+
], ' ') // "Arkstack v1.0.0"
|
|
223
|
+
|
|
224
|
+
// Return instead of print
|
|
225
|
+
const str = Logger.parse([['Ready', 'green']], ' ', false)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
#### `Logger.log(config, joiner?, log?, sc?)`
|
|
231
|
+
|
|
232
|
+
A flexible polymorphic logger that accepts either a string + style, or a `LoggerParseSignature` array. Returns the `Logger` class when called with no arguments.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
Logger.log('PORT:3000', 'cyan')
|
|
236
|
+
Logger.log([['PORT', 'bold'], ['3000', 'cyan']], ' ')
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
#### `Logger.chalker(styles[])`
|
|
242
|
+
|
|
243
|
+
Returns a function that applies a chain of chalk styles to any input.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const highlight = Logger.chalker(['bold', 'green'])
|
|
247
|
+
console.log(highlight('Ready'))
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
#### `Logger.console()`
|
|
253
|
+
|
|
254
|
+
Returns a `Console`-compatible object with `log`, `debug`, `warn`, `info`, and `error` methods. Can be used as a drop-in replacement for `globalThis.console`.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const console = Logger.console()
|
|
258
|
+
console.log('hello')
|
|
259
|
+
console.warn('watch out')
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### ErrorHandler
|
|
265
|
+
|
|
266
|
+
**`src/ErrorHandler.ts`**
|
|
267
|
+
|
|
268
|
+
A static utility class for normalising, serialising, classifying, and logging errors. Integrates with [Pino](https://getpino.io) for persistent error file logging.
|
|
269
|
+
|
|
270
|
+
#### `ErrorHandler.createErrorPayload(err, fallbackMessage?)`
|
|
271
|
+
|
|
272
|
+
The primary method. Converts any thrown value into a consistent `ArkstackErrorPayload` object, handling validation errors, model-not-found errors, and generic errors uniformly.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { ErrorHandler } from '@arkstack/common'
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// ...
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const payload = ErrorHandler.createErrorPayload(err, 'Request failed')
|
|
281
|
+
// { status: 'error', code: 422, message: '...', errors: {...} }
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Payload shape:**
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
{
|
|
289
|
+
status: 'error'
|
|
290
|
+
code: number // HTTP status code (100–599)
|
|
291
|
+
message: string
|
|
292
|
+
errors?: unknown // present for validation errors
|
|
293
|
+
stack?: string // present in development unless HIDE_ERROR_STACK is set
|
|
28
294
|
}
|
|
29
295
|
```
|
|
30
296
|
|
|
31
|
-
|
|
297
|
+
**Classification logic:**
|
|
298
|
+
|
|
299
|
+
| Error type | `code` | `errors` populated |
|
|
300
|
+
|------------|--------|--------------------|
|
|
301
|
+
| Validation error (has `.errors`) | `statusCode` / `status` or `422` | Yes |
|
|
302
|
+
| Model not found (has `.getModelName()`) | `404` | No |
|
|
303
|
+
| Generic error | `statusCode` / `status` or `500` | Stack trace as object |
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
#### `ErrorHandler.serializeError(value, seen?)`
|
|
308
|
+
|
|
309
|
+
Recursively serialises any value — including `Error` instances and circular references — into a plain JSON-safe object. Circular references are replaced with `'[Circular]'`.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const serialized = ErrorHandler.serializeError(new Error('oops'))
|
|
313
|
+
// { name: 'Error', message: 'oops', stack: '...' }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
#### `ErrorHandler.normalizeStatusCode(value, fallback?)`
|
|
319
|
+
|
|
320
|
+
Ensures a status code is a valid integer in the range `100–599`. Returns the fallback (default `500`) for anything invalid.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
ErrorHandler.normalizeStatusCode('422') // 422
|
|
324
|
+
ErrorHandler.normalizeStatusCode('xyz') // 500
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
#### `ErrorHandler.getErrorLogger()`
|
|
330
|
+
|
|
331
|
+
Returns a Pino logger instance that writes to `storage/logs/error.log` (created automatically). Instances are cached per destination path.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
#### `ErrorHandler.logUnhandledError(err, request, message)`
|
|
336
|
+
|
|
337
|
+
Persists an unhandled error to the error log file, including the serialised error and the associated request context.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
ErrorHandler.logUnhandledError(err, { method: 'GET', url: '/api' }, 'Unhandled exception')
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
#### Classification helpers
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
ErrorHandler.isValidationError(err) // true if err.errors is defined
|
|
349
|
+
ErrorHandler.isModelNotFoundError(err) // true if err.getModelName is a function
|
|
350
|
+
ErrorHandler.shouldLogError(err) // false for validation/model-not-found errors
|
|
351
|
+
ErrorHandler.shouldHideStack() // true if HIDE_ERROR_STACK env is set
|
|
352
|
+
ErrorHandler.getPrimaryError(err) // unwraps err.cause if present
|
|
353
|
+
ErrorHandler.toErrorShape(value) // casts unknown to ArkstackErrorShape if object
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
All static methods are also exported as named standalone functions for convenience:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import {
|
|
360
|
+
createErrorPayload,
|
|
361
|
+
isValidationError,
|
|
362
|
+
serializeError,
|
|
363
|
+
logUnhandledError,
|
|
364
|
+
// ...
|
|
365
|
+
} from '@arkstack/common'
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
### Exceptions
|
|
371
|
+
|
|
372
|
+
**`src/Exceptions/`**
|
|
373
|
+
|
|
374
|
+
A three-level exception hierarchy for structured error throwing.
|
|
375
|
+
|
|
376
|
+
#### `Exception`
|
|
377
|
+
|
|
378
|
+
Base class extending `Error`. Sets `.name` to `'Exception'`.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { Exception } from '@arkstack/common'
|
|
382
|
+
|
|
383
|
+
throw new Exception('Something went wrong')
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
#### `AppException`
|
|
389
|
+
|
|
390
|
+
Extends `Exception`. Adds `statusCode` (default `400`) and an optional `errors` map for field-level validation errors.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { AppException } from '@arkstack/common'
|
|
394
|
+
|
|
395
|
+
const err = new AppException('Validation failed', 422)
|
|
396
|
+
err.errors = { email: ['Email is required'] }
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
#### `RequestException`
|
|
402
|
+
|
|
403
|
+
Extends `AppException`. Intended for HTTP request-level errors. Provides two static assertion helpers:
|
|
404
|
+
|
|
405
|
+
**`RequestException.assertNotEmpty(value, message, code?)`**
|
|
406
|
+
|
|
407
|
+
Throws a `RequestException` if the value is `null` or `undefined`. Narrows the type on success.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { RequestException } from '@arkstack/common'
|
|
411
|
+
|
|
412
|
+
const user = await User.find(id)
|
|
413
|
+
RequestException.assertNotEmpty(user, 'User not found', 404)
|
|
414
|
+
// user is now User (not null | undefined)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**`RequestException.abortIf(condition, message, code?)`**
|
|
418
|
+
|
|
419
|
+
Throws if the condition is truthy.
|
|
32
420
|
|
|
33
421
|
```ts
|
|
34
|
-
|
|
422
|
+
RequestException.abortIf(!user.isActive, 'Account is suspended', 403)
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
### Hook
|
|
428
|
+
|
|
429
|
+
**`src/Hook.ts`**
|
|
430
|
+
|
|
431
|
+
A global, named hook registry for extending Arkstack internals without modifying core code. Hooks are keyed by name and support positional slots (`before`, `after`, or any custom string).
|
|
432
|
+
|
|
433
|
+
#### `Hook.set(name, hook)`
|
|
434
|
+
|
|
435
|
+
Registers a hook. Multiple calls for the same name are merged.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { Hook } from '@arkstack/common'
|
|
439
|
+
|
|
440
|
+
Hook.set('request:handle', {
|
|
441
|
+
before: (ctx) => console.log('before handler'),
|
|
442
|
+
after: (ctx) => console.log('after handler'),
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
#### `Hook.get(name, pos?)`
|
|
449
|
+
|
|
450
|
+
Retrieves the full hook object or a specific positional handler.
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
const hook = Hook.get('request:handle') // IHook | undefined
|
|
454
|
+
const before = Hook.get('request:handle', 'before') // function | undefined
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
#### `Hook.has(name, pos?)`
|
|
460
|
+
|
|
461
|
+
Checks whether a hook (or a specific position within it) exists.
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
Hook.has('request:handle') // true | false
|
|
465
|
+
Hook.has('request:handle', 'after') // true | false
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
#### `Hook.unset(name?, pos?)`
|
|
471
|
+
|
|
472
|
+
Removes a hook or a single positional handler. If the hook becomes empty after removal, it is deleted entirely. Called with no arguments, it delegates to `Hook.clear()`.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
Hook.unset('request:handle', 'before') // removes only the 'before' handler
|
|
476
|
+
Hook.unset('request:handle') // removes the entire hook
|
|
477
|
+
Hook.unset() // clears all hooks
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
#### `Hook.getAll()`
|
|
483
|
+
|
|
484
|
+
Returns all registered hooks as a plain record.
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
const hooks = Hook.getAll()
|
|
488
|
+
// { 'request:handle': { before: fn, after: fn } }
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
#### `Hook.clear()`
|
|
494
|
+
|
|
495
|
+
Clears all registered hooks.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### Encryption
|
|
500
|
+
|
|
501
|
+
**`src/utils/encryption.ts`**
|
|
502
|
+
|
|
503
|
+
AES-256-GCM symmetric encryption for sensitive values (e.g. two-factor authentication secrets). Requires the `TWO_FACTOR_ENCRYPTION_KEY` environment variable.
|
|
504
|
+
|
|
505
|
+
#### `Encryption.encrypt(value)`
|
|
506
|
+
|
|
507
|
+
Encrypts a string. Returns a colon-delimited base64url string: `<iv>:<authTag>:<ciphertext>`.
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
import { Encryption } from '@arkstack/common'
|
|
511
|
+
|
|
512
|
+
const token = Encryption.encrypt('my-secret-value')
|
|
513
|
+
// "abc123:def456:ghi789"
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
#### `Encryption.decrypt(payload)`
|
|
517
|
+
|
|
518
|
+
Decrypts a payload produced by `encrypt`. Throws if the format is invalid or the key is wrong.
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
const original = Encryption.decrypt(token)
|
|
522
|
+
// "my-secret-value"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Environment variable:**
|
|
526
|
+
|
|
527
|
+
| Variable | Required | Description |
|
|
528
|
+
|----------|----------|-------------|
|
|
529
|
+
| `TWO_FACTOR_ENCRYPTION_KEY` | Yes | Raw secret; hashed to a 256-bit key internally via SHA-256 |
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
### Hash
|
|
534
|
+
|
|
535
|
+
**`src/utils/hash.ts`**
|
|
536
|
+
|
|
537
|
+
Password hashing and OTP generation utilities.
|
|
538
|
+
|
|
539
|
+
#### `Hash.make(value)`
|
|
540
|
+
|
|
541
|
+
Hashes a string using bcrypt with a salt factor of 10.
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
import { Hash } from '@arkstack/common'
|
|
545
|
+
|
|
546
|
+
const hashed = await Hash.make('user-password')
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### `Hash.verify(value, hashedValue)`
|
|
550
|
+
|
|
551
|
+
Compares a plain-text value against a bcrypt hash.
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
const isValid = await Hash.verify('user-password', hashed)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
#### `Hash.otp(digits?, label?, period?)`
|
|
558
|
+
|
|
559
|
+
Creates a TOTP instance using the `otpauth` library with `SHA1` and a static secret. Suitable for simple time-based OTP flows.
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
const totp = Hash.otp(6, 'user@example.com', 60)
|
|
563
|
+
const token = totp.generate()
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
#### `Hash.totp(secret, label, issuer?, period?)`
|
|
567
|
+
|
|
568
|
+
Creates a TOTP instance from a base32-encoded secret. Intended for user-specific TOTP (e.g. authenticator app integration).
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
const totp = Hash.totp(user.totpSecret, user.email)
|
|
572
|
+
const isValid = totp.validate({ token: userInput }) !== null
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
### Network
|
|
578
|
+
|
|
579
|
+
**`src/network.ts`**
|
|
580
|
+
|
|
581
|
+
Utilities for starting an HTTP server with automatic port detection and rendering error views.
|
|
582
|
+
|
|
583
|
+
#### `bootWithDetectedPort(boot, preferredPort?, app?)`
|
|
584
|
+
|
|
585
|
+
Detects whether the preferred port is available (using `detect-port`) and boots the server on the first free port. Also initialises key globals: `env`, `config`, `str`, `app`, and `arkctx`.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { bootWithDetectedPort } from '@arkstack/common'
|
|
589
|
+
|
|
590
|
+
await bootWithDetectedPort(async (port) => {
|
|
591
|
+
server.listen(port)
|
|
592
|
+
Logger.success(`Server:http://localhost:${port}`)
|
|
593
|
+
}, 3000, appInstance)
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Globals set:**
|
|
597
|
+
|
|
598
|
+
| Global | Value |
|
|
599
|
+
|--------|-------|
|
|
600
|
+
| `globalThis.app` | `() => app` |
|
|
601
|
+
| `globalThis.env` | `env` |
|
|
602
|
+
| `globalThis.config` | `config` |
|
|
603
|
+
| `globalThis.str` | `str` (from `@h3ravel/support`) |
|
|
604
|
+
| `globalThis.arkctx` | `{ runtime: 'HTTP' }` |
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
#### `renderError({ message, stack, title, code })`
|
|
609
|
+
|
|
610
|
+
Renders an error page using the `~arkstack/common.error` view template. Falls back to a human-readable title from a built-in status code map.
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
import { renderError } from '@arkstack/common'
|
|
614
|
+
|
|
615
|
+
const html = renderError({ code: 404, message: 'Page not found' })
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
**Built-in status titles:** `400`, `401`, `403`, `404`, `500`, `502`, `503`, `504`.
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
### Lifecycle
|
|
623
|
+
|
|
624
|
+
**`src/lifecycle.ts`**
|
|
625
|
+
|
|
626
|
+
#### `bindGracefulShutdown(shutdown)`
|
|
627
|
+
|
|
628
|
+
Registers a cleanup callback for `SIGINT`, `SIGTERM`, and `SIGQUIT` signals, ensuring the application shuts down cleanly.
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
import { bindGracefulShutdown } from '@arkstack/common'
|
|
632
|
+
|
|
633
|
+
bindGracefulShutdown(async () => {
|
|
634
|
+
await db.disconnect()
|
|
635
|
+
Logger.info('Server shut down gracefully')
|
|
636
|
+
})
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
### Prototypes
|
|
642
|
+
|
|
643
|
+
**`src/prototypes.ts`**
|
|
644
|
+
|
|
645
|
+
#### `loadPrototypes()`
|
|
646
|
+
|
|
647
|
+
Extends `String.prototype` with four utility methods. Call this once during application bootstrap.
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
import { loadPrototypes } from '@arkstack/common'
|
|
651
|
+
|
|
652
|
+
loadPrototypes()
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Methods added:**
|
|
656
|
+
|
|
657
|
+
| Method | Description | Example |
|
|
658
|
+
|--------|-------------|---------|
|
|
659
|
+
| `.titleCase()` | Converts to Title Case (handles `_` and `-`) | `"hello_world".titleCase()` → `"Hello World"` |
|
|
660
|
+
| `.camelCase()` | Converts to camelCase | `"Hello World".camelCase()` → `"helloWorld"` |
|
|
661
|
+
| `.pascalCase()` | Converts to PascalCase | `"hello world".pascalCase()` → `"HelloWorld"` |
|
|
662
|
+
| `.truncate(len, suffix?)` | Truncates at word boundary | `"Hello World".truncate(7)` → `"Hello..."` |
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Global Augmentations
|
|
667
|
+
|
|
668
|
+
**`src/app.d.ts`**
|
|
669
|
+
|
|
670
|
+
When `loadPrototypes()` is called and `bootWithDetectedPort()` initialises the runtime, the following globals and String prototype extensions are available throughout the application:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
// Globals (set by bootWithDetectedPort)
|
|
674
|
+
globalThis.env // GlobalEnv — typed env() accessor
|
|
675
|
+
globalThis.config // GlobalConfig — typed config() accessor
|
|
676
|
+
|
|
677
|
+
// String prototype extensions (set by loadPrototypes)
|
|
678
|
+
"my_string".titleCase()
|
|
679
|
+
"my_string".camelCase()
|
|
680
|
+
"my_string".pascalCase()
|
|
681
|
+
"my long string".truncate(10, '…')
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## Types
|
|
687
|
+
|
|
688
|
+
**`src/types.ts`**
|
|
689
|
+
|
|
690
|
+
Key exported types from the package:
|
|
691
|
+
|
|
692
|
+
| Type | Description |
|
|
693
|
+
|------|-------------|
|
|
694
|
+
| `GlobalEnv` | Typed signature for the `env()` function |
|
|
695
|
+
| `GlobalConfig` | Typed signature for the `config()` function with dot-path support |
|
|
696
|
+
| `ArkstackErrorShape` | Union of common error properties across frameworks (`statusCode`, `status`, `errors`, `cause`, etc.) |
|
|
697
|
+
| `ArkstackErrorPayload` | Normalised HTTP error response shape produced by `ErrorHandler` |
|
|
698
|
+
| `LoggerChalk` | Chalk style identifier(s) accepted by `Logger` methods |
|
|
699
|
+
| `LoggerParseSignature` | Array of `[string, LoggerChalk]` pairs for `Logger.parse()` |
|
|
700
|
+
| `LoggerLog` | Overloaded function type for `Logger.log()` |
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Environment Variables Reference
|
|
705
|
+
|
|
706
|
+
| Variable | Module | Required | Description |
|
|
707
|
+
|----------|--------|----------|-------------|
|
|
708
|
+
| `PORT` | `system`, `network` | No | HTTP server port (default: `3000`) |
|
|
709
|
+
| `APP_URL` | `system` | No | Base application URL |
|
|
710
|
+
| `APP_NAME` | `hash` | No | Application name used as TOTP issuer |
|
|
711
|
+
| `NODE_ENV` | `system` | No | `development` or `production` |
|
|
712
|
+
| `OUTPUT_DIR` | `system` | No | Production build output directory (default: `dist`) |
|
|
713
|
+
| `OUTPUT_DIR_DEV` | `system` | No | Development build output directory (default: `.arkstack/build`) |
|
|
714
|
+
| `TWO_FACTOR_ENCRYPTION_KEY` | `encryption` | Yes* | Secret key for AES-256-GCM encryption (*required only if using `Encryption`) |
|
|
715
|
+
| `HIDE_ERROR_STACK` | `ErrorHandler` | No | Set to `true`, `1`, or `on` to suppress stack traces in error payloads |
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Helpers
|
|
720
|
+
|
|
721
|
+
**`src/utils/helpers.ts`**
|
|
722
|
+
|
|
723
|
+
#### `perPage(query)`
|
|
724
|
+
|
|
725
|
+
Extracts a safe pagination limit from a query object. Clamps the result between `1` and `50`, defaulting to `15`.
|
|
726
|
+
|
|
727
|
+
```ts
|
|
728
|
+
import { perPage } from '@arkstack/common'
|
|
729
|
+
|
|
730
|
+
const limit = perPage({ limit: 100 }) // 50 (clamped)
|
|
731
|
+
const limit2 = perPage({}) // 15 (default)
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
#### `getModel(modelName)`
|
|
735
|
+
|
|
736
|
+
Dynamically imports an application model by name from the configured models directory (default: `./src/models`). Supports augmenting `ModelRegistry` for type-safe lookups.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
import { getModel } from '@arkstack/common'
|
|
740
|
+
|
|
741
|
+
const User = await getModel('User')
|
|
742
|
+
const users = await User.findAll()
|
|
743
|
+
|
|
744
|
+
// With type augmentation:
|
|
745
|
+
declare module '@arkstack/common' {
|
|
746
|
+
interface ModelRegistry {
|
|
747
|
+
User: typeof User
|
|
748
|
+
}
|
|
749
|
+
}
|
|
35
750
|
|
|
36
|
-
const
|
|
751
|
+
const TypedUser = await getModel('User') // typeof User
|
|
37
752
|
```
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkstack/common",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Core utilities, primitives, and shared infrastructure for the Arkstack ecosystem.",
|
|
6
6
|
"homepage": "https://arkstack.toneflix.net",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/arkstack-
|
|
9
|
+
"url": "git+https://github.com/arkstack-tmp/arkstack.git",
|
|
10
10
|
"directory": "packages/common"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|