create-rebe 1.0.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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/bin/create-rebe.js +310 -0
  4. package/package.json +40 -0
  5. package/template/.env.example +141 -0
  6. package/template/.gitattributes +16 -0
  7. package/template/.nvmrc +1 -0
  8. package/template/.prettierignore +10 -0
  9. package/template/.prettierrc +12 -0
  10. package/template/LICENSE +21 -0
  11. package/template/README.md +150 -0
  12. package/template/SECURITY.md +133 -0
  13. package/template/app/hooks/register.hook.js +22 -0
  14. package/template/app/http/controllers/auth.controller.js +34 -0
  15. package/template/app/http/middlewares/auth.middleware.js +25 -0
  16. package/template/app/http/middlewares/register.middleware.js +18 -0
  17. package/template/app/http/validators/auth.validator.js +8 -0
  18. package/template/app/jobs/register.job.js +17 -0
  19. package/template/app/queue/register.queue.js +11 -0
  20. package/template/app/routes/api.route.js +18 -0
  21. package/template/app/routes/register.route.js +8 -0
  22. package/template/app/routes/web.route.js +7 -0
  23. package/template/app/socket/register.socket.js +15 -0
  24. package/template/config/app.config.js +12 -0
  25. package/template/config/bcrypt.config.js +7 -0
  26. package/template/config/cache.config.js +7 -0
  27. package/template/config/cors.config.js +37 -0
  28. package/template/config/cron.config.js +9 -0
  29. package/template/config/database.config.js +51 -0
  30. package/template/config/express.config.js +37 -0
  31. package/template/config/index.js +19 -0
  32. package/template/config/jwt.config.js +10 -0
  33. package/template/config/logger.config.js +10 -0
  34. package/template/config/mail.config.js +14 -0
  35. package/template/config/queue.config.js +11 -0
  36. package/template/config/redis.config.js +32 -0
  37. package/template/config/runtime.config.js +11 -0
  38. package/template/config/socket.config.js +15 -0
  39. package/template/config/storage.config.js +10 -0
  40. package/template/core/bootstrap.core.js +92 -0
  41. package/template/core/common/array.js +144 -0
  42. package/template/core/common/cache.js +100 -0
  43. package/template/core/common/collection.js +173 -0
  44. package/template/core/common/crypt.js +69 -0
  45. package/template/core/common/date.js +254 -0
  46. package/template/core/common/hash.js +61 -0
  47. package/template/core/common/object.js +155 -0
  48. package/template/core/common/path.js +80 -0
  49. package/template/core/common/storage.js +97 -0
  50. package/template/core/common/string.js +137 -0
  51. package/template/core/common/url.js +81 -0
  52. package/template/core/common.core.js +93 -0
  53. package/template/core/cron.core.js +141 -0
  54. package/template/core/database.core.js +113 -0
  55. package/template/core/error.core.js +83 -0
  56. package/template/core/express.core.js +161 -0
  57. package/template/core/hooks.core.js +47 -0
  58. package/template/core/jwt.core.js +81 -0
  59. package/template/core/logger.core.js +100 -0
  60. package/template/core/mailer.core.js +65 -0
  61. package/template/core/queue.core.js +226 -0
  62. package/template/core/redis.core.js +75 -0
  63. package/template/core/register.core.js +91 -0
  64. package/template/core/runtime.core.js +27 -0
  65. package/template/core/socket.core.js +93 -0
  66. package/template/core/validator.core.js +34 -0
  67. package/template/database/.gitkeep +0 -0
  68. package/template/database/models/post.model.js +26 -0
  69. package/template/database/models/user.model.js +30 -0
  70. package/template/docs/Bootstrap.md +50 -0
  71. package/template/docs/Common.md +48 -0
  72. package/template/docs/Cron.md +47 -0
  73. package/template/docs/Database.md +61 -0
  74. package/template/docs/Express.md +63 -0
  75. package/template/docs/Hooks.md +48 -0
  76. package/template/docs/Jwt.md +63 -0
  77. package/template/docs/Logger.md +60 -0
  78. package/template/docs/Mailer.md +50 -0
  79. package/template/docs/Queue.md +57 -0
  80. package/template/docs/README.md +32 -0
  81. package/template/docs/Redis.md +54 -0
  82. package/template/docs/Register.md +52 -0
  83. package/template/docs/Socket.md +55 -0
  84. package/template/docs/Validator.md +45 -0
  85. package/template/ecosystem.config.js +54 -0
  86. package/template/index.js +6 -0
  87. package/template/jest.config.js +27 -0
  88. package/template/jsconfig.json +37 -0
  89. package/template/logs/.gitkeep +0 -0
  90. package/template/nodemon.json +23 -0
  91. package/template/package-lock.json +7033 -0
  92. package/template/package.json +82 -0
  93. package/template/scripts/cli.js +258 -0
  94. package/template/storage/.gitkeep +0 -0
  95. package/template/tests/common.test.js +45 -0
  96. package/template/tests/crypt-hash-storage.test.js +44 -0
  97. package/template/tests/jwt.test.js +45 -0
  98. package/template/tests/register.test.js +55 -0
  99. package/template/tests/setup.js +11 -0
  100. package/template/tests/validator.test.js +65 -0
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('crypto')
4
+
5
+ const ALGORITHM = 'aes-256-gcm'
6
+ const IV_LENGTH = 12
7
+ const SALT_LENGTH = 16
8
+ const KEY_LENGTH = 32
9
+
10
+ // Derive a stable 32-byte key from the application secret.
11
+ function deriveKey(salt) {
12
+ const config = require('@config/index')
13
+ const secret = config.app.key || config.jwt.secret
14
+ return crypto.scryptSync(String(secret), salt, KEY_LENGTH)
15
+ }
16
+
17
+ // Authenticated symmetric encryption (AES-256-GCM) for at-rest secrets.
18
+ class Crypt {
19
+ // Encrypt a value. Objects are JSON-encoded. Returns a compact base64 token.
20
+ static encrypt(value) {
21
+ const plaintext = typeof value === 'string' ? value : JSON.stringify(value)
22
+ const salt = crypto.randomBytes(SALT_LENGTH)
23
+ const iv = crypto.randomBytes(IV_LENGTH)
24
+ const key = deriveKey(salt)
25
+
26
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
27
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
28
+ const tag = cipher.getAuthTag()
29
+
30
+ // Layout: salt | iv | authTag | ciphertext
31
+ return Buffer.concat([salt, iv, tag, encrypted]).toString('base64')
32
+ }
33
+
34
+ // Decrypt a token produced by encrypt(). Pass asJson to revive objects.
35
+ static decrypt(token, asJson = false) {
36
+ const data = Buffer.from(String(token), 'base64')
37
+ const salt = data.subarray(0, SALT_LENGTH)
38
+ const iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
39
+ const tag = data.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + 16)
40
+ const ciphertext = data.subarray(SALT_LENGTH + IV_LENGTH + 16)
41
+ const key = deriveKey(salt)
42
+
43
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
44
+ decipher.setAuthTag(tag)
45
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
46
+
47
+ return asJson ? JSON.parse(decrypted) : decrypted
48
+ }
49
+
50
+ // Plain base64 encode/decode helpers (encoding only, not encryption).
51
+ static base64Encode(value) {
52
+ return Buffer.from(String(value), 'utf8').toString('base64')
53
+ }
54
+
55
+ static base64Decode(value) {
56
+ return Buffer.from(String(value), 'base64').toString('utf8')
57
+ }
58
+
59
+ // URL-safe base64 variants.
60
+ static base64UrlEncode(value) {
61
+ return Buffer.from(String(value), 'utf8').toString('base64url')
62
+ }
63
+
64
+ static base64UrlDecode(value) {
65
+ return Buffer.from(String(value), 'base64url').toString('utf8')
66
+ }
67
+ }
68
+
69
+ module.exports = Crypt
@@ -0,0 +1,254 @@
1
+ 'use strict'
2
+
3
+ const MS = {
4
+ second: 1000,
5
+ minute: 60 * 1000,
6
+ hour: 60 * 60 * 1000,
7
+ day: 24 * 60 * 60 * 1000,
8
+ week: 7 * 24 * 60 * 60 * 1000,
9
+ }
10
+
11
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
12
+ const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
13
+
14
+ const pad = (n, len = 2) => String(Math.abs(n)).padStart(len, '0')
15
+
16
+ // A lightweight, chainable date wrapper in the spirit of Carbon / dayjs.
17
+ // Instances are immutable: arithmetic methods return a fresh DateTime.
18
+ class DateTime {
19
+ constructor(input = null) {
20
+ if (input instanceof DateTime) {
21
+ this._date = new Date(input._date.getTime())
22
+ } else if (input instanceof Date) {
23
+ this._date = new Date(input.getTime())
24
+ } else if (input === null || input === undefined) {
25
+ this._date = new Date()
26
+ } else {
27
+ this._date = new Date(input)
28
+ }
29
+ }
30
+
31
+ // ── Factories ────────────────────────────────────────────────────────────
32
+ static now() {
33
+ return new DateTime()
34
+ }
35
+
36
+ static parse(input) {
37
+ return new DateTime(input)
38
+ }
39
+
40
+ static create(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
41
+ return new DateTime(new Date(year, month - 1, day, hour, minute, second))
42
+ }
43
+
44
+ static fromTimestamp(seconds) {
45
+ return new DateTime(new Date(seconds * 1000))
46
+ }
47
+
48
+ // ── Accessors ────────────────────────────────────────────────────────────
49
+ year() {
50
+ return this._date.getFullYear()
51
+ }
52
+ month() {
53
+ return this._date.getMonth() + 1
54
+ }
55
+ day() {
56
+ return this._date.getDate()
57
+ }
58
+ hour() {
59
+ return this._date.getHours()
60
+ }
61
+ minute() {
62
+ return this._date.getMinutes()
63
+ }
64
+ second() {
65
+ return this._date.getSeconds()
66
+ }
67
+ dayOfWeek() {
68
+ return this._date.getDay()
69
+ }
70
+ timestamp() {
71
+ return Math.floor(this._date.getTime() / 1000)
72
+ }
73
+ valueOf() {
74
+ return this._date.getTime()
75
+ }
76
+ toDate() {
77
+ return new Date(this._date.getTime())
78
+ }
79
+ clone() {
80
+ return new DateTime(this._date)
81
+ }
82
+
83
+ // ── Arithmetic (immutable) ───────────────────────────────────────────────
84
+ _add(ms) {
85
+ return new DateTime(new Date(this._date.getTime() + ms))
86
+ }
87
+
88
+ addSeconds(n) {
89
+ return this._add(n * MS.second)
90
+ }
91
+ subSeconds(n) {
92
+ return this._add(-n * MS.second)
93
+ }
94
+ addMinutes(n) {
95
+ return this._add(n * MS.minute)
96
+ }
97
+ subMinutes(n) {
98
+ return this._add(-n * MS.minute)
99
+ }
100
+ addHours(n) {
101
+ return this._add(n * MS.hour)
102
+ }
103
+ subHours(n) {
104
+ return this._add(-n * MS.hour)
105
+ }
106
+ addDays(n) {
107
+ return this._add(n * MS.day)
108
+ }
109
+ subDays(n) {
110
+ return this._add(-n * MS.day)
111
+ }
112
+ addWeeks(n) {
113
+ return this._add(n * MS.week)
114
+ }
115
+ subWeeks(n) {
116
+ return this._add(-n * MS.week)
117
+ }
118
+
119
+ addMonths(n) {
120
+ const d = new Date(this._date.getTime())
121
+ d.setMonth(d.getMonth() + n)
122
+ return new DateTime(d)
123
+ }
124
+
125
+ subMonths(n) {
126
+ return this.addMonths(-n)
127
+ }
128
+
129
+ addYears(n) {
130
+ const d = new Date(this._date.getTime())
131
+ d.setFullYear(d.getFullYear() + n)
132
+ return new DateTime(d)
133
+ }
134
+
135
+ subYears(n) {
136
+ return this.addYears(-n)
137
+ }
138
+
139
+ // ── Boundaries ───────────────────────────────────────────────────────────
140
+ startOfDay() {
141
+ const d = new Date(this._date.getTime())
142
+ d.setHours(0, 0, 0, 0)
143
+ return new DateTime(d)
144
+ }
145
+
146
+ endOfDay() {
147
+ const d = new Date(this._date.getTime())
148
+ d.setHours(23, 59, 59, 999)
149
+ return new DateTime(d)
150
+ }
151
+
152
+ // ── Comparisons ──────────────────────────────────────────────────────────
153
+ isBefore(other) {
154
+ return this.valueOf() < new DateTime(other).valueOf()
155
+ }
156
+ isAfter(other) {
157
+ return this.valueOf() > new DateTime(other).valueOf()
158
+ }
159
+ isSame(other) {
160
+ return this.valueOf() === new DateTime(other).valueOf()
161
+ }
162
+ isPast() {
163
+ return this.valueOf() < Date.now()
164
+ }
165
+ isFuture() {
166
+ return this.valueOf() > Date.now()
167
+ }
168
+
169
+ // Difference from another date, in the requested unit (signed).
170
+ diff(other, unit = 'seconds') {
171
+ const ms = this.valueOf() - new DateTime(other).valueOf()
172
+ switch (unit) {
173
+ case 'seconds':
174
+ return Math.trunc(ms / MS.second)
175
+ case 'minutes':
176
+ return Math.trunc(ms / MS.minute)
177
+ case 'hours':
178
+ return Math.trunc(ms / MS.hour)
179
+ case 'days':
180
+ return Math.trunc(ms / MS.day)
181
+ case 'weeks':
182
+ return Math.trunc(ms / MS.week)
183
+ default:
184
+ return ms
185
+ }
186
+ }
187
+
188
+ // Human friendly relative phrasing: "3 hours ago", "in 2 days".
189
+ diffForHumans(other = null) {
190
+ const ref = other ? new DateTime(other).valueOf() : Date.now()
191
+ const ms = this.valueOf() - ref
192
+ const abs = Math.abs(ms)
193
+ const units = [
194
+ ['year', MS.day * 365],
195
+ ['month', MS.day * 30],
196
+ ['day', MS.day],
197
+ ['hour', MS.hour],
198
+ ['minute', MS.minute],
199
+ ['second', MS.second],
200
+ ]
201
+ for (const [name, size] of units) {
202
+ const value = Math.floor(abs / size)
203
+ if (value >= 1) {
204
+ const label = `${value} ${name}${value > 1 ? 's' : ''}`
205
+ return ms < 0 ? `${label} ago` : `in ${label}`
206
+ }
207
+ }
208
+ return 'just now'
209
+ }
210
+
211
+ // ── Formatting ───────────────────────────────────────────────────────────
212
+ // Token format: YYYY MM DD HH mm ss, plus MMMM/MMM/dddd/ddd/A/a.
213
+ format(pattern = 'YYYY-MM-DD HH:mm:ss') {
214
+ const d = this._date
215
+ const h12 = d.getHours() % 12 || 12
216
+ const map = {
217
+ YYYY: d.getFullYear(),
218
+ YY: pad(d.getFullYear() % 100),
219
+ MMMM: MONTHS[d.getMonth()],
220
+ MMM: MONTHS[d.getMonth()].slice(0, 3),
221
+ MM: pad(d.getMonth() + 1),
222
+ M: d.getMonth() + 1,
223
+ DD: pad(d.getDate()),
224
+ D: d.getDate(),
225
+ dddd: DAYS[d.getDay()],
226
+ ddd: DAYS[d.getDay()].slice(0, 3),
227
+ HH: pad(d.getHours()),
228
+ H: d.getHours(),
229
+ hh: pad(h12),
230
+ h: h12,
231
+ mm: pad(d.getMinutes()),
232
+ m: d.getMinutes(),
233
+ ss: pad(d.getSeconds()),
234
+ s: d.getSeconds(),
235
+ A: d.getHours() < 12 ? 'AM' : 'PM',
236
+ a: d.getHours() < 12 ? 'am' : 'pm',
237
+ }
238
+ return pattern.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g, (token) => map[token])
239
+ }
240
+
241
+ toISOString() {
242
+ return this._date.toISOString()
243
+ }
244
+
245
+ toString() {
246
+ return this.format('YYYY-MM-DD HH:mm:ss')
247
+ }
248
+
249
+ toJSON() {
250
+ return this._date.toISOString()
251
+ }
252
+ }
253
+
254
+ module.exports = DateTime
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('crypto')
4
+ const bcrypt = require('bcrypt')
5
+
6
+ // Hashing utilities: password hashing via bcrypt plus raw digest helpers.
7
+ class Hash {
8
+ // Hash a plaintext value with bcrypt. Salt rounds come from config.
9
+ static async make(value, rounds = null) {
10
+ const config = require('@config/index')
11
+ const saltRounds = rounds ?? config.bcrypt.saltRounds
12
+ return bcrypt.hash(String(value), saltRounds)
13
+ }
14
+
15
+ // Verify a plaintext value against a bcrypt hash.
16
+ static async check(value, hashed) {
17
+ if (!hashed) return false
18
+ return bcrypt.compare(String(value), hashed)
19
+ }
20
+
21
+ // Detect whether a stored hash should be re-hashed at higher cost.
22
+ static needsRehash(hashed, rounds = null) {
23
+ const config = require('@config/index')
24
+ const target = rounds ?? config.bcrypt.saltRounds
25
+ const match = /^\$2[aby]\$(\d{2})\$/.exec(hashed || '')
26
+ if (!match) return true
27
+ return parseInt(match[1], 10) < target
28
+ }
29
+
30
+ // Generic digest (md5, sha1, sha256, sha512, ...).
31
+ static digest(value, algorithm = 'sha256', encoding = 'hex') {
32
+ return crypto.createHash(algorithm).update(String(value)).digest(encoding)
33
+ }
34
+
35
+ static md5(value) {
36
+ return Hash.digest(value, 'md5')
37
+ }
38
+
39
+ static sha256(value) {
40
+ return Hash.digest(value, 'sha256')
41
+ }
42
+
43
+ static sha512(value) {
44
+ return Hash.digest(value, 'sha512')
45
+ }
46
+
47
+ // Keyed HMAC digest.
48
+ static hmac(value, secret, algorithm = 'sha256') {
49
+ return crypto.createHmac(algorithm, secret).update(String(value)).digest('hex')
50
+ }
51
+
52
+ // Constant-time comparison to avoid timing attacks.
53
+ static equals(a, b) {
54
+ const bufA = Buffer.from(String(a))
55
+ const bufB = Buffer.from(String(b))
56
+ if (bufA.length !== bufB.length) return false
57
+ return crypto.timingSafeEqual(bufA, bufB)
58
+ }
59
+ }
60
+
61
+ module.exports = Hash
@@ -0,0 +1,155 @@
1
+ 'use strict'
2
+
3
+ // Object helpers with dot-notation access and immutable-friendly merges.
4
+ class Obj {
5
+ // Read a nested value by dot path: get(obj, 'a.b.c', fallback).
6
+ static get(obj, path, fallback = null) {
7
+ if (!obj || !path) return fallback
8
+ const keys = Array.isArray(path) ? path : String(path).split('.')
9
+ let current = obj
10
+ for (const key of keys) {
11
+ if (current === null || current === undefined || !(key in current)) {
12
+ return fallback
13
+ }
14
+ current = current[key]
15
+ }
16
+ return current === undefined ? fallback : current
17
+ }
18
+
19
+ // Write a nested value by dot path, creating intermediate objects.
20
+ static set(obj, path, value) {
21
+ const keys = Array.isArray(path) ? path : String(path).split('.')
22
+ let current = obj
23
+ for (let i = 0; i < keys.length - 1; i++) {
24
+ const key = keys[i]
25
+ if (typeof current[key] !== 'object' || current[key] === null) {
26
+ current[key] = {}
27
+ }
28
+ current = current[key]
29
+ }
30
+ current[keys[keys.length - 1]] = value
31
+ return obj
32
+ }
33
+
34
+ // True when the dot path exists.
35
+ static has(obj, path) {
36
+ const keys = Array.isArray(path) ? path : String(path).split('.')
37
+ let current = obj
38
+ for (const key of keys) {
39
+ if (current === null || current === undefined || !(key in current)) {
40
+ return false
41
+ }
42
+ current = current[key]
43
+ }
44
+ return true
45
+ }
46
+
47
+ // Delete a nested key by dot path.
48
+ static forget(obj, path) {
49
+ const keys = Array.isArray(path) ? path : String(path).split('.')
50
+ let current = obj
51
+ for (let i = 0; i < keys.length - 1; i++) {
52
+ if (typeof current[keys[i]] !== 'object' || current[keys[i]] === null) return obj
53
+ current = current[keys[i]]
54
+ }
55
+ delete current[keys[keys.length - 1]]
56
+ return obj
57
+ }
58
+
59
+ // New object containing only the listed keys.
60
+ static pick(obj, keys) {
61
+ const out = {}
62
+ for (const key of Arr(keys)) {
63
+ if (obj && key in obj) out[key] = obj[key]
64
+ }
65
+ return out
66
+ }
67
+
68
+ // New object without the listed keys.
69
+ static omit(obj, keys) {
70
+ const exclude = new Set(Arr(keys))
71
+ const out = {}
72
+ for (const key of Object.keys(obj || {})) {
73
+ if (!exclude.has(key)) out[key] = obj[key]
74
+ }
75
+ return out
76
+ }
77
+
78
+ // Recursive deep merge; later sources win.
79
+ static merge(target, ...sources) {
80
+ for (const source of sources) {
81
+ if (!source) continue
82
+ for (const key of Object.keys(source)) {
83
+ const sv = source[key]
84
+ const tv = target[key]
85
+ if (Obj.isPlain(sv) && Obj.isPlain(tv)) {
86
+ target[key] = Obj.merge({ ...tv }, sv)
87
+ } else {
88
+ target[key] = sv
89
+ }
90
+ }
91
+ }
92
+ return target
93
+ }
94
+
95
+ // Structured deep clone, with a JSON fallback for older runtimes.
96
+ static clone(obj) {
97
+ if (typeof structuredClone === 'function') {
98
+ try {
99
+ return structuredClone(obj)
100
+ } catch {
101
+ /* fall through to JSON clone */
102
+ }
103
+ }
104
+ return JSON.parse(JSON.stringify(obj))
105
+ }
106
+
107
+ static isPlain(value) {
108
+ if (typeof value !== 'object' || value === null) return false
109
+ const proto = Object.getPrototypeOf(value)
110
+ return proto === Object.prototype || proto === null
111
+ }
112
+
113
+ static isEmpty(value) {
114
+ if (value === null || value === undefined) return true
115
+ if (Array.isArray(value)) return value.length === 0
116
+ if (typeof value === 'object') return Object.keys(value).length === 0
117
+ return false
118
+ }
119
+
120
+ // Build an object from [key, value] entries.
121
+ static fromEntries(entries) {
122
+ return Object.fromEntries(entries)
123
+ }
124
+
125
+ // Map over values while keeping keys.
126
+ static mapValues(obj, fn) {
127
+ const out = {}
128
+ for (const [key, val] of Object.entries(obj || {})) {
129
+ out[key] = fn(val, key)
130
+ }
131
+ return out
132
+ }
133
+
134
+ // Flatten nested objects into dot-path keys.
135
+ static flatten(obj, prefix = '') {
136
+ const out = {}
137
+ for (const [key, val] of Object.entries(obj || {})) {
138
+ const path = prefix ? `${prefix}.${key}` : key
139
+ if (Obj.isPlain(val)) {
140
+ Object.assign(out, Obj.flatten(val, path))
141
+ } else {
142
+ out[path] = val
143
+ }
144
+ }
145
+ return out
146
+ }
147
+ }
148
+
149
+ // Local helper so pick/omit accept either a single key or an array.
150
+ function Arr(value) {
151
+ if (value === null || value === undefined) return []
152
+ return Array.isArray(value) ? value : [value]
153
+ }
154
+
155
+ module.exports = Obj
@@ -0,0 +1,80 @@
1
+ 'use strict'
2
+
3
+ const nodePath = require('path')
4
+ const fs = require('fs')
5
+
6
+ const ROOT = process.cwd()
7
+
8
+ // Path helpers anchored at the project root, plus a few filesystem niceties.
9
+ class Path {
10
+ // Absolute path from the project root.
11
+ static base(...segments) {
12
+ return nodePath.resolve(ROOT, ...segments)
13
+ }
14
+
15
+ static src(...segments) {
16
+ return Path.base('src', ...segments)
17
+ }
18
+
19
+ static core(...segments) {
20
+ return Path.base('core', ...segments)
21
+ }
22
+
23
+ static storage(...segments) {
24
+ return Path.base('storage', ...segments)
25
+ }
26
+
27
+ static public(...segments) {
28
+ return Path.base('public', ...segments)
29
+ }
30
+
31
+ static join(...segments) {
32
+ return nodePath.join(...segments)
33
+ }
34
+
35
+ static resolve(...segments) {
36
+ return nodePath.resolve(...segments)
37
+ }
38
+
39
+ static dirname(p) {
40
+ return nodePath.dirname(p)
41
+ }
42
+
43
+ static basename(p, ext) {
44
+ return nodePath.basename(p, ext)
45
+ }
46
+
47
+ // File extension without the leading dot.
48
+ static extension(p) {
49
+ return nodePath.extname(p).replace('.', '')
50
+ }
51
+
52
+ static filename(p) {
53
+ return nodePath.basename(p, nodePath.extname(p))
54
+ }
55
+
56
+ // Relative path from the project root, in posix form.
57
+ static relative(p) {
58
+ return nodePath.relative(ROOT, p).split(nodePath.sep).join('/')
59
+ }
60
+
61
+ static exists(p) {
62
+ return fs.existsSync(p)
63
+ }
64
+
65
+ static isFile(p) {
66
+ return fs.existsSync(p) && fs.statSync(p).isFile()
67
+ }
68
+
69
+ static isDir(p) {
70
+ return fs.existsSync(p) && fs.statSync(p).isDirectory()
71
+ }
72
+
73
+ // Create a directory (and parents) if it does not yet exist.
74
+ static ensureDir(p) {
75
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true })
76
+ return p
77
+ }
78
+ }
79
+
80
+ module.exports = Path
@@ -0,0 +1,97 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const fsp = require('fs/promises')
5
+ const nodePath = require('path')
6
+
7
+ // Resolve a path inside the configured storage root, guarding against escapes.
8
+ function resolvePath(relative) {
9
+ const config = require('@config/index')
10
+ const root = nodePath.resolve(process.cwd(), config.storage.root)
11
+ const full = nodePath.resolve(root, relative || '')
12
+ if (full !== root && !full.startsWith(root + nodePath.sep)) {
13
+ throw new Error(`Storage path escapes the storage root: ${relative}`)
14
+ }
15
+ return { root, full }
16
+ }
17
+
18
+ // Filesystem storage abstraction rooted at config.storage.root. All paths are
19
+ // relative to that root, so application code never deals in absolute paths.
20
+ class Storage {
21
+ // Absolute path for a relative storage key.
22
+ static path(relative = '') {
23
+ return resolvePath(relative).full
24
+ }
25
+
26
+ static exists(relative) {
27
+ return fs.existsSync(resolvePath(relative).full)
28
+ }
29
+
30
+ // Write contents, creating parent directories as needed.
31
+ static async put(relative, contents) {
32
+ const { full } = resolvePath(relative)
33
+ await fsp.mkdir(nodePath.dirname(full), { recursive: true })
34
+ await fsp.writeFile(full, contents)
35
+ return relative
36
+ }
37
+
38
+ static async get(relative, encoding = 'utf8') {
39
+ return fsp.readFile(resolvePath(relative).full, encoding)
40
+ }
41
+
42
+ static async getBuffer(relative) {
43
+ return fsp.readFile(resolvePath(relative).full)
44
+ }
45
+
46
+ static async append(relative, contents) {
47
+ const { full } = resolvePath(relative)
48
+ await fsp.mkdir(nodePath.dirname(full), { recursive: true })
49
+ await fsp.appendFile(full, contents)
50
+ return relative
51
+ }
52
+
53
+ static async delete(relative) {
54
+ const { full } = resolvePath(relative)
55
+ if (!fs.existsSync(full)) return false
56
+ await fsp.rm(full, { recursive: true, force: true })
57
+ return true
58
+ }
59
+
60
+ static async copy(from, to) {
61
+ const src = resolvePath(from).full
62
+ const dest = resolvePath(to).full
63
+ await fsp.mkdir(nodePath.dirname(dest), { recursive: true })
64
+ await fsp.copyFile(src, dest)
65
+ return to
66
+ }
67
+
68
+ static async move(from, to) {
69
+ const src = resolvePath(from).full
70
+ const dest = resolvePath(to).full
71
+ await fsp.mkdir(nodePath.dirname(dest), { recursive: true })
72
+ await fsp.rename(src, dest)
73
+ return to
74
+ }
75
+
76
+ static async makeDir(relative) {
77
+ await fsp.mkdir(resolvePath(relative).full, { recursive: true })
78
+ return relative
79
+ }
80
+
81
+ // List entries in a directory (names only).
82
+ static async list(relative = '') {
83
+ const { full } = resolvePath(relative)
84
+ if (!fs.existsSync(full)) return []
85
+ return fsp.readdir(full)
86
+ }
87
+
88
+ // File size in bytes, or null when missing.
89
+ static async size(relative) {
90
+ const { full } = resolvePath(relative)
91
+ if (!fs.existsSync(full)) return null
92
+ const stat = await fsp.stat(full)
93
+ return stat.size
94
+ }
95
+ }
96
+
97
+ module.exports = Storage