@onlineapps/service-common 2.0.1 → 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/CHANGELOG.md +601 -0
- package/README.md +282 -22
- package/package.json +10 -7
- package/src/config.js +13 -1
- package/src/defaults.js +28 -1
- package/src/index.js +18 -30
- package/src/infrastructure/prefixedLogger.js +49 -0
- package/src/infrastructure/waitForHealthCheckQueueReady.js +47 -44
- package/src/infrastructure/waitForInfrastructureReady.js +88 -60
- package/src/jwt/createJwtValidator.js +159 -29
- package/src/jwt/verifyAccessToken.js +30 -11
- package/src/redisClient.js +175 -37
- package/src/registryReader.js +26 -42
- package/src/reporting/monitoringFallbackEmail.js +163 -28
- package/src/runtime-config.js +154 -35
- package/src/errors/BusinessError.js +0 -118
- package/src/errors/errorMiddleware.js +0 -112
- package/src/errors/index.js +0 -29
- package/src/redactSensitive.js +0 -87
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// See: docs/biz/70-contracts/error-handling.md
|
|
4
|
-
const { BusinessError, isBusinessError } = require('./BusinessError');
|
|
5
|
-
|
|
6
|
-
function businessErrorHandler(err, req, res, next) {
|
|
7
|
-
if (res.headersSent) {
|
|
8
|
-
return next(err);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const logger = req.app?.locals?.logger || console;
|
|
12
|
-
|
|
13
|
-
if (isBusinessError(err)) {
|
|
14
|
-
logger.warn(`[BusinessError] ${err.errorCode}: ${err.message}`, {
|
|
15
|
-
statusCode: err.statusCode,
|
|
16
|
-
errorCode: err.errorCode,
|
|
17
|
-
type: err.type,
|
|
18
|
-
details: err.details,
|
|
19
|
-
service: err.service,
|
|
20
|
-
operation: err.operation,
|
|
21
|
-
method: req.method,
|
|
22
|
-
url: req.originalUrl
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
return res.status(err.statusCode).json({
|
|
26
|
-
error: err.toJSON()
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (err.name === 'SequelizeUniqueConstraintError') {
|
|
31
|
-
const fields = err.errors?.map(e => e.path) || [];
|
|
32
|
-
logger.warn(`[BusinessError] DUPLICATE_RESOURCE: ${err.message}`, {
|
|
33
|
-
statusCode: 409,
|
|
34
|
-
fields,
|
|
35
|
-
method: req.method,
|
|
36
|
-
url: req.originalUrl
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
return res.status(409).json({
|
|
40
|
-
error: {
|
|
41
|
-
code: 'DUPLICATE_RESOURCE',
|
|
42
|
-
message: 'Resource already exists',
|
|
43
|
-
statusCode: 409,
|
|
44
|
-
details: fields.length > 0 ? [`Duplicate value for: ${fields.join(', ')}`] : undefined
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (err.name === 'SequelizeValidationError') {
|
|
50
|
-
const details = err.errors?.map(e => e.message) || [];
|
|
51
|
-
logger.warn(`[BusinessError] VALIDATION_FAILED: ${err.message}`, {
|
|
52
|
-
statusCode: 400,
|
|
53
|
-
details,
|
|
54
|
-
method: req.method,
|
|
55
|
-
url: req.originalUrl
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return res.status(400).json({
|
|
59
|
-
error: {
|
|
60
|
-
code: 'VALIDATION_FAILED',
|
|
61
|
-
message: 'Validation failed',
|
|
62
|
-
statusCode: 400,
|
|
63
|
-
details
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (err.type === 'entity.parse.failed' || (err.message && err.message.includes('JSON'))) {
|
|
69
|
-
return res.status(400).json({
|
|
70
|
-
error: {
|
|
71
|
-
code: 'INVALID_JSON',
|
|
72
|
-
message: 'Invalid JSON in request body',
|
|
73
|
-
statusCode: 400
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
logger.error(`[UnhandledError] ${err.message}`, {
|
|
79
|
-
error: err.message,
|
|
80
|
-
stack: err.stack,
|
|
81
|
-
method: req.method,
|
|
82
|
-
url: req.originalUrl
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
86
|
-
|
|
87
|
-
return res.status(500).json({
|
|
88
|
-
error: {
|
|
89
|
-
code: 'INTERNAL_ERROR',
|
|
90
|
-
message: isProduction ? 'Internal server error' : err.message,
|
|
91
|
-
statusCode: 500
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function notFoundHandler(req, res) {
|
|
97
|
-
const logger = req.app?.locals?.logger || console;
|
|
98
|
-
logger.warn('Route not found', {
|
|
99
|
-
method: req.method,
|
|
100
|
-
url: req.originalUrl
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
res.status(404).json({
|
|
104
|
-
error: {
|
|
105
|
-
code: 'ROUTE_NOT_FOUND',
|
|
106
|
-
message: `Route ${req.method} ${req.originalUrl} not found`,
|
|
107
|
-
statusCode: 404
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = { businessErrorHandler, notFoundHandler };
|
package/src/errors/index.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
BusinessError,
|
|
5
|
-
NotFoundError,
|
|
6
|
-
ValidationError,
|
|
7
|
-
ConflictError,
|
|
8
|
-
BusinessRuleError,
|
|
9
|
-
AuthorizationError,
|
|
10
|
-
ServiceUnavailableError,
|
|
11
|
-
isBusinessError,
|
|
12
|
-
ERROR_TYPES
|
|
13
|
-
} = require('./BusinessError');
|
|
14
|
-
|
|
15
|
-
const { businessErrorHandler, notFoundHandler } = require('./errorMiddleware');
|
|
16
|
-
|
|
17
|
-
module.exports = {
|
|
18
|
-
BusinessError,
|
|
19
|
-
NotFoundError,
|
|
20
|
-
ValidationError,
|
|
21
|
-
ConflictError,
|
|
22
|
-
BusinessRuleError,
|
|
23
|
-
AuthorizationError,
|
|
24
|
-
ServiceUnavailableError,
|
|
25
|
-
isBusinessError,
|
|
26
|
-
ERROR_TYPES,
|
|
27
|
-
businessErrorHandler,
|
|
28
|
-
notFoundHandler
|
|
29
|
-
};
|
package/src/redactSensitive.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sensitive-input redaction (SecretBox F1.5).
|
|
5
|
-
* Contract: api/docs/architecture/secretbox.md §8.
|
|
6
|
-
*
|
|
7
|
-
* Pure, dependency-free helpers used by every layer that logs, traces, or
|
|
8
|
-
* otherwise persists workflow/operation inputs (gateway, orchestrator, monitoring
|
|
9
|
-
* consumer). Single implementation — no duplication across layers.
|
|
10
|
-
*
|
|
11
|
-
* Operations declare sensitive input fields at operation level in operations.json:
|
|
12
|
-
* "sensitive_input_fields": ["value", ...]
|
|
13
|
-
* (NOT inside the JSON Schema — service-wrapper compiles input schemas with AJV
|
|
14
|
-
* strict:true, which throws on unknown keywords.)
|
|
15
|
-
*
|
|
16
|
-
* `redactSensitiveDeep` deep-clones the value and replaces any object key whose
|
|
17
|
-
* name is in `fieldNames` with the REDACTED placeholder, at any depth. It never
|
|
18
|
-
* mutates the input. Over-redaction (a sensitive field name matching elsewhere in
|
|
19
|
-
* the same payload) is acceptable and safe for observability sinks.
|
|
20
|
-
*
|
|
21
|
-
* IMPORTANT: this is for observability/log/trace copies only. The execution message
|
|
22
|
-
* that carries the value to its owning handler MUST NOT be redacted.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const REDACTED_PLACEHOLDER = '[REDACTED]';
|
|
26
|
-
const CIRCULAR_PLACEHOLDER = '[Circular]';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Extract the sensitive input field names for one operation from a service spec
|
|
30
|
-
* (the `registry:service:<name>:spec` shape, or a local operations.json object).
|
|
31
|
-
* @param {object} serviceSpec - object with `.operations[<op>]`.
|
|
32
|
-
* @param {string} operationName
|
|
33
|
-
* @returns {string[]} field names (empty array when none / not found).
|
|
34
|
-
*/
|
|
35
|
-
function sensitiveFieldsForOperation(serviceSpec, operationName) {
|
|
36
|
-
const op =
|
|
37
|
-
serviceSpec &&
|
|
38
|
-
serviceSpec.operations &&
|
|
39
|
-
typeof serviceSpec.operations === 'object'
|
|
40
|
-
? serviceSpec.operations[operationName]
|
|
41
|
-
: undefined;
|
|
42
|
-
const list = op && op.sensitive_input_fields;
|
|
43
|
-
return Array.isArray(list) ? list.filter((f) => typeof f === 'string' && f.length > 0) : [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function _walk(node, fields, seen) {
|
|
47
|
-
if (node === null || typeof node !== 'object') return node;
|
|
48
|
-
// Do not recurse into non-plain objects; return a shallow copy where safe.
|
|
49
|
-
if (node instanceof Date) return new Date(node.getTime());
|
|
50
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(node)) return node;
|
|
51
|
-
if (seen.has(node)) return CIRCULAR_PLACEHOLDER;
|
|
52
|
-
seen.add(node);
|
|
53
|
-
|
|
54
|
-
if (Array.isArray(node)) {
|
|
55
|
-
return node.map((el) => _walk(el, fields, seen));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const out = {};
|
|
59
|
-
for (const key of Object.keys(node)) {
|
|
60
|
-
if (fields.has(key)) {
|
|
61
|
-
out[key] = REDACTED_PLACEHOLDER;
|
|
62
|
-
} else {
|
|
63
|
-
out[key] = _walk(node[key], fields, seen);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Deep-clone `value`, replacing any object property named in `fieldNames` with
|
|
71
|
-
* the REDACTED placeholder. Does not mutate the input.
|
|
72
|
-
* @param {*} value - any JSON-ish value (object/array/primitive).
|
|
73
|
-
* @param {string[]|Set<string>} fieldNames - sensitive field names to redact.
|
|
74
|
-
* @returns {*} redacted deep clone (or the original value when there is nothing
|
|
75
|
-
* to redact — no sensitive fields means no mutation risk).
|
|
76
|
-
*/
|
|
77
|
-
function redactSensitiveDeep(value, fieldNames) {
|
|
78
|
-
const fields = fieldNames instanceof Set ? fieldNames : new Set(fieldNames || []);
|
|
79
|
-
if (fields.size === 0) return value;
|
|
80
|
-
return _walk(value, fields, new WeakSet());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = {
|
|
84
|
-
REDACTED_PLACEHOLDER,
|
|
85
|
-
sensitiveFieldsForOperation,
|
|
86
|
-
redactSensitiveDeep
|
|
87
|
-
};
|