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.
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/bin/create-rebe.js +310 -0
- package/package.json +40 -0
- package/template/.env.example +141 -0
- package/template/.gitattributes +16 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +10 -0
- package/template/.prettierrc +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +150 -0
- package/template/SECURITY.md +133 -0
- package/template/app/hooks/register.hook.js +22 -0
- package/template/app/http/controllers/auth.controller.js +34 -0
- package/template/app/http/middlewares/auth.middleware.js +25 -0
- package/template/app/http/middlewares/register.middleware.js +18 -0
- package/template/app/http/validators/auth.validator.js +8 -0
- package/template/app/jobs/register.job.js +17 -0
- package/template/app/queue/register.queue.js +11 -0
- package/template/app/routes/api.route.js +18 -0
- package/template/app/routes/register.route.js +8 -0
- package/template/app/routes/web.route.js +7 -0
- package/template/app/socket/register.socket.js +15 -0
- package/template/config/app.config.js +12 -0
- package/template/config/bcrypt.config.js +7 -0
- package/template/config/cache.config.js +7 -0
- package/template/config/cors.config.js +37 -0
- package/template/config/cron.config.js +9 -0
- package/template/config/database.config.js +51 -0
- package/template/config/express.config.js +37 -0
- package/template/config/index.js +19 -0
- package/template/config/jwt.config.js +10 -0
- package/template/config/logger.config.js +10 -0
- package/template/config/mail.config.js +14 -0
- package/template/config/queue.config.js +11 -0
- package/template/config/redis.config.js +32 -0
- package/template/config/runtime.config.js +11 -0
- package/template/config/socket.config.js +15 -0
- package/template/config/storage.config.js +10 -0
- package/template/core/bootstrap.core.js +92 -0
- package/template/core/common/array.js +144 -0
- package/template/core/common/cache.js +100 -0
- package/template/core/common/collection.js +173 -0
- package/template/core/common/crypt.js +69 -0
- package/template/core/common/date.js +254 -0
- package/template/core/common/hash.js +61 -0
- package/template/core/common/object.js +155 -0
- package/template/core/common/path.js +80 -0
- package/template/core/common/storage.js +97 -0
- package/template/core/common/string.js +137 -0
- package/template/core/common/url.js +81 -0
- package/template/core/common.core.js +93 -0
- package/template/core/cron.core.js +141 -0
- package/template/core/database.core.js +113 -0
- package/template/core/error.core.js +83 -0
- package/template/core/express.core.js +161 -0
- package/template/core/hooks.core.js +47 -0
- package/template/core/jwt.core.js +81 -0
- package/template/core/logger.core.js +100 -0
- package/template/core/mailer.core.js +65 -0
- package/template/core/queue.core.js +226 -0
- package/template/core/redis.core.js +75 -0
- package/template/core/register.core.js +91 -0
- package/template/core/runtime.core.js +27 -0
- package/template/core/socket.core.js +93 -0
- package/template/core/validator.core.js +34 -0
- package/template/database/.gitkeep +0 -0
- package/template/database/models/post.model.js +26 -0
- package/template/database/models/user.model.js +30 -0
- package/template/docs/Bootstrap.md +50 -0
- package/template/docs/Common.md +48 -0
- package/template/docs/Cron.md +47 -0
- package/template/docs/Database.md +61 -0
- package/template/docs/Express.md +63 -0
- package/template/docs/Hooks.md +48 -0
- package/template/docs/Jwt.md +63 -0
- package/template/docs/Logger.md +60 -0
- package/template/docs/Mailer.md +50 -0
- package/template/docs/Queue.md +57 -0
- package/template/docs/README.md +32 -0
- package/template/docs/Redis.md +54 -0
- package/template/docs/Register.md +52 -0
- package/template/docs/Socket.md +55 -0
- package/template/docs/Validator.md +45 -0
- package/template/ecosystem.config.js +54 -0
- package/template/index.js +6 -0
- package/template/jest.config.js +27 -0
- package/template/jsconfig.json +37 -0
- package/template/logs/.gitkeep +0 -0
- package/template/nodemon.json +23 -0
- package/template/package-lock.json +7033 -0
- package/template/package.json +82 -0
- package/template/scripts/cli.js +258 -0
- package/template/storage/.gitkeep +0 -0
- package/template/tests/common.test.js +45 -0
- package/template/tests/crypt-hash-storage.test.js +44 -0
- package/template/tests/jwt.test.js +45 -0
- package/template/tests/register.test.js +55 -0
- package/template/tests/setup.js +11 -0
- 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
|