@volcanicminds/backend 2.3.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -28
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -36
- package/dist/index.js.map +1 -1
- package/dist/lib/api/auth/controller/auth.d.ts.map +1 -1
- package/dist/lib/api/auth/controller/auth.js +14 -7
- package/dist/lib/api/auth/controller/auth.js.map +1 -1
- package/dist/lib/api/health/controller/health.d.ts +1 -1
- package/dist/lib/api/health/controller/health.d.ts.map +1 -1
- package/dist/lib/api/health/controller/health.js +1 -1
- package/dist/lib/api/health/controller/health.js.map +1 -1
- package/dist/lib/api/health/routes.d.ts +1 -0
- package/dist/lib/api/health/routes.d.ts.map +1 -1
- package/dist/lib/api/health/routes.js +1 -0
- package/dist/lib/api/health/routes.js.map +1 -1
- package/dist/lib/api/tenants/controller/tenants.d.ts +17 -0
- package/dist/lib/api/tenants/controller/tenants.d.ts.map +1 -0
- package/dist/lib/api/tenants/controller/tenants.js +150 -0
- package/dist/lib/api/tenants/controller/tenants.js.map +1 -0
- package/dist/lib/api/tenants/routes.d.ts +167 -0
- package/dist/lib/api/tenants/routes.d.ts.map +1 -0
- package/dist/lib/api/tenants/routes.js +143 -0
- package/dist/lib/api/tenants/routes.js.map +1 -0
- package/dist/lib/config/plugins.js +1 -1
- package/dist/lib/config/plugins.js.map +1 -1
- package/dist/lib/database/typeorm/entities/change.d.ts +11 -0
- package/dist/lib/database/typeorm/entities/change.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/change.js +4 -0
- package/dist/lib/database/typeorm/entities/change.js.map +1 -0
- package/dist/lib/database/typeorm/entities/tenant.d.ts +13 -0
- package/dist/lib/database/typeorm/entities/tenant.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/tenant.js +4 -0
- package/dist/lib/database/typeorm/entities/tenant.js.map +1 -0
- package/dist/lib/database/typeorm/entities/token.d.ts +16 -0
- package/dist/lib/database/typeorm/entities/token.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/token.js +4 -0
- package/dist/lib/database/typeorm/entities/token.js.map +1 -0
- package/dist/lib/database/typeorm/entities/user.d.ts +28 -0
- package/dist/lib/database/typeorm/entities/user.d.ts.map +1 -0
- package/dist/lib/database/typeorm/entities/user.js +4 -0
- package/dist/lib/database/typeorm/entities/user.js.map +1 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts +6 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.js +73 -0
- package/dist/lib/database/typeorm/loader/dataBaseManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/entities.d.ts +6 -0
- package/dist/lib/database/typeorm/loader/entities.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/entities.js +37 -0
- package/dist/lib/database/typeorm/loader/entities.js.map +1 -0
- package/dist/lib/database/typeorm/loader/tenantManager.d.ts +17 -0
- package/dist/lib/database/typeorm/loader/tenantManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/tenantManager.js +193 -0
- package/dist/lib/database/typeorm/loader/tenantManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/tokenManager.d.ts +23 -0
- package/dist/lib/database/typeorm/loader/tokenManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/tokenManager.js +131 -0
- package/dist/lib/database/typeorm/loader/tokenManager.js.map +1 -0
- package/dist/lib/database/typeorm/loader/userManager.d.ts +39 -0
- package/dist/lib/database/typeorm/loader/userManager.d.ts.map +1 -0
- package/dist/lib/database/typeorm/loader/userManager.js +358 -0
- package/dist/lib/database/typeorm/loader/userManager.js.map +1 -0
- package/dist/lib/database/typeorm/query/builder.d.ts +2 -0
- package/dist/lib/database/typeorm/query/builder.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query/builder.js +33 -0
- package/dist/lib/database/typeorm/query/builder.js.map +1 -0
- package/dist/lib/database/typeorm/query/parser.d.ts +2 -0
- package/dist/lib/database/typeorm/query/parser.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query/parser.js +27 -0
- package/dist/lib/database/typeorm/query/parser.js.map +1 -0
- package/dist/lib/database/typeorm/query.d.ts +36 -0
- package/dist/lib/database/typeorm/query.d.ts.map +1 -0
- package/dist/lib/database/typeorm/query.js +259 -0
- package/dist/lib/database/typeorm/query.js.map +1 -0
- package/dist/lib/database/typeorm/util/crypto.d.ts +3 -0
- package/dist/lib/database/typeorm/util/crypto.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/crypto.js +46 -0
- package/dist/lib/database/typeorm/util/crypto.js.map +1 -0
- package/dist/lib/database/typeorm/util/error.d.ts +5 -0
- package/dist/lib/database/typeorm/util/error.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/error.js +8 -0
- package/dist/lib/database/typeorm/util/error.js.map +1 -0
- package/dist/lib/database/typeorm/util/logger.d.ts +7 -0
- package/dist/lib/database/typeorm/util/logger.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/logger.js +31 -0
- package/dist/lib/database/typeorm/util/logger.js.map +1 -0
- package/dist/lib/database/typeorm/util/yn.d.ts +2 -0
- package/dist/lib/database/typeorm/util/yn.d.ts.map +1 -0
- package/dist/lib/database/typeorm/util/yn.js +15 -0
- package/dist/lib/database/typeorm/util/yn.js.map +1 -0
- package/dist/lib/defaults/managers.d.ts.map +1 -1
- package/dist/lib/defaults/managers.js +186 -58
- package/dist/lib/defaults/managers.js.map +1 -1
- package/dist/lib/hooks/onRequest.d.ts.map +1 -1
- package/dist/lib/hooks/onRequest.js +11 -48
- package/dist/lib/hooks/onRequest.js.map +1 -1
- package/dist/lib/loader/router.d.ts.map +1 -1
- package/dist/lib/loader/router.js +92 -83
- package/dist/lib/loader/router.js.map +1 -1
- package/dist/lib/loader/tenant.d.ts.map +1 -1
- package/dist/lib/loader/tenant.js +18 -1
- package/dist/lib/loader/tenant.js.map +1 -1
- package/dist/lib/locales/en.json +12 -0
- package/dist/lib/locales/it.json +12 -0
- package/dist/lib/schemas/tenant.d.ts +88 -0
- package/dist/lib/schemas/tenant.d.ts.map +1 -0
- package/dist/lib/schemas/tenant.js +45 -0
- package/dist/lib/schemas/tenant.js.map +1 -0
- package/dist/lib/util/regexp.d.ts +2 -0
- package/dist/lib/util/regexp.d.ts.map +1 -1
- package/dist/lib/util/regexp.js +5 -3
- package/dist/lib/util/regexp.js.map +1 -1
- package/dist/lib/util/secret.d.ts +11 -0
- package/dist/lib/util/secret.d.ts.map +1 -0
- package/dist/lib/util/secret.js +70 -0
- package/dist/lib/util/secret.js.map +1 -0
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/typeorm.d.ts +15 -0
- package/dist/typeorm.d.ts.map +1 -0
- package/dist/typeorm.js +86 -0
- package/dist/typeorm.js.map +1 -0
- package/dist/types/database/typeorm/global.d.ts +56 -0
- package/dist/types/database/typeorm/global.d.ts.map +1 -0
- package/dist/types/database/typeorm/global.js +2 -0
- package/dist/types/database/typeorm/global.js.map +1 -0
- package/dist/types/global.d.ts +318 -0
- package/dist/types/orm.d.ts +22 -0
- package/lib/api/auth/controller/auth.ts +28 -12
- package/lib/api/health/controller/health.ts +1 -1
- package/lib/api/health/routes.ts +1 -0
- package/lib/api/tenants/controller/tenants.ts +211 -0
- package/lib/api/tenants/routes.ts +145 -0
- package/lib/config/plugins.ts +1 -1
- package/lib/database/typeorm/entities/change.ts +11 -0
- package/lib/database/typeorm/entities/tenant.ts +20 -0
- package/lib/database/typeorm/entities/token.ts +18 -0
- package/lib/database/typeorm/entities/user.ts +33 -0
- package/lib/database/typeorm/loader/dataBaseManager.ts +89 -0
- package/lib/database/typeorm/loader/entities.ts +47 -0
- package/lib/database/typeorm/loader/tenantManager.ts +260 -0
- package/lib/database/typeorm/loader/tokenManager.ts +157 -0
- package/lib/database/typeorm/loader/userManager.ts +432 -0
- package/lib/database/typeorm/query/builder.ts +39 -0
- package/lib/database/typeorm/query/parser.ts +29 -0
- package/lib/database/typeorm/query.ts +326 -0
- package/lib/database/typeorm/util/crypto.ts +52 -0
- package/lib/database/typeorm/util/error.ts +7 -0
- package/lib/database/typeorm/util/logger.ts +35 -0
- package/lib/database/typeorm/util/yn.ts +20 -0
- package/lib/defaults/managers.ts +191 -64
- package/lib/hooks/onRequest.ts +12 -64
- package/lib/loader/router.ts +138 -112
- package/lib/loader/tenant.ts +29 -13
- package/lib/schemas/tenant.ts +49 -0
- package/lib/util/regexp.ts +31 -8
- package/lib/util/secret.ts +113 -0
- package/package.json +56 -15
- package/dist/lib/apollo/context.d.ts +0 -6
- package/dist/lib/apollo/context.d.ts.map +0 -1
- package/dist/lib/apollo/context.js +0 -5
- package/dist/lib/apollo/context.js.map +0 -1
- package/dist/lib/apollo/resolvers.d.ts +0 -8
- package/dist/lib/apollo/resolvers.d.ts.map +0 -1
- package/dist/lib/apollo/resolvers.js +0 -8
- package/dist/lib/apollo/resolvers.js.map +0 -1
- package/dist/lib/apollo/type-defs.d.ts +0 -3
- package/dist/lib/apollo/type-defs.d.ts.map +0 -1
- package/dist/lib/apollo/type-defs.js +0 -8
- package/dist/lib/apollo/type-defs.js.map +0 -1
- package/lib/apollo/context.ts +0 -11
- package/lib/apollo/resolvers.ts +0 -12
- package/lib/apollo/type-defs.ts +0 -9
package/lib/loader/tenant.ts
CHANGED
|
@@ -6,7 +6,7 @@ export async function apply(server: FastifyInstance) {
|
|
|
6
6
|
|
|
7
7
|
// Se multi-tenant non è abilitato, usciamo subito e iniettiamo il contesto single-tenant
|
|
8
8
|
if (!multi_tenant?.enabled) {
|
|
9
|
-
if (log.i) log.info('Multi-Tenant: Disabled
|
|
9
|
+
if (log.i) log.info('Multi-Tenant: Disabled (Using single-tenant DB context)')
|
|
10
10
|
|
|
11
11
|
server.addHook('onRequest', async (req) => {
|
|
12
12
|
const dataSource = global.connection
|
|
@@ -25,23 +25,33 @@ export async function apply(server: FastifyInstance) {
|
|
|
25
25
|
// Recuperiamo il gestore tenant iniettato o di default
|
|
26
26
|
const tm = server['tenantManager'] as TenantManagement
|
|
27
27
|
|
|
28
|
+
// Check if route opts out of tenant context
|
|
29
|
+
const cfg = (req.routeOptions?.config as { tenantContext?: boolean }) || {}
|
|
30
|
+
if (cfg.tenantContext === false) {
|
|
31
|
+
if (log.t) log.trace(`Multi-Tenant: Route ${req.url} opted out of tenant context via config`)
|
|
32
|
+
// Inject global DB context (public schema) like single-tenant
|
|
33
|
+
const dataSource = global.connection
|
|
34
|
+
if (dataSource) req.db = dataSource.manager
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
// Controllo critico: se è abilitato il MT, DEVE esserci un manager implementato
|
|
29
39
|
if (!tm || !tm.isImplemented()) {
|
|
30
40
|
const errorMsg = 'Multi-Tenant enabled but no TenantManager provided/implemented!'
|
|
31
41
|
if (log.f) log.fatal(errorMsg)
|
|
32
|
-
throw new Error(errorMsg)
|
|
42
|
+
throw new Error(errorMsg)
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
try {
|
|
36
46
|
// 1. Risoluzione Tenant
|
|
37
47
|
const tenant = await tm.resolveTenant(req)
|
|
38
|
-
|
|
48
|
+
|
|
39
49
|
if (!tenant) {
|
|
40
50
|
if (log.w) log.warn(`Multi-Tenant: Tenant resolution failed for request ${req.id}`)
|
|
41
|
-
return reply.code(404).send({
|
|
42
|
-
statusCode: 404,
|
|
43
|
-
error: 'Not Found',
|
|
44
|
-
message: 'Tenant not found or resolution failed'
|
|
51
|
+
return reply.code(404).send({
|
|
52
|
+
statusCode: 404,
|
|
53
|
+
error: 'Not Found',
|
|
54
|
+
message: 'Tenant not found or resolution failed'
|
|
45
55
|
})
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -55,9 +65,10 @@ export async function apply(server: FastifyInstance) {
|
|
|
55
65
|
if (dataSource) {
|
|
56
66
|
const qr = dataSource.createQueryRunner()
|
|
57
67
|
await qr.connect()
|
|
58
|
-
|
|
68
|
+
|
|
59
69
|
// Assegnamo il manager del QueryRunner alla richiesta
|
|
60
70
|
req.db = qr.manager
|
|
71
|
+
req.runner = qr
|
|
61
72
|
|
|
62
73
|
// 4. Switch Schema su QUESTO QueryRunner
|
|
63
74
|
// Passiamo il manager affinché il TenantManager possa eseguire "SET search_path" su questa connessione specifica
|
|
@@ -66,20 +77,25 @@ export async function apply(server: FastifyInstance) {
|
|
|
66
77
|
// 5. Cleanup: Rilascio del QueryRunner alla fine della richiesta
|
|
67
78
|
reply.raw.on('finish', async () => {
|
|
68
79
|
if (!qr.isReleased) {
|
|
80
|
+
try {
|
|
81
|
+
// Vital: Reset search_path to public avoids polluting the connection pool
|
|
82
|
+
await qr.query('SET search_path TO public')
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (log.e) log.error(`Multi-Tenant: Failed to reset search_path: ${err}`)
|
|
85
|
+
}
|
|
69
86
|
await qr.release()
|
|
70
87
|
}
|
|
71
88
|
})
|
|
72
89
|
} else {
|
|
73
90
|
if (log.w) log.warn('Multi-Tenant: Global connection not found! Skipping DB context creation.')
|
|
74
91
|
}
|
|
75
|
-
|
|
76
92
|
} catch (err: unknown) {
|
|
77
93
|
const message = err instanceof Error ? err.message : String(err)
|
|
78
94
|
if (log.e) log.error(`Multi-Tenant Error: ${message}`)
|
|
79
|
-
return reply.code(500).send({
|
|
80
|
-
statusCode: 500,
|
|
81
|
-
error: 'Internal Server Error',
|
|
82
|
-
message: 'Tenant Context Switch Failed'
|
|
95
|
+
return reply.code(500).send({
|
|
96
|
+
statusCode: 500,
|
|
97
|
+
error: 'Internal Server Error',
|
|
98
|
+
message: 'Tenant Context Switch Failed'
|
|
83
99
|
})
|
|
84
100
|
}
|
|
85
101
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const tenantBodySchema = {
|
|
2
|
+
$id: 'tenantBodySchema',
|
|
3
|
+
type: 'object',
|
|
4
|
+
required: ['name', 'slug', 'dbSchema'],
|
|
5
|
+
properties: {
|
|
6
|
+
name: { type: 'string', minLength: 1, maxLength: 255 },
|
|
7
|
+
slug: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
minLength: 1,
|
|
10
|
+
maxLength: 100,
|
|
11
|
+
pattern: '^[a-z0-9_-]+$'
|
|
12
|
+
},
|
|
13
|
+
// S12: constrain to a safe SQL identifier alphabet (used in `SET search_path`).
|
|
14
|
+
// maxLength 63 = Postgres identifier limit; pattern mirrors the runtime sanitizer.
|
|
15
|
+
dbSchema: { type: 'string', minLength: 1, maxLength: 63, pattern: '^[a-zA-Z0-9_]+$' },
|
|
16
|
+
config: { type: 'object', additionalProperties: true }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const tenantUpdateBodySchema = {
|
|
21
|
+
$id: 'tenantUpdateBodySchema',
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
name: { type: 'string', minLength: 1, maxLength: 255 },
|
|
25
|
+
status: { type: 'string', enum: ['active', 'suspended', 'archived'] },
|
|
26
|
+
config: { type: 'object' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const tenantResponseSchema = {
|
|
31
|
+
$id: 'tenantResponseSchema',
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
id: { type: 'string', format: 'uuid' },
|
|
35
|
+
name: { type: 'string' },
|
|
36
|
+
slug: { type: 'string' },
|
|
37
|
+
dbSchema: { type: 'string' },
|
|
38
|
+
status: { type: 'string', enum: ['active', 'suspended', 'archived'] },
|
|
39
|
+
config: { type: 'object' },
|
|
40
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
41
|
+
updatedAt: { type: 'string', format: 'date-time' }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const tenantListResponseSchema = {
|
|
46
|
+
$id: 'tenantListResponseSchema',
|
|
47
|
+
type: 'array',
|
|
48
|
+
items: { $ref: 'tenantResponseSchema#' }
|
|
49
|
+
}
|
package/lib/util/regexp.ts
CHANGED
|
@@ -2,25 +2,48 @@
|
|
|
2
2
|
* min 3 max 33, one special character (. _ -) only in the middle
|
|
3
3
|
* username can have uppercase or lowercase chars
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
// NOTE: no `g` flag — with it, `.test()` is stateful (advances lastIndex) and
|
|
6
|
+
// returns alternating true/false across calls on the same input.
|
|
7
|
+
export const username = /(?=^.{3,33}$)^[a-z][a-z0-9]*[._-]?[a-z0-9]+$/i
|
|
8
|
+
// NOTE (S10): separators here are already required (`\.`), so this variant is
|
|
9
|
+
// linear and not ReDoS-prone. Guard length with MAX_EMAIL_LENGTH if you use it.
|
|
6
10
|
export const emailAlt =
|
|
7
11
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
8
12
|
|
|
9
13
|
/*
|
|
10
14
|
* email can have multiple words
|
|
11
15
|
* email can use . - or + for smart labeling
|
|
16
|
+
* TLD has no upper length limit (e.g. .test, .info, .email)
|
|
17
|
+
* NOTE (S10 — ReDoS): the separators inside the repeating groups are REQUIRED
|
|
18
|
+
* (`[.+-]`/`[.-]`, not `[.+-]?`/`[.-]?`). With an optional separator the pattern
|
|
19
|
+
* degenerates to `\w+(\w+)*`, which partitions a run of word-chars in
|
|
20
|
+
* exponentially many ways → catastrophic backtracking on a long non-matching
|
|
21
|
+
* input (e.g. many word-chars with no `@`). Making the separator required gives
|
|
22
|
+
* a single partition → linear time. Always validate via `isEmail()` so the
|
|
23
|
+
* length guard runs first.
|
|
12
24
|
*/
|
|
13
|
-
export const email = /^\w+([.+-]
|
|
25
|
+
export const email = /^\w+([.+-]\w+)*@\w+([.-]\w+)*(\.\w{2,})+$/
|
|
26
|
+
|
|
27
|
+
// RFC 5321: an email address (forward path) is at most 254 characters.
|
|
28
|
+
export const MAX_EMAIL_LENGTH = 254
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Canonical email validator: bounds the input length BEFORE the regex match
|
|
32
|
+
* (defense in depth) and then applies the (linear) `email` pattern. Prefer this
|
|
33
|
+
* over calling `email.test()` directly.
|
|
34
|
+
*/
|
|
35
|
+
export const isEmail = (value: unknown): boolean =>
|
|
36
|
+
typeof value === 'string' && value.length > 0 && value.length <= MAX_EMAIL_LENGTH && email.test(value)
|
|
14
37
|
|
|
15
38
|
/*
|
|
16
|
-
* password must contain 1
|
|
17
|
-
* password
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
39
|
+
* password must contain at least: 1 lowercase, 1 uppercase, 1 digit, 1 special char
|
|
40
|
+
* password is at least 8 characters with no space
|
|
41
|
+
* NOTE: the `-` inside the special-char class is escaped (`\-`). Without the
|
|
42
|
+
* escape, `()-_` is parsed as the range )..._ (0x29-0x5F), which makes letters
|
|
43
|
+
* and digits satisfy the "1 special char" requirement (i.e. it is NOT enforced).
|
|
21
44
|
*/
|
|
22
45
|
export const password =
|
|
23
|
-
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%&*()
|
|
46
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%&*()\-_=+[\]{}|;:'",.<>?^])[A-Za-z\d!@#$%&*()\-_=+[\]{}|;:'",.<>?^]{8,}$/
|
|
24
47
|
export const zipCode = /(^\d{5}$)|(^\d{5}-\d{4}$)/
|
|
25
48
|
export const taxCodePersona =
|
|
26
49
|
/^[a-zA-Z]{6}[0-9]{2}[abcdehlmprstABCDEHLMPRST]{1}[0-9]{2}([a-zA-Z]{1}[0-9]{3})[a-zA-Z]{1}$/
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Startup hardening for cryptographic secrets (JWT, refresh, cookie).
|
|
5
|
+
*
|
|
6
|
+
* Rationale: @fastify/jwt already refuses to register with a falsy secret
|
|
7
|
+
* (`assert(options.secret, 'missing secret')`), but it happily accepts a WEAK
|
|
8
|
+
* one (short, default or low-entropy), which makes signed tokens forgeable.
|
|
9
|
+
* For B2B/enterprise usage we fail fast with an actionable message instead.
|
|
10
|
+
*
|
|
11
|
+
* Policy:
|
|
12
|
+
* - missing/empty -> always fatal (process.exit(1))
|
|
13
|
+
* - weak (short/known/low-entropy):
|
|
14
|
+
* - production -> fatal (process.exit(1))
|
|
15
|
+
* - non-production -> warning (tolerated, must be fixed before deploy)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import logger from './logger.js'
|
|
19
|
+
|
|
20
|
+
export const MIN_SECRET_LENGTH = 32
|
|
21
|
+
const MIN_DISTINCT_CHARS = 8
|
|
22
|
+
|
|
23
|
+
// Well-known / placeholder values that must never reach production.
|
|
24
|
+
const COMMON_WEAK_SECRETS = new Set([
|
|
25
|
+
'secret',
|
|
26
|
+
'secretkey',
|
|
27
|
+
'secret-key',
|
|
28
|
+
'jwtsecret',
|
|
29
|
+
'jwt_secret',
|
|
30
|
+
'jwt-secret',
|
|
31
|
+
'your-secret',
|
|
32
|
+
'your-secret-key',
|
|
33
|
+
'your_jwt_secret',
|
|
34
|
+
'changeme',
|
|
35
|
+
'change-me',
|
|
36
|
+
'please-change-me',
|
|
37
|
+
'password',
|
|
38
|
+
'passw0rd',
|
|
39
|
+
'admin',
|
|
40
|
+
'default',
|
|
41
|
+
'test',
|
|
42
|
+
'testing',
|
|
43
|
+
'example',
|
|
44
|
+
'mysecret',
|
|
45
|
+
'supersecret',
|
|
46
|
+
'topsecret',
|
|
47
|
+
'token',
|
|
48
|
+
'volcanic',
|
|
49
|
+
'123456',
|
|
50
|
+
'12345678',
|
|
51
|
+
'123456789',
|
|
52
|
+
'qwerty',
|
|
53
|
+
'letmein'
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
export type SecretCheck = { ok: boolean; missing: boolean; reason?: string }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pure validator: returns the verdict without side effects (testable).
|
|
60
|
+
*/
|
|
61
|
+
export function validateSecretStrength(value: string | undefined): SecretCheck {
|
|
62
|
+
if (!value || value.trim().length === 0) {
|
|
63
|
+
return { ok: false, missing: true, reason: 'missing or empty' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const v = value.trim()
|
|
67
|
+
|
|
68
|
+
// Check the denylist BEFORE the length check, using a substring match: a weak
|
|
69
|
+
// value padded to reach the minimum length (e.g. "changeme" + filler) must
|
|
70
|
+
// still be rejected. An exact-match-after-length check would be dead code,
|
|
71
|
+
// since every denylisted token is shorter than MIN_SECRET_LENGTH.
|
|
72
|
+
const lower = v.toLowerCase()
|
|
73
|
+
const weak = [...COMMON_WEAK_SECRETS].find((w) => lower.includes(w))
|
|
74
|
+
if (weak) {
|
|
75
|
+
return { ok: false, missing: false, reason: `contains a well-known/default value ("${weak}")` }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (v.length < MIN_SECRET_LENGTH) {
|
|
79
|
+
return { ok: false, missing: false, reason: `too short (${v.length} chars, minimum ${MIN_SECRET_LENGTH})` }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const distinct = new Set(v).size
|
|
83
|
+
if (distinct < MIN_DISTINCT_CHARS) {
|
|
84
|
+
return { ok: false, missing: false, reason: `low entropy (only ${distinct} distinct characters)` }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: true, missing: false }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Enforce the policy. Exits the process on a fatal condition.
|
|
92
|
+
*
|
|
93
|
+
* @param name human-readable env var name (for the message)
|
|
94
|
+
* @param value the secret value
|
|
95
|
+
* @param opts.prod whether we are running in production
|
|
96
|
+
*/
|
|
97
|
+
export function assertSecretStrength(name: string, value: string | undefined, opts: { prod: boolean }): void {
|
|
98
|
+
const { ok, missing, reason } = validateSecretStrength(value)
|
|
99
|
+
if (ok) return
|
|
100
|
+
|
|
101
|
+
const hint = `Set a strong random value (>= ${MIN_SECRET_LENGTH} chars), e.g. \`openssl rand -base64 48\`.`
|
|
102
|
+
|
|
103
|
+
// Missing is always fatal; weak is fatal only in production.
|
|
104
|
+
if (missing || opts.prod) {
|
|
105
|
+
if (logger.f) logger.fatal(`Startup Security: ${name} is ${reason}. ${hint}`)
|
|
106
|
+
process.exit(1)
|
|
107
|
+
} else {
|
|
108
|
+
if (logger.w)
|
|
109
|
+
logger.warn(
|
|
110
|
+
`Startup Security: ${name} is ${reason}. Tolerated in non-production but MUST be fixed before deploying. ${hint}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volcanicminds/backend",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"codename": "rome",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,16 +14,13 @@
|
|
|
14
14
|
"http",
|
|
15
15
|
"https",
|
|
16
16
|
"fastify",
|
|
17
|
-
"apollo",
|
|
18
17
|
"swagger",
|
|
19
|
-
"graphql",
|
|
20
18
|
"helmet",
|
|
21
19
|
"json",
|
|
22
20
|
"typescript",
|
|
23
21
|
"javascript",
|
|
24
22
|
"rest",
|
|
25
23
|
"restful",
|
|
26
|
-
"graphql",
|
|
27
24
|
"pino",
|
|
28
25
|
"rate limit",
|
|
29
26
|
"jest"
|
|
@@ -31,13 +28,22 @@
|
|
|
31
28
|
"main": "dist/index.js",
|
|
32
29
|
"module": "dist/index.js",
|
|
33
30
|
"types": "dist/index.d.ts",
|
|
34
|
-
"sideEffects":
|
|
31
|
+
"sideEffects": [
|
|
32
|
+
"./dist/typeorm.js",
|
|
33
|
+
"./lib/database/typeorm/**"
|
|
34
|
+
],
|
|
35
35
|
"exports": {
|
|
36
36
|
".": {
|
|
37
37
|
"types": "./dist/index.d.ts",
|
|
38
38
|
"import": "./dist/index.js",
|
|
39
39
|
"require": "./dist/index.js"
|
|
40
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"./typeorm": {
|
|
42
|
+
"types": "./dist/typeorm.d.ts",
|
|
43
|
+
"import": "./dist/typeorm.js",
|
|
44
|
+
"require": "./dist/typeorm.js"
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
41
47
|
},
|
|
42
48
|
"files": [
|
|
43
49
|
"dist",
|
|
@@ -47,28 +53,30 @@
|
|
|
47
53
|
"clean": "rm -rf dist",
|
|
48
54
|
"prebuild": "npm run clean",
|
|
49
55
|
"build": "tsc",
|
|
56
|
+
"postbuild": "node scripts/copy-assets.mjs",
|
|
50
57
|
"preprod": "npm run build",
|
|
51
58
|
"prod": "cd dist && node server.js",
|
|
52
59
|
"start": "tsx --env-file .env server.ts",
|
|
53
60
|
"dev": "tsx watch --env-file .env server.ts",
|
|
54
|
-
"test": "npm run test:
|
|
55
|
-
"test:
|
|
61
|
+
"test": "npm run test:core && npm run test:typeorm",
|
|
62
|
+
"test:core": "cross-env PORT=2231 NODE_ENV=memory BROWSER=false mocha --import=tsx ./test/index.spec.ts -t 100000 --exit",
|
|
63
|
+
"test:typeorm": "cross-env NODE_ENV=memory MFA_DB_SECRET=unit-test-secret-please-change-32xyz mocha --import=tsx ./test/typeorm/**/*.spec.ts -t 20000 --exit",
|
|
64
|
+
"test:full": "npm run test:core",
|
|
56
65
|
"reset": "npm install && npm update && npm run build",
|
|
57
66
|
"upgrade-deps": "npx npm-check-updates -u",
|
|
58
67
|
"lint": "eslint .",
|
|
59
68
|
"lint:fix": "eslint . --fix",
|
|
60
69
|
"type-check": "tsc --noEmit",
|
|
61
|
-
"
|
|
70
|
+
"depcruise": "depcruise index.ts typeorm.ts lib --config .dependency-cruiser.cjs",
|
|
71
|
+
"check-all": "npm run lint && npm run type-check && npm run depcruise",
|
|
62
72
|
"combine": "node combine.js"
|
|
63
73
|
},
|
|
64
74
|
"dependencies": {
|
|
65
|
-
"@apollo/server": "^5.2.0",
|
|
66
|
-
"@as-integrations/fastify": "^3.1.0",
|
|
67
75
|
"@fastify/compress": "^8.3.0",
|
|
68
76
|
"@fastify/cookie": "^11.0.2",
|
|
69
77
|
"@fastify/cors": "^11.2.0",
|
|
70
78
|
"@fastify/helmet": "^13.0.2",
|
|
71
|
-
"@fastify/jwt": "^10.
|
|
79
|
+
"@fastify/jwt": "^10.1.0",
|
|
72
80
|
"@fastify/multipart": "^9.3.0",
|
|
73
81
|
"@fastify/rate-limit": "^10.3.0",
|
|
74
82
|
"@fastify/schedule": "^6.0.0",
|
|
@@ -79,7 +87,6 @@
|
|
|
79
87
|
"fastify": "^5.6.2",
|
|
80
88
|
"fastify-raw-body": "^5.0.0",
|
|
81
89
|
"glob": "^13.0.0",
|
|
82
|
-
"graphql": "^16.12.0",
|
|
83
90
|
"i18n": "^0.15.3",
|
|
84
91
|
"lodash": "^4.17.21",
|
|
85
92
|
"nanoid": "^5.1.6",
|
|
@@ -90,18 +97,52 @@
|
|
|
90
97
|
"semver": "^7.7.3",
|
|
91
98
|
"toad-scheduler": "^3.1.0"
|
|
92
99
|
},
|
|
100
|
+
"peerDependencies": {
|
|
101
|
+
"bcrypt": "^6.0.0",
|
|
102
|
+
"pg": "^8.13.0",
|
|
103
|
+
"pluralize": "^8.0.0",
|
|
104
|
+
"reflect-metadata": "^0.2.2",
|
|
105
|
+
"typeorm": "^0.3.28"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"typeorm": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"bcrypt": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"pluralize": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"reflect-metadata": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"pg": {
|
|
121
|
+
"optional": true
|
|
122
|
+
}
|
|
123
|
+
},
|
|
93
124
|
"devDependencies": {
|
|
94
125
|
"@eslint/js": "^9.39.2",
|
|
126
|
+
"@types/bcrypt": "^6.0.0",
|
|
95
127
|
"@types/mocha": "^10.0.10",
|
|
96
128
|
"@types/node": "^24.10.4",
|
|
129
|
+
"@types/pg": "^8.11.10",
|
|
130
|
+
"@types/pluralize": "^0.0.33",
|
|
97
131
|
"@types/semver": "^7.7.1",
|
|
132
|
+
"bcrypt": "^6.0.0",
|
|
98
133
|
"cross-env": "^10.1.0",
|
|
134
|
+
"dependency-cruiser": "^18.0.0",
|
|
99
135
|
"eslint": "^9.39.2",
|
|
100
136
|
"expect": "^30.2.0",
|
|
101
137
|
"globals": "^16.5.0",
|
|
102
|
-
"mocha": "^
|
|
103
|
-
"npm-upgrade": "^3.
|
|
138
|
+
"mocha": "^8.1.3",
|
|
139
|
+
"npm-upgrade": "^0.3.0",
|
|
140
|
+
"pg": "^8.13.0",
|
|
141
|
+
"pluralize": "^8.0.0",
|
|
142
|
+
"reflect-metadata": "^0.2.2",
|
|
143
|
+
"ts-node": "^10.9.2",
|
|
104
144
|
"tsx": "^4.21.0",
|
|
145
|
+
"typeorm": "^0.3.28",
|
|
105
146
|
"typescript": "^5.9.3",
|
|
106
147
|
"typescript-eslint": "^8.50.0"
|
|
107
148
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../lib/apollo/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AAEvE,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,eAAO,MAAM,iBAAiB,EAAE,4BAA4B,CAAC,SAAS,CAEpE,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../lib/apollo/context.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAQZ,MAAM,CAAC,MAAM,iBAAiB,GAA4C,KAAK,IAAI,EAAE,CAAC,CAAC;IACrF,QAAQ,EAAE,gBAAgB,GAAG,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE;CAClD,CAAC,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../lib/apollo/resolvers.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,QAAA,MAAM,SAAS;;8BAEW,GAAG,SAAS,GAAG,WAAW,SAAS,SAAS,GAAG;;CAExE,CAAA;AAED,eAAe,SAAS,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../../../lib/apollo/resolvers.ts"],"names":[],"mappings":"AACA,YAAY,CAAA;AAIZ,MAAM,SAAS,GAAG;IAChB,KAAK,EAAE;QACL,UAAU,EAAE,CAAC,OAAY,EAAE,KAAU,EAAE,OAAkB,EAAE,KAAU,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ;KAC3F;CACF,CAAA;AAED,eAAe,SAAS,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"type-defs.d.ts","sourceRoot":"","sources":["../../../lib/apollo/type-defs.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,QAAQ,qDAIb,CAAA;AAED,eAAe,QAAQ,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"type-defs.js","sourceRoot":"","sources":["../../../lib/apollo/type-defs.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,MAAM,QAAQ,GAAG;;;;CAIhB,CAAA;AAED,eAAe,QAAQ,CAAA"}
|
package/lib/apollo/context.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
import { ApolloFastifyContextFunction } from '@as-integrations/fastify'
|
|
4
|
-
|
|
5
|
-
export interface MyContext {
|
|
6
|
-
greeting: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const myContextFunction: ApolloFastifyContextFunction<MyContext> = async () => ({
|
|
10
|
-
greeting: 'Hello World!! ' + new Date().getTime()
|
|
11
|
-
})
|
package/lib/apollo/resolvers.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
import { MyContext } from './context.js'
|
|
5
|
-
|
|
6
|
-
const resolvers = {
|
|
7
|
-
Query: {
|
|
8
|
-
helloWorld: (_parent: any, _args: any, context: MyContext, _info: any) => context.greeting
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default resolvers
|