@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
|
@@ -1,92 +1,116 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.dispatchDataKeySchema = exports.dispatchRecipientSchema = exports.dispatchValueSchema = exports.DISPATCH_LIMITS = exports.NOTIFICATION_DISPATCH = void 0;
|
|
4
|
+
exports.createNotificationDispatchSchema = createNotificationDispatchSchema;
|
|
5
|
+
/**
|
|
6
|
+
* Notification dispatch: a generic command contract (shared plumbing, no app vocabulary).
|
|
7
|
+
*
|
|
8
|
+
* Services never send email, SMS or push themselves. They stage one dispatch command (default
|
|
9
|
+
* `notification:dispatch`) through their outbox, and the app's notification-service renders a seeded
|
|
10
|
+
* template and delivers it. Both sides build their schema from {@link createNotificationDispatchSchema},
|
|
11
|
+
* so producers and the consumer cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* App-specific parts are parameters: the app passes its own categories (e.g. `security`, `order`) and
|
|
14
|
+
* channels (e.g. `email`, `sms`, `in_app`). Template slugs and their variables also belong to the app
|
|
15
|
+
* (keep a table next to the notification-service seeds and test both sides against it).
|
|
16
|
+
*/
|
|
4
17
|
const zod_1 = require("zod");
|
|
5
18
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and notification-service validates against it, so the two cannot drift.
|
|
19
|
+
* Default command name for a dispatch. An app may use another name and pass it to its producers.
|
|
20
|
+
*
|
|
21
|
+
* @example await outbox.command(tx, NOTIFICATION_DISPATCH, dispatch, { correlationId });
|
|
10
22
|
*/
|
|
11
23
|
exports.NOTIFICATION_DISPATCH = 'notification:dispatch';
|
|
12
|
-
/** Channels a dispatch may ask for. `in_app` needs a recipient `userId` (the bell feed is per user). */
|
|
13
|
-
exports.DISPATCH_CHANNELS = ['email', 'in_app'];
|
|
14
24
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
25
|
+
* Size limits that keep a dispatch small enough for a queue message and safe to render.
|
|
26
|
+
*
|
|
27
|
+
* @example z.string().max(DISPATCH_LIMITS.template)
|
|
17
28
|
*/
|
|
18
|
-
exports.NOTIFICATION_CATEGORIES = ['security', 'account', 'team', 'marketing', 'staff'];
|
|
19
29
|
exports.DISPATCH_LIMITS = {
|
|
20
30
|
template: 100,
|
|
21
31
|
email: 254,
|
|
22
32
|
name: 200,
|
|
33
|
+
userId: 36,
|
|
23
34
|
dataKeys: 40,
|
|
24
35
|
dataKey: 64,
|
|
25
36
|
dataValue: 2000,
|
|
26
37
|
secretKeys: 10,
|
|
27
38
|
permission: 100,
|
|
28
39
|
};
|
|
29
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Template variables are flat scalars: they are printed into the copy, never serialised objects.
|
|
42
|
+
*
|
|
43
|
+
* @example dispatchValueSchema.parse('1250.00')
|
|
44
|
+
*/
|
|
30
45
|
exports.dispatchValueSchema = zod_1.z.union([zod_1.z.string().max(exports.DISPATCH_LIMITS.dataValue), zod_1.z.number().finite(), zod_1.z.boolean(), zod_1.z.null()]);
|
|
46
|
+
/**
|
|
47
|
+
* Who receives a dispatch. `userId` is needed for per-user channels such as an in-app feed;
|
|
48
|
+
* addresses with no account behind them carry only `email`.
|
|
49
|
+
*
|
|
50
|
+
* @example { email: 'wanjiru@example.com', name: 'Wanjiru', userId: '01J…', timezone: 'Africa/Nairobi' }
|
|
51
|
+
*/
|
|
31
52
|
exports.dispatchRecipientSchema = zod_1.z.object({
|
|
32
53
|
email: zod_1.z.string().trim().email().max(exports.DISPATCH_LIMITS.email),
|
|
33
54
|
name: zod_1.z.string().trim().max(exports.DISPATCH_LIMITS.name).optional(),
|
|
34
|
-
userId: zod_1.z.string().max(
|
|
55
|
+
userId: zod_1.z.string().max(exports.DISPATCH_LIMITS.userId).nullish(),
|
|
35
56
|
locale: zod_1.z.string().max(20).nullish(),
|
|
36
57
|
/** IANA zone; time variables are rendered in it. */
|
|
37
58
|
timezone: zod_1.z.string().max(64).nullish(),
|
|
38
59
|
});
|
|
39
|
-
const dataKey = zod_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/, 'Use a template variable name').max(exports.DISPATCH_LIMITS.dataKey);
|
|
40
|
-
exports.notificationDispatchSchema = zod_1.z
|
|
41
|
-
.object({
|
|
42
|
-
/** Seeded `notification_templates.slug`. */
|
|
43
|
-
template: zod_1.z.string().trim().min(1).max(exports.DISPATCH_LIMITS.template),
|
|
44
|
-
category: zod_1.z.enum(exports.NOTIFICATION_CATEGORIES),
|
|
45
|
-
recipient: exports.dispatchRecipientSchema,
|
|
46
|
-
channels: zod_1.z.array(zod_1.z.enum(exports.DISPATCH_CHANNELS)).min(1).max(exports.DISPATCH_CHANNELS.length),
|
|
47
|
-
data: zod_1.z
|
|
48
|
-
.record(dataKey, exports.dispatchValueSchema)
|
|
49
|
-
.default({})
|
|
50
|
-
.refine((data) => Object.keys(data).length <= exports.DISPATCH_LIMITS.dataKeys, `At most ${exports.DISPATCH_LIMITS.dataKeys} variables`),
|
|
51
|
-
/**
|
|
52
|
-
* Keys of `data` holding a one-time secret (a sign-in code, a reset or confirm link). They are used to
|
|
53
|
-
* render the email and nothing else: notification-service never stores them and never puts them in the
|
|
54
|
-
* in-app feed, and email-service masks them in what it keeps.
|
|
55
|
-
*/
|
|
56
|
-
secretKeys: zod_1.z.array(dataKey).max(exports.DISPATCH_LIMITS.secretKeys).default([]),
|
|
57
|
-
/** Security and staff alerts are always delivered, whatever the recipient's preferences. */
|
|
58
|
-
bypassPreferences: zod_1.z.boolean().optional(),
|
|
59
|
-
/** The permission staff recipients hold; informational until recipients are resolved by permission. */
|
|
60
|
-
targetPermission: zod_1.z.string().max(exports.DISPATCH_LIMITS.permission).optional(),
|
|
61
|
-
})
|
|
62
|
-
.superRefine((dispatch, ctx) => {
|
|
63
|
-
for (const key of dispatch.secretKeys) {
|
|
64
|
-
if (!(key in dispatch.data)) {
|
|
65
|
-
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['secretKeys'], message: `Secret key "${key}" is not in data` });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
60
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
61
|
+
* A template variable name the renderer can match.
|
|
62
|
+
*
|
|
63
|
+
* @example dispatchDataKeySchema.parse('expiresInMinutes')
|
|
73
64
|
*/
|
|
74
|
-
exports.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
65
|
+
exports.dispatchDataKeySchema = zod_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/, 'Use a template variable name').max(exports.DISPATCH_LIMITS.dataKey);
|
|
66
|
+
/**
|
|
67
|
+
* Build the dispatch schema for an app's categories and channels. Producers type their payload with it;
|
|
68
|
+
* the notification-service validates incoming commands with the same schema.
|
|
69
|
+
*
|
|
70
|
+
* Rules every app gets: a seeded `template` slug; one or more distinct channels; at most 40 flat-scalar `data`
|
|
71
|
+
* variables with renderer-safe names; `secretKeys` (one-time codes, reset links) must be keys of `data`;
|
|
72
|
+
* the consumer must use them only to render, never store them or show them in a feed.
|
|
73
|
+
*
|
|
74
|
+
* @typeParam C The app's categories (inferred as literals).
|
|
75
|
+
* @typeParam Ch The app's channels (inferred as literals).
|
|
76
|
+
* @param vocabulary The app's categories and channels.
|
|
77
|
+
* @returns A Zod schema; `data` defaults to `{}` and `secretKeys` to `[]`.
|
|
78
|
+
* @example
|
|
79
|
+
* export const dispatchSchema = createNotificationDispatchSchema({
|
|
80
|
+
* categories: ['security', 'account', 'order', 'staff'],
|
|
81
|
+
* channels: ['email', 'sms'],
|
|
82
|
+
* });
|
|
83
|
+
* export type Dispatch = z.input<typeof dispatchSchema>;
|
|
84
|
+
*/
|
|
85
|
+
function createNotificationDispatchSchema(vocabulary) {
|
|
86
|
+
return zod_1.z
|
|
87
|
+
.object({
|
|
88
|
+
/** Seeded template slug in the notification-service. */
|
|
89
|
+
template: zod_1.z.string().trim().min(1).max(exports.DISPATCH_LIMITS.template),
|
|
90
|
+
category: zod_1.z.enum([...vocabulary.categories]),
|
|
91
|
+
recipient: exports.dispatchRecipientSchema,
|
|
92
|
+
channels: zod_1.z
|
|
93
|
+
.array(zod_1.z.enum([...vocabulary.channels]))
|
|
94
|
+
.min(1)
|
|
95
|
+
.max(vocabulary.channels.length)
|
|
96
|
+
.refine((channels) => new Set(channels).size === channels.length, 'Each channel at most once'),
|
|
97
|
+
data: zod_1.z
|
|
98
|
+
.record(exports.dispatchDataKeySchema, exports.dispatchValueSchema)
|
|
99
|
+
.default({})
|
|
100
|
+
.refine((data) => Object.keys(data).length <= exports.DISPATCH_LIMITS.dataKeys, `At most ${exports.DISPATCH_LIMITS.dataKeys} variables`),
|
|
101
|
+
/** Keys of `data` holding a one-time secret: used to render, never stored or shown in a feed. */
|
|
102
|
+
secretKeys: zod_1.z.array(exports.dispatchDataKeySchema).max(exports.DISPATCH_LIMITS.secretKeys).default([]),
|
|
103
|
+
/** Deliver whatever the recipient's notification preferences say (for mail the recipient cannot opt out of). */
|
|
104
|
+
bypassPreferences: zod_1.z.boolean().optional(),
|
|
105
|
+
/** The permission the recipients were resolved by, when they were (traceability only). */
|
|
106
|
+
targetPermission: zod_1.z.string().max(exports.DISPATCH_LIMITS.permission).optional(),
|
|
107
|
+
})
|
|
108
|
+
.superRefine((dispatch, ctx) => {
|
|
109
|
+
for (const key of dispatch.secretKeys) {
|
|
110
|
+
if (!(key in dispatch.data)) {
|
|
111
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['secretKeys'], message: `Secret key "${key}" is not in data` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
92
116
|
//# sourceMappingURL=notification-dispatch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-dispatch.js","sourceRoot":"","sources":["../../../src/contracts/notification-dispatch.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"notification-dispatch.js","sourceRoot":"","sources":["../../../src/contracts/notification-dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/express/auth.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.requireInternalServiceSecret = requireInternalServiceSecret;
|
|
|
7
7
|
const index_js_1 = require("../access/index.js");
|
|
8
8
|
const crypto_js_1 = require("../common/crypto.js");
|
|
9
9
|
const response_js_1 = require("../common/response.js");
|
|
10
|
+
const middleware_js_1 = require("./middleware.js");
|
|
10
11
|
const index_js_2 = require("../identity/index.js");
|
|
11
12
|
function resolveIdentity(req, options) {
|
|
12
13
|
const result = (0, index_js_2.verifyIdentityHeaders)(req.headers, options.secret, { maxSkewMs: options.maxSkewMs });
|
|
@@ -20,19 +21,32 @@ function resolveIdentity(req, options) {
|
|
|
20
21
|
}
|
|
21
22
|
return 'anonymous';
|
|
22
23
|
}
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Requires a valid gateway-signed identity and sets `req.user`; 401 UNAUTHENTICATED (with `requestId`) otherwise.
|
|
26
|
+
*
|
|
27
|
+
* @param options Secret, skew window and logger.
|
|
28
|
+
* @returns Express middleware.
|
|
29
|
+
* @example router.use(authenticateFromGateway({ secret: config.gatewaySecret, logger }));
|
|
30
|
+
*/
|
|
24
31
|
function authenticateFromGateway(options) {
|
|
25
32
|
return (req, res, next) => {
|
|
26
33
|
if (resolveIdentity(req, options) === 'ok')
|
|
27
34
|
return next();
|
|
28
|
-
res.status(401).json((0, response_js_1.
|
|
35
|
+
res.status(401).json((0, response_js_1.errorBody)('UNAUTHENTICATED', 'Authentication required', { requestId: (0, middleware_js_1.correlationId)(req) }));
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Attaches `req.user` when a valid identity is present and continues anonymously when none is sent.
|
|
40
|
+
* A forged or stale identity is still rejected with 401, never downgraded to anonymous.
|
|
41
|
+
*
|
|
42
|
+
* @param options Secret, skew window and logger.
|
|
43
|
+
* @returns Express middleware.
|
|
44
|
+
* @example router.get('/products', optionalAuthFromGateway({ secret }), controller.list);
|
|
45
|
+
*/
|
|
32
46
|
function optionalAuthFromGateway(options) {
|
|
33
47
|
return (req, res, next) => {
|
|
34
48
|
if (resolveIdentity(req, options) === 'invalid') {
|
|
35
|
-
res.status(401).json((0, response_js_1.
|
|
49
|
+
res.status(401).json((0, response_js_1.errorBody)('UNAUTHENTICATED', 'Invalid identity', { requestId: (0, middleware_js_1.correlationId)(req) }));
|
|
36
50
|
return;
|
|
37
51
|
}
|
|
38
52
|
next();
|
|
@@ -41,32 +55,51 @@ function optionalAuthFromGateway(options) {
|
|
|
41
55
|
function guard(check, message = 'You do not have permission to perform this action') {
|
|
42
56
|
return (req, res, next) => {
|
|
43
57
|
if (!req.user) {
|
|
44
|
-
res.status(401).json((0, response_js_1.
|
|
58
|
+
res.status(401).json((0, response_js_1.errorBody)('UNAUTHENTICATED', 'Authentication required', { requestId: (0, middleware_js_1.correlationId)(req) }));
|
|
45
59
|
return;
|
|
46
60
|
}
|
|
47
61
|
if (!check(req.user)) {
|
|
48
|
-
res.status(403).json((0, response_js_1.
|
|
62
|
+
res.status(403).json((0, response_js_1.errorBody)('FORBIDDEN', message, { requestId: (0, middleware_js_1.correlationId)(req) }));
|
|
49
63
|
return;
|
|
50
64
|
}
|
|
51
65
|
next();
|
|
52
66
|
};
|
|
53
67
|
}
|
|
54
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Allows callers holding any of `permissions` (401 without identity, 403 FORBIDDEN otherwise).
|
|
70
|
+
*
|
|
71
|
+
* @example router.post('/refunds', requirePermission('refunds.create'), controller.create);
|
|
72
|
+
*/
|
|
55
73
|
const requirePermission = (...permissions) => guard((u) => (0, index_js_1.hasPermission)(u, ...permissions));
|
|
56
74
|
exports.requirePermission = requirePermission;
|
|
57
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Allows callers holding every one of `permissions`.
|
|
77
|
+
*
|
|
78
|
+
* @example router.delete('/x/:id', requireAllPermissions('x.read', 'x.delete'), controller.remove);
|
|
79
|
+
*/
|
|
58
80
|
const requireAllPermissions = (...permissions) => guard((u) => (0, index_js_1.hasAllPermissions)(u, ...permissions));
|
|
59
81
|
exports.requireAllPermissions = requireAllPermissions;
|
|
60
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Allows callers with any of `roles`. Prefer permissions; roles are for coarse gates only.
|
|
84
|
+
*
|
|
85
|
+
* @example router.use('/ops', requireRole('platform_admin'));
|
|
86
|
+
*/
|
|
61
87
|
const requireRole = (...roles) => guard((u) => (0, index_js_1.hasRole)(u, ...roles), 'Insufficient role');
|
|
62
88
|
exports.requireRole = requireRole;
|
|
63
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Guards `/internal/*` routes: requires the service secret header (constant-time compare);
|
|
91
|
+
* 401 UNAUTHENTICATED with `requestId` otherwise.
|
|
92
|
+
*
|
|
93
|
+
* @param secret The expected `x-internal-service-secret`; an empty secret rejects every call (fail closed).
|
|
94
|
+
* @returns Express middleware.
|
|
95
|
+
* @example router.use('/internal', requireInternalServiceSecret(config.internalSecret));
|
|
96
|
+
*/
|
|
64
97
|
function requireInternalServiceSecret(secret) {
|
|
65
98
|
return (req, res, next) => {
|
|
66
99
|
const provided = req.headers[index_js_2.CONTEXT_HEADERS.INTERNAL_SECRET];
|
|
67
100
|
if (typeof provided === 'string' && secret && (0, crypto_js_1.safeEqual)(provided, secret))
|
|
68
101
|
return next();
|
|
69
|
-
res.status(401).json((0, response_js_1.
|
|
102
|
+
res.status(401).json((0, response_js_1.errorBody)('UNAUTHENTICATED', 'Invalid internal service credentials', { requestId: (0, middleware_js_1.correlationId)(req) }));
|
|
70
103
|
};
|
|
71
104
|
}
|
|
72
105
|
//# sourceMappingURL=auth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/express/auth.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/express/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,4 +18,5 @@ __exportStar(require("./auth.js"), exports);
|
|
|
18
18
|
__exportStar(require("./middleware.js"), exports);
|
|
19
19
|
__exportStar(require("./health.js"), exports);
|
|
20
20
|
__exportStar(require("./security.js"), exports);
|
|
21
|
+
__exportStar(require("./respond.js"), exports);
|
|
21
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/express/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4CAA0B;AAC1B,kDAAgC;AAChC,8CAA4B;AAC5B,gDAA8B"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/express/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4CAA0B;AAC1B,kDAAgC;AAChC,8CAA4B;AAC5B,gDAA8B;AAC9B,+CAA6B"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CORRELATION_ID_PATTERN = void 0;
|
|
3
4
|
exports.correlationMiddleware = correlationMiddleware;
|
|
4
5
|
exports.correlationId = correlationId;
|
|
5
6
|
exports.clientIp = clientIp;
|
|
@@ -12,84 +13,203 @@ exports.notFoundHandler = notFoundHandler;
|
|
|
12
13
|
const errors_js_1 = require("../common/errors.js");
|
|
13
14
|
const id_js_1 = require("../common/id.js");
|
|
14
15
|
const response_js_1 = require("../common/response.js");
|
|
15
|
-
const index_js_1 = require("../
|
|
16
|
-
const index_js_2 = require("../
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const index_js_1 = require("../context/index.js");
|
|
17
|
+
const index_js_2 = require("../identity/index.js");
|
|
18
|
+
const index_js_3 = require("../validation/index.js");
|
|
19
|
+
/** An incoming correlation id is trusted only if it is 8–64 chars of `[A-Za-z0-9-]` (no log injection). */
|
|
20
|
+
exports.CORRELATION_ID_PATTERN = /^[A-Za-z0-9-]{8,64}$/;
|
|
21
|
+
/**
|
|
22
|
+
* Gives every request one correlation id and runs the rest of the request inside a
|
|
23
|
+
* {@link RequestContext}. A valid incoming `x-correlation-id` (see {@link CORRELATION_ID_PATTERN}) is
|
|
24
|
+
* kept; anything else is replaced with a new ULID. The id is echoed on the response, written back to
|
|
25
|
+
* the request header (so outbound calls forward it) and becomes every error body's `requestId`.
|
|
26
|
+
* Mount it first: correlation → json → routes → notFoundHandler → errorHandler.
|
|
27
|
+
*
|
|
28
|
+
* @param serviceName Recorded in the context (and so in log lines written without a service).
|
|
29
|
+
* @returns Express middleware.
|
|
30
|
+
* @example app.use(correlationMiddleware('orders'));
|
|
31
|
+
*/
|
|
32
|
+
function correlationMiddleware(serviceName) {
|
|
19
33
|
return (req, res, next) => {
|
|
20
|
-
const incoming = req.headers[
|
|
21
|
-
const id = typeof incoming === 'string' && incoming
|
|
22
|
-
req.headers[
|
|
23
|
-
res.setHeader(
|
|
24
|
-
|
|
34
|
+
const incoming = req.headers[index_js_2.CONTEXT_HEADERS.CORRELATION_ID];
|
|
35
|
+
const id = typeof incoming === 'string' && exports.CORRELATION_ID_PATTERN.test(incoming) ? incoming : (0, id_js_1.newId)();
|
|
36
|
+
req.headers[index_js_2.CONTEXT_HEADERS.CORRELATION_ID] = id;
|
|
37
|
+
res.setHeader(index_js_2.CONTEXT_HEADERS.CORRELATION_ID, id);
|
|
38
|
+
(0, index_js_1.runWithContext)({ correlationId: id, service: serviceName }, next);
|
|
25
39
|
};
|
|
26
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The request's correlation id (the source of every error body's `requestId`): the active request
|
|
43
|
+
* context's id, else the `x-correlation-id` header.
|
|
44
|
+
*
|
|
45
|
+
* @param req Express request.
|
|
46
|
+
* @returns The id, or undefined when neither is present.
|
|
47
|
+
* @example logger.info('Order placed', { correlationId: correlationId(req) });
|
|
48
|
+
*/
|
|
27
49
|
function correlationId(req) {
|
|
28
|
-
const
|
|
50
|
+
const fromContext = (0, index_js_1.getRequestContext)()?.correlationId;
|
|
51
|
+
if (fromContext)
|
|
52
|
+
return fromContext;
|
|
53
|
+
const value = req.headers[index_js_2.CONTEXT_HEADERS.CORRELATION_ID];
|
|
29
54
|
return typeof value === 'string' ? value : undefined;
|
|
30
55
|
}
|
|
31
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Real client IP: gateway-forwarded header first, then Express's `req.ip`.
|
|
58
|
+
*
|
|
59
|
+
* @param req Express request.
|
|
60
|
+
* @returns The IP, or null when unknown.
|
|
61
|
+
* @example const ip = clientIp(req);
|
|
62
|
+
*/
|
|
32
63
|
function clientIp(req) {
|
|
33
|
-
const forwarded = req.headers[
|
|
64
|
+
const forwarded = req.headers[index_js_2.CONTEXT_HEADERS.CLIENT_IP];
|
|
34
65
|
if (typeof forwarded === 'string' && forwarded)
|
|
35
66
|
return forwarded;
|
|
36
67
|
return req.ip ?? null;
|
|
37
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The caller's IANA time zone as forwarded by the gateway.
|
|
71
|
+
*
|
|
72
|
+
* @param req Express request.
|
|
73
|
+
* @returns e.g. `Europe/Paris`, or null when not sent.
|
|
74
|
+
* @example formatInTimeZone(date, clientTimezone(req) ?? 'UTC');
|
|
75
|
+
*/
|
|
38
76
|
function clientTimezone(req) {
|
|
39
|
-
const tz = req.headers[
|
|
77
|
+
const tz = req.headers[index_js_2.CONTEXT_HEADERS.TIMEZONE];
|
|
40
78
|
return typeof tz === 'string' && tz ? tz : null;
|
|
41
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The anonymous visitor id forwarded by the gateway (for anonymous visitor state, rate limits and analytics).
|
|
82
|
+
*
|
|
83
|
+
* @param req Express request.
|
|
84
|
+
* @returns The id, or null when not sent.
|
|
85
|
+
* @example const visitor = req.user?.id ?? anonId(req);
|
|
86
|
+
*/
|
|
42
87
|
function anonId(req) {
|
|
43
|
-
const id = req.headers[
|
|
88
|
+
const id = req.headers[index_js_2.CONTEXT_HEADERS.ANON_ID];
|
|
44
89
|
return typeof id === 'string' && id ? id : null;
|
|
45
90
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
const PART_ORDER = ['params', 'query', 'body'];
|
|
92
|
+
/** Field-error keys: body fields keep their own path; params/query are prefixed (`query.limit`). */
|
|
93
|
+
function prefixErrors(part, errors) {
|
|
94
|
+
if (part === 'body')
|
|
95
|
+
return errors;
|
|
96
|
+
return Object.fromEntries(Object.entries(errors).map(([key, messages]) => [key === '_' ? part : `${part}.${key}`, messages]));
|
|
97
|
+
}
|
|
98
|
+
function isSchema(value) {
|
|
99
|
+
return typeof value?.safeParse === 'function';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validates the request with Zod at the controller boundary. Each given part is parsed and
|
|
103
|
+
* `req[part]` is replaced with the parsed value, so unknown keys are stripped (or rejected with
|
|
104
|
+
* `.strict()`) and coercions/defaults apply. Every part is checked before failing, and the combined
|
|
105
|
+
* field errors are thrown as a 422 {@link ValidationError} for the error handler (body fields keep
|
|
106
|
+
* their path; params and query fields are prefixed, e.g. `params.id`).
|
|
107
|
+
*
|
|
108
|
+
* The older single-schema form `validate(schema, part = 'body')` still works.
|
|
109
|
+
*
|
|
110
|
+
* @param schemas `{ params?, query?, body? }`, or one schema.
|
|
111
|
+
* @param part With a single schema: the part it checks (default `body`).
|
|
112
|
+
* @returns Express middleware.
|
|
113
|
+
* @example router.patch('/:id', validate({ params: idParam, body: updateSchema }), controller.update);
|
|
114
|
+
*/
|
|
115
|
+
function validate(schemas, part = 'body') {
|
|
116
|
+
const parts = isSchema(schemas) ? { [part]: schemas } : schemas;
|
|
117
|
+
return (req, _res, next) => {
|
|
118
|
+
let errors = {};
|
|
119
|
+
const parsed = {};
|
|
120
|
+
for (const name of PART_ORDER) {
|
|
121
|
+
const schema = parts[name];
|
|
122
|
+
if (!schema)
|
|
123
|
+
continue;
|
|
124
|
+
const result = schema.safeParse(req[name]);
|
|
125
|
+
if (result.success)
|
|
126
|
+
parsed[name] = result.data;
|
|
127
|
+
else
|
|
128
|
+
errors = { ...errors, ...prefixErrors(name, (0, index_js_3.zodIssuesToErrors)(result.error)) };
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(errors).length > 0) {
|
|
131
|
+
next(new errors_js_1.ValidationError(errors));
|
|
52
132
|
return;
|
|
53
133
|
}
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
135
|
+
// Express 5 exposes req.query as a getter; redefine to store the parsed value.
|
|
136
|
+
Object.defineProperty(req, name, { value, writable: true, configurable: true, enumerable: true });
|
|
137
|
+
}
|
|
56
138
|
next();
|
|
57
139
|
};
|
|
58
140
|
}
|
|
59
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Wraps an async handler so rejections reach the error middleware (needed on Express 4; harmless on 5).
|
|
143
|
+
*
|
|
144
|
+
* @param fn Async route handler.
|
|
145
|
+
* @returns A handler that forwards rejections to `next(err)`.
|
|
146
|
+
* @example router.get('/:id', asyncHandler(controller.get));
|
|
147
|
+
*/
|
|
60
148
|
function asyncHandler(fn) {
|
|
61
149
|
return (req, res, next) => {
|
|
62
150
|
fn(req, res, next).catch(next);
|
|
63
151
|
};
|
|
64
152
|
}
|
|
65
|
-
/**
|
|
153
|
+
/** Zod's error, recognised structurally so a service on another zod copy/version still maps to 422. */
|
|
154
|
+
function isZodError(err) {
|
|
155
|
+
return err instanceof Error && err.name === 'ZodError' && Array.isArray(err.issues);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Last error middleware. Every body is `{ success: false, code, message, errors?, requestId }`:
|
|
159
|
+
* - body-parser failures → 400 BAD_REQUEST / 413 PAYLOAD_TOO_LARGE;
|
|
160
|
+
* - a thrown ZodError (e.g. from `schema.parse`) → 422 VALIDATION_FAILED with field errors;
|
|
161
|
+
* - AppError → its status, code and errors; 4xx keep their message, 5xx get a generic one; a
|
|
162
|
+
* `retryAfterSeconds` becomes the `Retry-After` header;
|
|
163
|
+
* - anything else → logged in full, answered 500 INTERNAL_ERROR with a generic message.
|
|
164
|
+
* `requestId` is the request context's correlation id (set by {@link correlationMiddleware}).
|
|
165
|
+
*
|
|
166
|
+
* @param logger Receives 5xx details (message, stack, path, correlation id); never sent to the client.
|
|
167
|
+
* @returns Express error middleware.
|
|
168
|
+
* @example app.use(notFoundHandler()); app.use(errorHandler(logger));
|
|
169
|
+
*/
|
|
66
170
|
function errorHandler(logger) {
|
|
67
|
-
return (err, req, res,
|
|
68
|
-
if (res.headersSent)
|
|
171
|
+
return (err, req, res, next) => {
|
|
172
|
+
if (res.headersSent) {
|
|
173
|
+
next(err);
|
|
69
174
|
return;
|
|
175
|
+
}
|
|
176
|
+
const requestId = correlationId(req);
|
|
70
177
|
const bodyError = err;
|
|
71
178
|
if (bodyError?.type === 'entity.parse.failed') {
|
|
72
|
-
res.status(400).json((0, response_js_1.
|
|
179
|
+
res.status(400).json((0, response_js_1.errorBody)('BAD_REQUEST', 'Malformed JSON body', { requestId }));
|
|
73
180
|
return;
|
|
74
181
|
}
|
|
75
182
|
if (bodyError?.type === 'entity.too.large') {
|
|
76
|
-
res.status(413).json((0, response_js_1.
|
|
183
|
+
res.status(413).json((0, response_js_1.errorBody)('PAYLOAD_TOO_LARGE', 'Request body too large', { requestId }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (isZodError(err)) {
|
|
187
|
+
res.status(422).json((0, response_js_1.errorBody)('VALIDATION_FAILED', 'Validation failed', { errors: (0, index_js_3.zodIssuesToErrors)(err), requestId }));
|
|
77
188
|
return;
|
|
78
189
|
}
|
|
79
190
|
if ((0, errors_js_1.isAppError)(err)) {
|
|
80
191
|
if (err.statusCode >= 500)
|
|
81
|
-
logger?.error(err.message, { path: req.path,
|
|
82
|
-
|
|
192
|
+
logger?.error(err.message, { path: req.path, errorCode: err.code, stack: err.stack, correlationId: requestId });
|
|
193
|
+
if (err.retryAfterSeconds !== undefined)
|
|
194
|
+
res.setHeader('Retry-After', String(Math.max(0, Math.ceil(err.retryAfterSeconds))));
|
|
195
|
+
const errors = err.statusCode < 500 ? err.errors : undefined;
|
|
196
|
+
res.status(err.statusCode).json((0, response_js_1.errorBody)(err.code, (0, errors_js_1.publicMessage)(err), { errors, requestId }));
|
|
83
197
|
return;
|
|
84
198
|
}
|
|
85
199
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
86
|
-
logger?.error('Unhandled error', { error: error.message, stack: error.stack, path: req.path, correlationId:
|
|
87
|
-
res.status(500).json((0, response_js_1.
|
|
200
|
+
logger?.error('Unhandled error', { error: error.message, stack: error.stack, path: req.path, correlationId: requestId });
|
|
201
|
+
res.status(500).json((0, response_js_1.errorBody)('INTERNAL_ERROR', errors_js_1.GENERIC_ERROR_MESSAGE, { requestId }));
|
|
88
202
|
};
|
|
89
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Catch-all for unmatched routes: 404 NOT_FOUND with the request id. Mount after every router.
|
|
206
|
+
*
|
|
207
|
+
* @returns Express middleware.
|
|
208
|
+
* @example app.use(notFoundHandler()); app.use(errorHandler(logger));
|
|
209
|
+
*/
|
|
90
210
|
function notFoundHandler() {
|
|
91
|
-
return (
|
|
92
|
-
res.status(404).json((0, response_js_1.
|
|
211
|
+
return (req, res) => {
|
|
212
|
+
res.status(404).json((0, response_js_1.errorBody)('NOT_FOUND', 'Resource not found', { requestId: correlationId(req) }));
|
|
93
213
|
};
|
|
94
214
|
}
|
|
95
215
|
//# sourceMappingURL=middleware.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../src/express/middleware.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../src/express/middleware.ts"],"names":[],"mappings":"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"}
|