@heavybit/shared-lib 1.3.1 → 2.1.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/README.md +100 -0
- package/dist/cjs/common/errors.js +44 -5
- package/dist/cjs/common/errors.js.map +1 -1
- package/dist/cjs/common/phone.js +43 -10
- package/dist/cjs/common/phone.js.map +1 -1
- package/dist/cjs/common/response.js +78 -5
- package/dist/cjs/common/response.js.map +1 -1
- package/dist/cjs/config/index.js +1 -1
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/context/index.js +49 -0
- package/dist/cjs/context/index.js.map +1 -0
- package/dist/cjs/contracts/index.js +0 -2
- package/dist/cjs/contracts/index.js.map +1 -1
- package/dist/cjs/contracts/notification-dispatch.js +87 -63
- package/dist/cjs/contracts/notification-dispatch.js.map +1 -1
- package/dist/cjs/express/auth.js +44 -11
- package/dist/cjs/express/auth.js.map +1 -1
- package/dist/cjs/express/index.js +1 -0
- package/dist/cjs/express/index.js.map +1 -1
- package/dist/cjs/express/middleware.js +154 -34
- package/dist/cjs/express/middleware.js.map +1 -1
- package/dist/cjs/express/respond.js +47 -0
- package/dist/cjs/express/respond.js.map +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/logging/index.js +76 -40
- package/dist/cjs/logging/index.js.map +1 -1
- package/dist/cjs/logging/redact.js +117 -0
- package/dist/cjs/logging/redact.js.map +1 -0
- package/dist/cjs/messaging/index.js +1 -0
- package/dist/cjs/messaging/index.js.map +1 -1
- package/dist/cjs/messaging/staff-notifier.js +36 -44
- package/dist/cjs/messaging/staff-notifier.js.map +1 -1
- package/dist/cjs/messaging/staff-recipients.js +57 -0
- package/dist/cjs/messaging/staff-recipients.js.map +1 -0
- package/dist/cjs/validation/index.js +27 -3
- package/dist/cjs/validation/index.js.map +1 -1
- package/dist/esm/common/errors.js +42 -4
- package/dist/esm/common/errors.js.map +1 -1
- package/dist/esm/common/phone.js +43 -10
- package/dist/esm/common/phone.js.map +1 -1
- package/dist/esm/common/response.js +75 -5
- package/dist/esm/common/response.js.map +1 -1
- package/dist/esm/config/index.js +1 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/context/index.js +44 -0
- package/dist/esm/context/index.js.map +1 -0
- package/dist/esm/contracts/index.js +0 -2
- package/dist/esm/contracts/index.js.map +1 -1
- package/dist/esm/contracts/notification-dispatch.js +85 -62
- package/dist/esm/contracts/notification-dispatch.js.map +1 -1
- package/dist/esm/express/auth.js +45 -12
- package/dist/esm/express/auth.js.map +1 -1
- package/dist/esm/express/index.js +1 -0
- package/dist/esm/express/index.js.map +1 -1
- package/dist/esm/express/middleware.js +146 -27
- package/dist/esm/express/middleware.js.map +1 -1
- package/dist/esm/express/respond.js +42 -0
- package/dist/esm/express/respond.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logging/index.js +68 -39
- package/dist/esm/logging/index.js.map +1 -1
- package/dist/esm/logging/redact.js +110 -0
- package/dist/esm/logging/redact.js.map +1 -0
- package/dist/esm/messaging/index.js +1 -0
- package/dist/esm/messaging/index.js.map +1 -1
- package/dist/esm/messaging/staff-notifier.js +36 -43
- package/dist/esm/messaging/staff-notifier.js.map +1 -1
- package/dist/esm/messaging/staff-recipients.js +53 -0
- package/dist/esm/messaging/staff-recipients.js.map +1 -0
- package/dist/esm/validation/index.js +27 -2
- package/dist/esm/validation/index.js.map +1 -1
- package/dist/types/alerting/index.d.ts +1 -1
- package/dist/types/alerting/index.d.ts.map +1 -1
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/index.d.ts.map +1 -1
- package/dist/types/common/errors.d.ts +48 -7
- package/dist/types/common/errors.d.ts.map +1 -1
- package/dist/types/common/phone.d.ts +35 -6
- package/dist/types/common/phone.d.ts.map +1 -1
- package/dist/types/common/response.d.ts +66 -5
- package/dist/types/common/response.d.ts.map +1 -1
- package/dist/types/config/index.d.ts +1 -1
- package/dist/types/config/index.d.ts.map +1 -1
- package/dist/types/context/index.d.ts +39 -0
- package/dist/types/context/index.d.ts.map +1 -0
- package/dist/types/contracts/index.d.ts +0 -2
- package/dist/types/contracts/index.d.ts.map +1 -1
- package/dist/types/contracts/notification-dispatch.d.ts +208 -90
- package/dist/types/contracts/notification-dispatch.d.ts.map +1 -1
- package/dist/types/express/auth.d.ts +42 -6
- package/dist/types/express/auth.d.ts.map +1 -1
- package/dist/types/express/index.d.ts +1 -0
- package/dist/types/express/index.d.ts.map +1 -1
- package/dist/types/express/middleware.d.ts +86 -7
- package/dist/types/express/middleware.d.ts.map +1 -1
- package/dist/types/express/respond.d.ts +41 -0
- package/dist/types/express/respond.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/logging/index.d.ts +30 -4
- package/dist/types/logging/index.d.ts.map +1 -1
- package/dist/types/logging/redact.d.ts +54 -0
- package/dist/types/logging/redact.d.ts.map +1 -0
- package/dist/types/messaging/index.d.ts +1 -0
- package/dist/types/messaging/index.d.ts.map +1 -1
- package/dist/types/messaging/staff-notifier.d.ts +67 -36
- package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
- package/dist/types/messaging/staff-recipients.d.ts +50 -0
- package/dist/types/messaging/staff-recipients.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +36 -1
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/validation/index.d.ts +33 -2
- package/dist/types/validation/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/cjs/contracts/editorial-draft.js +0 -120
- package/dist/cjs/contracts/editorial-draft.js.map +0 -1
- package/dist/cjs/contracts/upload-import.js +0 -54
- package/dist/cjs/contracts/upload-import.js.map +0 -1
- package/dist/esm/contracts/editorial-draft.js +0 -117
- package/dist/esm/contracts/editorial-draft.js.map +0 -1
- package/dist/esm/contracts/upload-import.js +0 -51
- package/dist/esm/contracts/upload-import.js.map +0 -1
- package/dist/types/contracts/editorial-draft.d.ts +0 -610
- package/dist/types/contracts/editorial-draft.d.ts.map +0 -1
- package/dist/types/contracts/upload-import.d.ts +0 -96
- package/dist/types/contracts/upload-import.d.ts.map +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ok = ok;
|
|
4
|
+
exports.created = created;
|
|
5
|
+
exports.okPage = okPage;
|
|
6
|
+
const response_js_1 = require("../common/response.js");
|
|
7
|
+
/**
|
|
8
|
+
* 200 with the success envelope.
|
|
9
|
+
*
|
|
10
|
+
* @param res Express response.
|
|
11
|
+
* @param data The payload placed under `data`.
|
|
12
|
+
* @param message Optional human-readable message (default `Success`).
|
|
13
|
+
* @param meta Optional list metadata.
|
|
14
|
+
* @returns The Express response.
|
|
15
|
+
* @example ok(res, product, 'Product updated');
|
|
16
|
+
*/
|
|
17
|
+
function ok(res, data, message, meta) {
|
|
18
|
+
const body = (0, response_js_1.successResponse)(data, message);
|
|
19
|
+
return res.status(200).json(meta ? { ...body, meta } : body);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 201 with the success envelope, for a newly created resource.
|
|
23
|
+
*
|
|
24
|
+
* @param res Express response.
|
|
25
|
+
* @param data The created resource (or its id).
|
|
26
|
+
* @param message Optional message (default `Created`).
|
|
27
|
+
* @returns The Express response.
|
|
28
|
+
*
|
|
29
|
+
* @example created(res, { id }, 'Shop created');
|
|
30
|
+
*/
|
|
31
|
+
function created(res, data, message = 'Created') {
|
|
32
|
+
return res.status(201).json((0, response_js_1.successResponse)(data, message));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 200 with a cursor-paginated list: `data` is the items, `meta` is `{ limit, nextCursor, hasMore }`.
|
|
36
|
+
*
|
|
37
|
+
* @param res Express response.
|
|
38
|
+
* @param page The page from the service (see `buildCursorPageMeta`).
|
|
39
|
+
* @param message Optional message.
|
|
40
|
+
* @returns The Express response.
|
|
41
|
+
*
|
|
42
|
+
* @example okPage(res, await service.list(scope, { cursor, limit }));
|
|
43
|
+
*/
|
|
44
|
+
function okPage(res, page, message) {
|
|
45
|
+
return res.status(200).json((0, response_js_1.cursorPaginatedResponse)(page, message));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=respond.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"respond.js","sourceRoot":"","sources":["../../../src/express/respond.ts"],"names":[],"mappings":";;AAmBA,gBAGC;AAYD,0BAEC;AAYD,wBAEC;AA5CD,uDAAiF;AAGjF;;;;;;;;;GASG;AACH,SAAgB,EAAE,CAAI,GAAa,EAAE,IAAO,EAAE,OAAgB,EAAE,IAAsC;IACpG,MAAM,IAAI,GAAG,IAAA,6BAAe,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,OAAO,CAAI,GAAa,EAAE,IAAO,EAAE,OAAO,GAAG,SAAS;IACpE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAA,6BAAe,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,MAAM,CAAI,GAAa,EAAE,IAAmB,EAAE,OAAgB;IAC5E,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAA,qCAAuB,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;AACtE,CAAC"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
// live behind subpaths: `@heavybit/shared-lib/express`, `/cache`, `/messaging`, …
|
|
20
20
|
__exportStar(require("./types/index.js"), exports);
|
|
21
21
|
__exportStar(require("./common/index.js"), exports);
|
|
22
|
+
__exportStar(require("./context/index.js"), exports);
|
|
22
23
|
__exportStar(require("./identity/index.js"), exports);
|
|
23
24
|
__exportStar(require("./access/index.js"), exports);
|
|
24
25
|
__exportStar(require("./validation/index.js"), exports);
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,6EAA6E;AAC7E,8FAA8F;AAC9F,kFAAkF;AAClF,mDAAiC;AACjC,oDAAkC;AAClC,sDAAoC;AACpC,oDAAkC;AAClC,wDAAsC;AACtC,qDAAmC;AACnC,oDAAkC;AAClC,uDAAqC;AACrC,sDAAoC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,6EAA6E;AAC7E,8FAA8F;AAC9F,kFAAkF;AAClF,mDAAiC;AACjC,oDAAkC;AAClC,qDAAmC;AACnC,sDAAoC;AACpC,oDAAkC;AAClC,wDAAsC;AACtC,qDAAmC;AACnC,oDAAkC;AAClC,uDAAqC;AACrC,sDAAoC"}
|
|
@@ -3,60 +3,91 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.silentLogger = exports.DEFAULT_REDACT_KEYS = void 0;
|
|
6
|
+
exports.silentLogger = exports.redactValue = exports.maskPhonesInText = exports.maskPhoneForLog = exports.createKeyClassifier = exports.REDACTED = exports.DEFAULT_REDACT_KEYS = exports.DEFAULT_ALLOW_KEYS = void 0;
|
|
7
7
|
exports.createLogger = createLogger;
|
|
8
8
|
exports.childLogger = childLogger;
|
|
9
9
|
const winston_1 = __importDefault(require("winston"));
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
10
|
+
const index_js_1 = require("../context/index.js");
|
|
11
|
+
const redact_js_1 = require("./redact.js");
|
|
12
|
+
var redact_js_2 = require("./redact.js");
|
|
13
|
+
Object.defineProperty(exports, "DEFAULT_ALLOW_KEYS", { enumerable: true, get: function () { return redact_js_2.DEFAULT_ALLOW_KEYS; } });
|
|
14
|
+
Object.defineProperty(exports, "DEFAULT_REDACT_KEYS", { enumerable: true, get: function () { return redact_js_2.DEFAULT_REDACT_KEYS; } });
|
|
15
|
+
Object.defineProperty(exports, "REDACTED", { enumerable: true, get: function () { return redact_js_2.REDACTED; } });
|
|
16
|
+
Object.defineProperty(exports, "createKeyClassifier", { enumerable: true, get: function () { return redact_js_2.createKeyClassifier; } });
|
|
17
|
+
Object.defineProperty(exports, "maskPhoneForLog", { enumerable: true, get: function () { return redact_js_2.maskPhoneForLog; } });
|
|
18
|
+
Object.defineProperty(exports, "maskPhonesInText", { enumerable: true, get: function () { return redact_js_2.maskPhonesInText; } });
|
|
19
|
+
Object.defineProperty(exports, "redactValue", { enumerable: true, get: function () { return redact_js_2.redactValue; } });
|
|
20
|
+
const { combine, timestamp, errors, colorize, printf, splat } = winston_1.default.format;
|
|
21
|
+
/** Winston bookkeeping keys that never appear as metadata. */
|
|
22
|
+
const RESERVED_KEYS = new Set(['level', 'message', 'timestamp', 'splat']);
|
|
23
|
+
/** Ids the library itself puts on a line; never redacted or phone-masked. */
|
|
24
|
+
const CONTEXT_KEYS = new Set(['service', 'correlationId', 'userId', 'scopes']);
|
|
25
|
+
/** Adds the request context (correlation id, user id, scopes, service) unless the line already has it. */
|
|
26
|
+
const withContext = winston_1.default.format((info) => {
|
|
27
|
+
const ctx = (0, index_js_1.getRequestContext)();
|
|
28
|
+
if (!ctx)
|
|
29
|
+
return info;
|
|
30
|
+
const line = info;
|
|
31
|
+
line.correlationId ??= ctx.correlationId;
|
|
32
|
+
if (ctx.userId !== undefined)
|
|
33
|
+
line.userId ??= ctx.userId;
|
|
34
|
+
if (ctx.scopes && Object.keys(ctx.scopes).length > 0)
|
|
35
|
+
line.scopes ??= ctx.scopes;
|
|
36
|
+
if (ctx.service !== undefined)
|
|
37
|
+
line.service ??= ctx.service;
|
|
38
|
+
return info;
|
|
39
|
+
});
|
|
40
|
+
/** Redacts secrets and masks phones in the message and every metadata value. */
|
|
41
|
+
const redact = (classifier) => winston_1.default.format((info) => {
|
|
42
|
+
const line = info;
|
|
43
|
+
if (typeof line.message === 'string')
|
|
44
|
+
line.message = (0, redact_js_1.maskPhonesInText)(line.message);
|
|
45
|
+
const meta = Object.fromEntries(Object.entries(line).filter(([key]) => !RESERVED_KEYS.has(key) && !CONTEXT_KEYS.has(key)));
|
|
46
|
+
Object.assign(line, (0, redact_js_1.redactValue)(meta, classifier));
|
|
36
47
|
return info;
|
|
37
|
-
})
|
|
38
|
-
|
|
48
|
+
});
|
|
49
|
+
/** One JSON object per line: `{ level, time, msg, service, correlationId, userId, scopes, ...meta }`. */
|
|
50
|
+
const jsonLine = printf((info) => {
|
|
51
|
+
const { level, message, timestamp: time, service, correlationId, userId, scopes, ...meta } = info;
|
|
52
|
+
return JSON.stringify({ level, time, msg: message, service, correlationId, userId, scopes, ...meta });
|
|
53
|
+
});
|
|
54
|
+
const prettyLine = printf((info) => {
|
|
39
55
|
const { level, message, timestamp: ts, service, stack, ...rest } = info;
|
|
40
|
-
const
|
|
41
|
-
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
|
56
|
+
const metaStr = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : '';
|
|
42
57
|
return `${String(ts ?? '')} ${String(level)}: ${service ? `[${String(service)}] ` : ''}${String(message)}${metaStr}${stack ? `\n${String(stack)}` : ''}`;
|
|
43
58
|
});
|
|
44
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Structured logger. Every line carries `service` and, inside a request or {@link runWithContext}, the
|
|
61
|
+
* context's `correlationId`, `userId` and `scopes`. Secrets are redacted by key word (see
|
|
62
|
+
* {@link DEFAULT_REDACT_KEYS}) and phone numbers are masked to their last 4 digits, in the message and
|
|
63
|
+
* at any depth of the metadata. Writes to stdout (JSON in production, pretty otherwise; silent under
|
|
64
|
+
* `NODE_ENV=test` unless `LOG_IN_TESTS` is set) plus an optional file and stream.
|
|
65
|
+
*
|
|
66
|
+
* @param service Service name on every line.
|
|
67
|
+
* @param options Level, format, extra redact/allow keys, file path, stream.
|
|
68
|
+
* @returns A winston logger that satisfies {@link Logger}.
|
|
69
|
+
* @example const logger = createLogger('api', { filePath: 'logs/api.log' });
|
|
70
|
+
*/
|
|
45
71
|
function createLogger(service, options = {}) {
|
|
46
72
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
47
73
|
const level = options.level ?? process.env.LOG_LEVEL ?? (isProduction ? 'info' : 'debug');
|
|
48
74
|
const format = options.format ?? process.env.LOG_FORMAT ?? (isProduction ? 'json' : 'pretty');
|
|
49
|
-
const
|
|
75
|
+
const classifier = (0, redact_js_1.createKeyClassifier)(options.redactKeys, options.allowKeys);
|
|
76
|
+
const transports = [
|
|
77
|
+
new winston_1.default.transports.Console({
|
|
78
|
+
silent: process.env.NODE_ENV === 'test' && !process.env.LOG_IN_TESTS,
|
|
79
|
+
format: format === 'json' ? jsonLine : combine(colorize({ level: true }), prettyLine),
|
|
80
|
+
}),
|
|
81
|
+
];
|
|
82
|
+
if (options.filePath)
|
|
83
|
+
transports.push(new winston_1.default.transports.File({ filename: options.filePath, format: jsonLine }));
|
|
84
|
+
if (options.stream)
|
|
85
|
+
transports.push(new winston_1.default.transports.Stream({ stream: options.stream, format: jsonLine }));
|
|
50
86
|
return winston_1.default.createLogger({
|
|
51
87
|
level,
|
|
52
88
|
defaultMeta: { service },
|
|
53
|
-
format: combine(errors({ stack: true }), splat(), redact(
|
|
54
|
-
transports
|
|
55
|
-
new winston_1.default.transports.Console({
|
|
56
|
-
silent: process.env.NODE_ENV === 'test' && !process.env.LOG_IN_TESTS,
|
|
57
|
-
format: format === 'json' ? json() : combine(colorize({ level: true }), devFormat),
|
|
58
|
-
}),
|
|
59
|
-
],
|
|
89
|
+
format: combine(errors({ stack: true }), splat(), withContext(), redact(classifier)(), timestamp()),
|
|
90
|
+
transports,
|
|
60
91
|
});
|
|
61
92
|
}
|
|
62
93
|
/** Logger that discards everything — handy default for libraries and tests. */
|
|
@@ -66,6 +97,11 @@ exports.silentLogger = { debug: () => { }, info: () => { }, warn: () => { }, err
|
|
|
66
97
|
* meta wins on a key clash. `undefined` context values are dropped so lines never carry empty keys.
|
|
67
98
|
* Works over any {@link Logger} (winston, a test spy, or another child), so handlers can create one
|
|
68
99
|
* per message and pass it down.
|
|
100
|
+
*
|
|
101
|
+
* @param logger The logger to write through.
|
|
102
|
+
* @param context Values added to every line.
|
|
103
|
+
* @returns A {@link Logger}.
|
|
104
|
+
* @example const log = childLogger(logger, { jobId });
|
|
69
105
|
*/
|
|
70
106
|
function childLogger(logger, context) {
|
|
71
107
|
const base = Object.fromEntries(Object.entries(context).filter(([, v]) => v !== undefined));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/logging/index.ts"],"names":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/logging/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Secret redaction and phone masking for log lines. Matching is by key *word*, not substring: a key is
|
|
4
|
+
* split on camelCase and separators (`refresh_token` → `refresh`, `token`; `OTPCode` → `otp`, `code`),
|
|
5
|
+
* and it is sensitive when a word (or two adjacent words joined, e.g. `api`+`key`) equals a listed term
|
|
6
|
+
* or its plural. So `accessToken` and `Set-Cookie` are redacted while `pinned` or `spinner` are not.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.REDACTED = exports.DEFAULT_ALLOW_KEYS = exports.DEFAULT_REDACT_KEYS = void 0;
|
|
10
|
+
exports.maskPhoneForLog = maskPhoneForLog;
|
|
11
|
+
exports.maskPhonesInText = maskPhonesInText;
|
|
12
|
+
exports.createKeyClassifier = createKeyClassifier;
|
|
13
|
+
exports.redactValue = redactValue;
|
|
14
|
+
/** Words whose values are always replaced with `[REDACTED]`. Apps add their own via `redactKeys`. */
|
|
15
|
+
exports.DEFAULT_REDACT_KEYS = [
|
|
16
|
+
'password', 'pass', 'passphrase', 'token', 'secret', 'code', 'otp', 'authorization', 'cookie', 'credential',
|
|
17
|
+
'pin', 'apikey', 'jwt', 'signature',
|
|
18
|
+
];
|
|
19
|
+
/** Keys that are never redacted although they contain a listed word (error, status and reference codes). */
|
|
20
|
+
exports.DEFAULT_ALLOW_KEYS = ['errorCode', 'statusCode', 'countryCode', 'currencyCode', 'postalCode', 'zipCode'];
|
|
21
|
+
/** Words that mark a value as a phone number, masked to its last 4 digits. */
|
|
22
|
+
const PHONE_KEY_WORDS = new Set(['phone', 'msisdn', 'mobile', 'tel']);
|
|
23
|
+
/** 9–15 digits, optionally with a leading `+`, not part of a longer word or number. */
|
|
24
|
+
const PHONE_IN_TEXT = /(?<![\w+]|\d\.)\+?\d{9,15}(?!\w|\.\d)/g;
|
|
25
|
+
/** The marker that replaces a secret value. */
|
|
26
|
+
exports.REDACTED = '[REDACTED]';
|
|
27
|
+
const MAX_DEPTH = 8;
|
|
28
|
+
/**
|
|
29
|
+
* Masks every digit except the last four (log rule; the UI uses `maskPhone` from common).
|
|
30
|
+
*
|
|
31
|
+
* @param value A phone number in any format.
|
|
32
|
+
* @returns e.g. `********5678`; values with fewer than 5 digits are fully masked.
|
|
33
|
+
* @example maskPhoneForLog('15551234567') // '*******4567'
|
|
34
|
+
*/
|
|
35
|
+
function maskPhoneForLog(value) {
|
|
36
|
+
const digits = value.replace(/\D/g, '');
|
|
37
|
+
if (digits.length < 5)
|
|
38
|
+
return '*'.repeat(digits.length || 4);
|
|
39
|
+
return `${'*'.repeat(digits.length - 4)}${digits.slice(-4)}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Masks phone-number-looking runs inside free text.
|
|
43
|
+
*
|
|
44
|
+
* @param text Any string (a log message or value).
|
|
45
|
+
* @returns The text with each 9–15 digit run masked to its last 4 digits.
|
|
46
|
+
* @example maskPhonesInText('OTP sent to 15551234567') // 'OTP sent to *******4567'
|
|
47
|
+
*/
|
|
48
|
+
function maskPhonesInText(text) {
|
|
49
|
+
return text.replace(PHONE_IN_TEXT, maskPhoneForLog);
|
|
50
|
+
}
|
|
51
|
+
function keyWords(key) {
|
|
52
|
+
return key
|
|
53
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
54
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.split(/[^a-z0-9]+/)
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Builds a key classifier from the default terms plus app-specific ones.
|
|
61
|
+
*
|
|
62
|
+
* @param extraKeys Extra words (matched like the defaults, case-insensitive).
|
|
63
|
+
* @param allowKeys Exact keys never treated as secret (defaults to {@link DEFAULT_ALLOW_KEYS}).
|
|
64
|
+
* @returns The classifier.
|
|
65
|
+
* @example createKeyClassifier(['nationalId']).isSecret('nationalId') // true
|
|
66
|
+
*/
|
|
67
|
+
function createKeyClassifier(extraKeys = [], allowKeys = exports.DEFAULT_ALLOW_KEYS) {
|
|
68
|
+
const terms = new Set([...exports.DEFAULT_REDACT_KEYS, ...extraKeys].map((k) => keyWords(k).join('')));
|
|
69
|
+
const allow = new Set(allowKeys);
|
|
70
|
+
const matches = (word) => terms.has(word) || (word.endsWith('s') && terms.has(word.slice(0, -1)));
|
|
71
|
+
return {
|
|
72
|
+
isSecret(key) {
|
|
73
|
+
if (allow.has(key))
|
|
74
|
+
return false;
|
|
75
|
+
if (key.endsWith('Enc') || key.endsWith('_enc'))
|
|
76
|
+
return true;
|
|
77
|
+
const words = keyWords(key);
|
|
78
|
+
if (matches(words.join('')))
|
|
79
|
+
return true;
|
|
80
|
+
return words.some((word, i) => matches(word) || (i > 0 && matches(`${words[i - 1]}${word}`)));
|
|
81
|
+
},
|
|
82
|
+
isPhone(key) {
|
|
83
|
+
return keyWords(key).some((word) => PHONE_KEY_WORDS.has(word));
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns a copy of `value` with secrets replaced by `[REDACTED]` and phone numbers masked, at any depth
|
|
89
|
+
* (up to 8 levels; deeper values are dropped as `[TRUNCATED]`). Errors are kept as-is.
|
|
90
|
+
*
|
|
91
|
+
* @param value Log metadata (object, array or scalar).
|
|
92
|
+
* @param classifier From {@link createKeyClassifier}.
|
|
93
|
+
* @param depth Internal recursion depth; callers omit it.
|
|
94
|
+
* @returns The redacted copy; the input is not modified.
|
|
95
|
+
* @example redactValue({ user: { otp: '1234', phone: '15551234567' } }, createKeyClassifier())
|
|
96
|
+
*/
|
|
97
|
+
function redactValue(value, classifier, depth = 0) {
|
|
98
|
+
if (typeof value === 'string')
|
|
99
|
+
return maskPhonesInText(value);
|
|
100
|
+
if (value === null || typeof value !== 'object' || value instanceof Error || value instanceof Date)
|
|
101
|
+
return value;
|
|
102
|
+
if (depth >= MAX_DEPTH)
|
|
103
|
+
return '[TRUNCATED]';
|
|
104
|
+
if (Array.isArray(value))
|
|
105
|
+
return value.map((v) => redactValue(v, classifier, depth + 1));
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, v] of Object.entries(value)) {
|
|
108
|
+
if (classifier.isSecret(key))
|
|
109
|
+
out[key] = exports.REDACTED;
|
|
110
|
+
else if (classifier.isPhone(key) && (typeof v === 'string' || typeof v === 'number'))
|
|
111
|
+
out[key] = maskPhoneForLog(String(v));
|
|
112
|
+
else
|
|
113
|
+
out[key] = redactValue(v, classifier, depth + 1);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../../src/logging/redact.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,6 +32,7 @@ __exportStar(require("./rpc.js"), exports);
|
|
|
32
32
|
__exportStar(require("./outbox.js"), exports);
|
|
33
33
|
__exportStar(require("./monitor.js"), exports);
|
|
34
34
|
__exportStar(require("./staff-notifier.js"), exports);
|
|
35
|
+
__exportStar(require("./staff-recipients.js"), exports);
|
|
35
36
|
/** Wires one connection with a publisher, consumer, RPC server/client and DLQ monitor. */
|
|
36
37
|
function createMessaging(options) {
|
|
37
38
|
const logger = options.logger ?? index_js_1.silentLogger;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/messaging/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/messaging/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AA2CA,0CA0BC;AArED,kDAAmD;AAEnD,mDAAiD;AACjD,+CAAgD;AAChD,6CAA4C;AAC5C,iDAAkD;AAClD,qCAAgD;AAChD,+CAA8D;AAE9D,kDAAgC;AAChC,gDAA8B;AAC9B,gDAA8B;AAC9B,iDAA+B;AAC/B,gDAA8B;AAC9B,mDAAiC;AACjC,2CAAyB;AACzB,8CAA4B;AAC5B,+CAA6B;AAC7B,sDAAoC;AACpC,wDAAsC;AAuBtC,0FAA0F;AAC1F,SAAgB,eAAe,CAAC,OAAyB;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,uBAAY,CAAC;IAC9C,MAAM,QAAQ,GAAG,IAAA,4BAAc,EAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9E,MAAM,UAAU,GAAG,IAAI,8BAAc,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5H,MAAM,SAAS,GAAG,IAAI,+BAAgB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAClG,MAAM,QAAQ,GAAG,IAAI,6BAAe,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,IAAI,kBAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,IAAI,kBAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,IAAI,yBAAY,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;IAEtF,OAAO;QACL,QAAQ;QACR,UAAU;QACV,SAAS;QACT,QAAQ;QACR,SAAS;QACT,SAAS;QACT,OAAO;QACP,KAAK,CAAC,KAAK;YACT,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC;YACvB,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
3
|
+
exports.StaffNotifier = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Staff alerts (messaging layer helper): stage one notification dispatch per recipient of a
|
|
6
|
+
* permission through the caller's outbox. App vocabulary (category, channels, command name) is
|
|
7
|
+
* passed in as options; nothing app-specific is assumed.
|
|
8
|
+
*/
|
|
5
9
|
const notification_dispatch_js_1 = require("../contracts/notification-dispatch.js");
|
|
6
10
|
const index_js_1 = require("../logging/index.js");
|
|
7
|
-
/** Recipients from configuration (env), keyed by the permission an alert targets. */
|
|
8
|
-
class ConfiguredStaffRecipients {
|
|
9
|
-
byPermission;
|
|
10
|
-
constructor(byPermission) {
|
|
11
|
-
this.byPermission = byPermission;
|
|
12
|
-
}
|
|
13
|
-
async resolve(permission) {
|
|
14
|
-
return [...(this.byPermission[permission] ?? [])];
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
exports.ConfiguredStaffRecipients = ConfiguredStaffRecipients;
|
|
18
11
|
/**
|
|
19
|
-
* Stages one
|
|
20
|
-
* alert is sent if and only if the state change that raised it commits. Never sends
|
|
12
|
+
* Stages one dispatch command per recipient through the outbox, in the caller's transaction, so an
|
|
13
|
+
* alert is sent if and only if the state change that raised it commits. Never sends anything itself.
|
|
21
14
|
* An alert that resolves to nobody is logged as a warning (an unconfigured list must not fail silently).
|
|
15
|
+
* Staff alerts always bypass recipient preferences and record the target permission.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam E The service's transaction/executor type.
|
|
18
|
+
* @typeParam C The app's dispatch categories.
|
|
19
|
+
* @typeParam Ch The app's dispatch channels.
|
|
20
|
+
* @example
|
|
21
|
+
* const notifier = new StaffNotifier(outbox, new ConfiguredStaffRecipients({ 'payouts.manage': config.financeEmails }),
|
|
22
|
+
* { category: 'staff', defaultChannels: ['email'], logger });
|
|
23
|
+
* await notifier.alert(tx, { template: 'payout-failed', permission: 'payouts.manage', data: { reference } }, { correlationId });
|
|
22
24
|
*/
|
|
23
25
|
class StaffNotifier {
|
|
24
26
|
outbox;
|
|
25
27
|
recipients;
|
|
28
|
+
category;
|
|
29
|
+
defaultChannels;
|
|
30
|
+
command;
|
|
26
31
|
logger;
|
|
27
|
-
constructor(outbox, recipients,
|
|
32
|
+
constructor(outbox, recipients, options) {
|
|
28
33
|
this.outbox = outbox;
|
|
29
34
|
this.recipients = recipients;
|
|
30
|
-
this.
|
|
35
|
+
this.category = options.category;
|
|
36
|
+
this.defaultChannels = options.defaultChannels;
|
|
37
|
+
this.command = options.command ?? notification_dispatch_js_1.NOTIFICATION_DISPATCH;
|
|
38
|
+
this.logger = options.logger ?? index_js_1.silentLogger;
|
|
31
39
|
}
|
|
32
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Stage the alert for every recipient of its permission.
|
|
42
|
+
*
|
|
43
|
+
* @param executor The caller's open transaction; the commands commit or roll back with it.
|
|
44
|
+
* @param alert Template, permission, variables and optional channels.
|
|
45
|
+
* @param meta Correlation id and aggregate for tracing; the actor is always the system.
|
|
46
|
+
* @returns How many commands were staged (0 when nobody is configured).
|
|
47
|
+
*/
|
|
33
48
|
async alert(executor, alert, meta = {}) {
|
|
34
49
|
const targets = await this.recipients.resolve(alert.permission);
|
|
35
50
|
if (targets.length === 0) {
|
|
@@ -39,40 +54,17 @@ class StaffNotifier {
|
|
|
39
54
|
for (const recipient of targets) {
|
|
40
55
|
const dispatch = {
|
|
41
56
|
template: alert.template,
|
|
42
|
-
category:
|
|
57
|
+
category: this.category,
|
|
43
58
|
recipient,
|
|
44
|
-
channels: alert.channels ??
|
|
59
|
+
channels: [...(alert.channels ?? this.defaultChannels)],
|
|
45
60
|
data: alert.data,
|
|
46
61
|
bypassPreferences: true,
|
|
47
62
|
targetPermission: alert.permission,
|
|
48
63
|
};
|
|
49
|
-
await this.outbox.command(executor,
|
|
64
|
+
await this.outbox.command(executor, this.command, dispatch, { ...meta, actorId: null });
|
|
50
65
|
}
|
|
51
66
|
return targets.length;
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
exports.StaffNotifier = StaffNotifier;
|
|
55
|
-
const emailSchema = zod_1.z.string().email().max(notification_dispatch_js_1.DISPATCH_LIMITS.email);
|
|
56
|
-
/**
|
|
57
|
-
* A comma-separated staff address list from env (`"a@x.io, b@y.io"`): trimmed, blanks dropped,
|
|
58
|
-
* de-duplicated case-insensitively. An invalid address fails validation, so a typo is caught at boot
|
|
59
|
-
* instead of dead-lettering in notification-service when the alert finally fires.
|
|
60
|
-
*/
|
|
61
|
-
exports.staffEmailListSchema = zod_1.z
|
|
62
|
-
.string()
|
|
63
|
-
.optional()
|
|
64
|
-
.transform((csv) => {
|
|
65
|
-
const seen = new Set();
|
|
66
|
-
return (csv ?? '')
|
|
67
|
-
.split(',')
|
|
68
|
-
.map((entry) => entry.trim())
|
|
69
|
-
.filter((email) => email && !seen.has(email.toLowerCase()) && seen.add(email.toLowerCase()));
|
|
70
|
-
})
|
|
71
|
-
.superRefine((emails, ctx) => {
|
|
72
|
-
for (const email of emails) {
|
|
73
|
-
if (!emailSchema.safeParse(email).success)
|
|
74
|
-
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Not a valid email address: ${email}` });
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
.transform((emails) => emails.map((email) => ({ email })));
|
|
78
70
|
//# sourceMappingURL=staff-notifier.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staff-notifier.js","sourceRoot":"","sources":["../../../src/messaging/staff-notifier.ts"],"names":[],"mappings":";;;AAAA
|
|
1
|
+
{"version":3,"file":"staff-notifier.js","sourceRoot":"","sources":["../../../src/messaging/staff-notifier.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,oFAI+C;AAC/C,kDAAmD;AAsDnD;;;;;;;;;;;;;GAaG;AACH,MAAa,aAAa;IAOL;IACA;IAPF,QAAQ,CAAI;IACZ,eAAe,CAAyB;IACxC,OAAO,CAAS;IAChB,MAAM,CAAS;IAEhC,YACmB,MAAwB,EACxB,UAAkC,EACnD,OAAoC;QAFnB,WAAM,GAAN,MAAM,CAAkB;QACxB,eAAU,GAAV,UAAU,CAAwB;QAGnD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;QAC/C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gDAAqB,CAAC;QACxD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,uBAAY,CAAC;IAC/C,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,KAAK,CAAC,QAAW,EAAE,KAAqB,EAAE,OAAuB,EAAE;QACvE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAChE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oDAAoD,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;YACnI,OAAO,CAAC,CAAC;QACX,CAAC;QACD,KAAK,MAAM,SAAS,IAAI,OAAO,EAAE,CAAC;YAChC,MAAM,QAAQ,GAAuC;gBACnD,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,SAAS;gBACT,QAAQ,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,eAAe,CAAC,CAAC;gBACvD,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,iBAAiB,EAAE,IAAI;gBACvB,gBAAgB,EAAE,KAAK,CAAC,UAAU;aACnC,CAAC;YACF,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,OAAO,CAAC,MAAM,CAAC;IACxB,CAAC;CACF;AA7CD,sCA6CC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.staffEmailListSchema = exports.ConfiguredStaffRecipients = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Staff alert recipients (messaging layer helper): who receives an alert raised for a permission.
|
|
6
|
+
* Today the lists come from configuration; an app can plug in a lookup (e.g. its auth service)
|
|
7
|
+
* by implementing {@link StaffRecipientResolver}.
|
|
8
|
+
*/
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const notification_dispatch_js_1 = require("../contracts/notification-dispatch.js");
|
|
11
|
+
/**
|
|
12
|
+
* Recipients from configuration (env), keyed by the permission an alert targets.
|
|
13
|
+
*
|
|
14
|
+
* @param byPermission Map of permission → recipients, typically built with {@link staffEmailListSchema}.
|
|
15
|
+
* @example new ConfiguredStaffRecipients({ 'payouts.manage': env.FINANCE_ALERT_EMAILS })
|
|
16
|
+
*/
|
|
17
|
+
class ConfiguredStaffRecipients {
|
|
18
|
+
byPermission;
|
|
19
|
+
constructor(byPermission) {
|
|
20
|
+
this.byPermission = byPermission;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* @param permission The permission an alert targets.
|
|
24
|
+
* @returns A copy of the configured recipients, or `[]` when none are configured.
|
|
25
|
+
*/
|
|
26
|
+
async resolve(permission) {
|
|
27
|
+
return [...(this.byPermission[permission] ?? [])];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.ConfiguredStaffRecipients = ConfiguredStaffRecipients;
|
|
31
|
+
const emailSchema = zod_1.z.string().email().max(notification_dispatch_js_1.DISPATCH_LIMITS.email);
|
|
32
|
+
/**
|
|
33
|
+
* A comma-separated staff address list from env (`"a@x.io, b@y.io"`): trimmed, blanks dropped,
|
|
34
|
+
* de-duplicated case-insensitively. An invalid address fails validation, so a typo is caught at boot
|
|
35
|
+
* instead of dead-lettering in the notification-service when the alert finally fires.
|
|
36
|
+
*
|
|
37
|
+
* @returns (on parse) `DispatchRecipient[]` with `email` only.
|
|
38
|
+
* @example const config = z.object({ FINANCE_ALERT_EMAILS: staffEmailListSchema }).parse(process.env);
|
|
39
|
+
*/
|
|
40
|
+
exports.staffEmailListSchema = zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.transform((csv) => {
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
return (csv ?? '')
|
|
46
|
+
.split(',')
|
|
47
|
+
.map((entry) => entry.trim())
|
|
48
|
+
.filter((email) => email && !seen.has(email.toLowerCase()) && seen.add(email.toLowerCase()));
|
|
49
|
+
})
|
|
50
|
+
.superRefine((emails, ctx) => {
|
|
51
|
+
for (const email of emails) {
|
|
52
|
+
if (!emailSchema.safeParse(email).success)
|
|
53
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Not a valid email address: ${email}` });
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.transform((emails) => emails.map((email) => ({ email })));
|
|
57
|
+
//# sourceMappingURL=staff-recipients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staff-recipients.js","sourceRoot":"","sources":["../../../src/messaging/staff-recipients.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,6BAAwB;AACxB,oFAAgG;AAehG;;;;;GAKG;AACH,MAAa,yBAAyB;IACP;IAA7B,YAA6B,YAAoE;QAApE,iBAAY,GAAZ,YAAY,CAAwD;IAAG,CAAC;IAErG;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,UAAkB;QAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,CAAC;CACF;AAVD,8DAUC;AAED,MAAM,WAAW,GAAG,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,0CAAe,CAAC,KAAK,CAAC,CAAC;AAElE;;;;;;;GAOG;AACU,QAAA,oBAAoB,GAAG,OAAC;KAClC,MAAM,EAAE;KACR,QAAQ,EAAE;KACV,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACjG,CAAC,CAAC;KACD,WAAW,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO;YAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAC,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,8BAA8B,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3I,CAAC;AACH,CAAC,CAAC;KACD,SAAS,CAAC,CAAC,MAAM,EAAuB,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC"}
|