@epztickets/common 1.64.0 → 1.66.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/build/events/subjects.d.ts +1 -0
- package/build/events/subjects.js +6 -0
- package/build/events/user-email-verification-requested-event.d.ts +12 -0
- package/build/events/user-email-verification-requested-event.js +2 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +5 -0
- package/build/logging/http-logger.d.ts +2 -0
- package/build/logging/http-logger.js +80 -0
- package/build/logging/logger.d.ts +8 -0
- package/build/logging/logger.js +52 -0
- package/build/middlewares/current-user.js +53 -2
- package/build/middlewares/request-id.d.ts +10 -0
- package/build/middlewares/request-id.js +35 -0
- package/package.json +4 -2
|
@@ -28,6 +28,7 @@ export declare enum Subjects {
|
|
|
28
28
|
UserDeleted = "events.user.deleted",
|
|
29
29
|
UserDeletionRequested = "events.user.deletion-requested",
|
|
30
30
|
UserDeletionRejected = "events.user.deletion-rejected",
|
|
31
|
+
UserEmailVerificationRequested = "events.user.email-verification-requested",
|
|
31
32
|
ConnectDisconnectRequested = "events.user.connect-disconnect-requested",
|
|
32
33
|
ConnectDisconnectApproved = "events.user.connect-disconnect-approved",
|
|
33
34
|
ConnectDisconnectRejected = "events.user.connect-disconnect-rejected"
|
package/build/events/subjects.js
CHANGED
|
@@ -45,6 +45,12 @@ var Subjects;
|
|
|
45
45
|
Subjects["UserDeleted"] = "events.user.deleted";
|
|
46
46
|
Subjects["UserDeletionRequested"] = "events.user.deletion-requested";
|
|
47
47
|
Subjects["UserDeletionRejected"] = "events.user.deletion-rejected";
|
|
48
|
+
// Auth publishes at signup (and on resend-verification) with the
|
|
49
|
+
// plaintext one-shot token + expiry. notifications-srv consumes
|
|
50
|
+
// and dispatches the verification email. The plaintext is in the
|
|
51
|
+
// event payload because the email is the ONE place it surfaces
|
|
52
|
+
// — auth's User row only has the SHA-256 hash.
|
|
53
|
+
Subjects["UserEmailVerificationRequested"] = "events.user.email-verification-requested";
|
|
48
54
|
// Owner-initiated "stop hosting" lifecycle. Mirrors the deletion
|
|
49
55
|
// request triplet — auth publishes Requested at submission, then
|
|
50
56
|
// either Approved (which IS the disconnect) or Rejected after admin
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Subjects } from "./subjects";
|
|
2
|
+
export interface UserEmailVerificationRequestedEvent {
|
|
3
|
+
subject: Subjects.UserEmailVerificationRequested;
|
|
4
|
+
data: {
|
|
5
|
+
userId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
/** Plaintext verification token to embed in the email link. */
|
|
8
|
+
token: string;
|
|
9
|
+
/** ISO timestamp of when the token expires (24h after issue). */
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export * from "./middlewares/require-auth";
|
|
|
11
11
|
export * from "./middlewares/validate-request";
|
|
12
12
|
export * from "./middlewares/require-owner";
|
|
13
13
|
export * from "./middlewares/require-admin";
|
|
14
|
+
export * from "./middlewares/request-id";
|
|
15
|
+
export * from "./logging/logger";
|
|
16
|
+
export * from "./logging/http-logger";
|
|
14
17
|
export * from "./events/subjects";
|
|
15
18
|
export * from "./events/types/order-status";
|
|
16
19
|
export * from "./events/order-ticket-cancelled-event";
|
|
@@ -45,6 +48,7 @@ export * from "./events/user-updated-event";
|
|
|
45
48
|
export * from "./events/user-deleted-event";
|
|
46
49
|
export * from "./events/user-deletion-requested-event";
|
|
47
50
|
export * from "./events/user-deletion-rejected-event";
|
|
51
|
+
export * from "./events/user-email-verification-requested-event";
|
|
48
52
|
export * from "./events/connect-disconnect-requested-event";
|
|
49
53
|
export * from "./events/connect-disconnect-approved-event";
|
|
50
54
|
export * from "./events/connect-disconnect-rejected-event";
|
package/build/index.js
CHANGED
|
@@ -29,6 +29,10 @@ __exportStar(require("./middlewares/require-auth"), exports);
|
|
|
29
29
|
__exportStar(require("./middlewares/validate-request"), exports);
|
|
30
30
|
__exportStar(require("./middlewares/require-owner"), exports);
|
|
31
31
|
__exportStar(require("./middlewares/require-admin"), exports);
|
|
32
|
+
__exportStar(require("./middlewares/request-id"), exports);
|
|
33
|
+
// logging
|
|
34
|
+
__exportStar(require("./logging/logger"), exports);
|
|
35
|
+
__exportStar(require("./logging/http-logger"), exports);
|
|
32
36
|
// events
|
|
33
37
|
__exportStar(require("./events/subjects"), exports);
|
|
34
38
|
__exportStar(require("./events/types/order-status"), exports);
|
|
@@ -64,6 +68,7 @@ __exportStar(require("./events/user-updated-event"), exports);
|
|
|
64
68
|
__exportStar(require("./events/user-deleted-event"), exports);
|
|
65
69
|
__exportStar(require("./events/user-deletion-requested-event"), exports);
|
|
66
70
|
__exportStar(require("./events/user-deletion-rejected-event"), exports);
|
|
71
|
+
__exportStar(require("./events/user-email-verification-requested-event"), exports);
|
|
67
72
|
__exportStar(require("./events/connect-disconnect-requested-event"), exports);
|
|
68
73
|
__exportStar(require("./events/connect-disconnect-approved-event"), exports);
|
|
69
74
|
__exportStar(require("./events/connect-disconnect-rejected-event"), exports);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createHttpLogger = createHttpLogger;
|
|
7
|
+
const pino_http_1 = __importDefault(require("pino-http"));
|
|
8
|
+
// Per-request access-log middleware. Builds on pino-http with the
|
|
9
|
+
// conventions we want across services:
|
|
10
|
+
//
|
|
11
|
+
// - Uses req.id (set by the requestId middleware) as the
|
|
12
|
+
// per-request log correlation id. Echoes it as `req.id` on
|
|
13
|
+
// every line.
|
|
14
|
+
// - Includes the route, method, status, response time (ms), and
|
|
15
|
+
// authenticated user id when present.
|
|
16
|
+
// - Demotes 4xx-but-expected statuses (401/403/404) from "warn"
|
|
17
|
+
// to "info" — they're routine and shouldn't fire log-volume
|
|
18
|
+
// alerts. Genuine 5xx stays "error".
|
|
19
|
+
// - Silences health-check noise. The k8s liveness/readiness
|
|
20
|
+
// probes hit /healthz + /readyz once per second per pod, which
|
|
21
|
+
// is a 7000x amplifier on log volume if we don't filter them.
|
|
22
|
+
//
|
|
23
|
+
// Mount this AFTER the requestId middleware and BEFORE the routes
|
|
24
|
+
// so every route handler can call req.log.info(...) with the per-
|
|
25
|
+
// request child logger already attached.
|
|
26
|
+
function createHttpLogger(logger) {
|
|
27
|
+
return (0, pino_http_1.default)({
|
|
28
|
+
logger,
|
|
29
|
+
// Reuse the id set by the requestId middleware. pino-http would
|
|
30
|
+
// otherwise generate its own (different) id, which defeats the
|
|
31
|
+
// whole correlation-with-the-X-Request-Id-header idea.
|
|
32
|
+
genReqId: (req) => { var _a; return (_a = req.id) !== null && _a !== void 0 ? _a : "no-id"; },
|
|
33
|
+
// Per-line log level. 5xx → error, 4xx → warn EXCEPT for the
|
|
34
|
+
// expected auth-related statuses which we keep at info to avoid
|
|
35
|
+
// alert fatigue.
|
|
36
|
+
customLogLevel: (_req, res, err) => {
|
|
37
|
+
if (err || res.statusCode >= 500)
|
|
38
|
+
return "error";
|
|
39
|
+
if (res.statusCode === 401 ||
|
|
40
|
+
res.statusCode === 403 ||
|
|
41
|
+
res.statusCode === 404) {
|
|
42
|
+
return "info";
|
|
43
|
+
}
|
|
44
|
+
if (res.statusCode >= 400)
|
|
45
|
+
return "warn";
|
|
46
|
+
return "info";
|
|
47
|
+
},
|
|
48
|
+
// Trim the access-log shape — pino-http's default dumps the
|
|
49
|
+
// entire req object which is verbose and noisy in dashboards.
|
|
50
|
+
serializers: {
|
|
51
|
+
req: (req) => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
return ({
|
|
54
|
+
id: req.id,
|
|
55
|
+
method: req.method,
|
|
56
|
+
url: req.url,
|
|
57
|
+
// userId comes from common's currentUser middleware. Helpful
|
|
58
|
+
// for "show me all failures by user X" queries. Optional —
|
|
59
|
+
// anonymous requests don't have one.
|
|
60
|
+
userId: (_b = (_a = req.raw) === null || _a === void 0 ? void 0 : _a.currentUser) === null || _b === void 0 ? void 0 : _b.id,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
res: (res) => ({ statusCode: res.statusCode }),
|
|
64
|
+
},
|
|
65
|
+
// Mute k8s probe traffic. These hit ~1/sec per pod and would
|
|
66
|
+
// otherwise dominate the log volume.
|
|
67
|
+
autoLogging: {
|
|
68
|
+
ignore: (req) => {
|
|
69
|
+
var _a;
|
|
70
|
+
const url = (_a = req.url) !== null && _a !== void 0 ? _a : "";
|
|
71
|
+
return url === "/healthz" || url === "/readyz";
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
// Per-request log message. Keep it short and consistent across
|
|
75
|
+
// services so a dashboard filtering on `msg: "request"` catches
|
|
76
|
+
// everything.
|
|
77
|
+
customSuccessMessage: () => "request",
|
|
78
|
+
customErrorMessage: () => "request",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
/** Service name — emitted on every line as `service` for filtering. */
|
|
4
|
+
service: string;
|
|
5
|
+
/** Override the global level (defaults to LOG_LEVEL env or "info"). */
|
|
6
|
+
level?: pino.Level;
|
|
7
|
+
}
|
|
8
|
+
export declare function createLogger(serviceOrOpts: string | LoggerOptions): pino.Logger;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createLogger = createLogger;
|
|
7
|
+
const pino_1 = __importDefault(require("pino"));
|
|
8
|
+
const isTest = process.env.NODE_ENV === "test";
|
|
9
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
10
|
+
function createLogger(serviceOrOpts) {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const opts = typeof serviceOrOpts === "string"
|
|
13
|
+
? { service: serviceOrOpts }
|
|
14
|
+
: serviceOrOpts;
|
|
15
|
+
const level = (_a = opts.level) !== null && _a !== void 0 ? _a : (isTest ? "silent" : (_b = process.env.LOG_LEVEL) !== null && _b !== void 0 ? _b : "info");
|
|
16
|
+
return (0, pino_1.default)({
|
|
17
|
+
name: opts.service,
|
|
18
|
+
level,
|
|
19
|
+
// Dev convenience: pretty-print colored lines instead of raw JSON.
|
|
20
|
+
// Production gets the raw JSON which is what log aggregators want.
|
|
21
|
+
transport: !isProd && !isTest
|
|
22
|
+
? {
|
|
23
|
+
target: "pino-pretty",
|
|
24
|
+
options: {
|
|
25
|
+
colorize: true,
|
|
26
|
+
translateTime: "SYS:HH:MM:ss.l",
|
|
27
|
+
ignore: "pid,hostname",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: undefined,
|
|
31
|
+
// Standardize the field names across services. Without these,
|
|
32
|
+
// some pino versions emit "time" while others emit "timestamp",
|
|
33
|
+
// making cross-service correlation harder.
|
|
34
|
+
timestamp: pino_1.default.stdTimeFunctions.isoTime,
|
|
35
|
+
formatters: {
|
|
36
|
+
level: (label) => ({ level: label }),
|
|
37
|
+
},
|
|
38
|
+
// Strip these standard fields from PII when they ride along on
|
|
39
|
+
// logged error objects. Add more as we discover them.
|
|
40
|
+
redact: {
|
|
41
|
+
paths: [
|
|
42
|
+
"req.headers.authorization",
|
|
43
|
+
"req.headers.cookie",
|
|
44
|
+
"*.password",
|
|
45
|
+
"*.token",
|
|
46
|
+
"*.refreshToken",
|
|
47
|
+
"*.jwt",
|
|
48
|
+
],
|
|
49
|
+
remove: true,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -5,15 +5,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.currentUser = void 0;
|
|
7
7
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
// Look up the public key for a given `kid` (key id, from the JWT
|
|
9
|
+
// header). Falls through to single-key JWT_PUBLIC_KEY if either no
|
|
10
|
+
// kid is present (legacy token issued before rotation was wired)
|
|
11
|
+
// or the keyed env var isn't set (still operating in single-key
|
|
12
|
+
// mode pre-rotation).
|
|
13
|
+
//
|
|
14
|
+
// Two supported env layouts:
|
|
15
|
+
//
|
|
16
|
+
// Single-key (legacy, default):
|
|
17
|
+
// JWT_PUBLIC_KEY=<pem>
|
|
18
|
+
//
|
|
19
|
+
// Multi-key (during/after rotation):
|
|
20
|
+
// JWT_PUBLIC_KEYS={"kid-1":"<pem>","kid-2":"<pem>"}
|
|
21
|
+
// JWT_PUBLIC_KEY=<pem> (still accepted as fallback)
|
|
22
|
+
//
|
|
23
|
+
// JWT_PUBLIC_KEYS holds a JSON object mapping kid → PEM. The PEMs
|
|
24
|
+
// have embedded newlines which is awkward in shell env vars; the
|
|
25
|
+
// production convention is to base64-encode each PEM in the JSON
|
|
26
|
+
// value and decode here. To keep this middleware compatible with
|
|
27
|
+
// plain-text PEMs too (the common case in tests + dev), we accept
|
|
28
|
+
// both: if a value contains "-----BEGIN" it's used as-is; otherwise
|
|
29
|
+
// we base64-decode it.
|
|
30
|
+
//
|
|
31
|
+
// The JSON parse + decode runs ON EACH REQUEST today. That's fine
|
|
32
|
+
// for current traffic; if it ever becomes hot, memoize at first use.
|
|
33
|
+
function lookupPublicKey(kid) {
|
|
34
|
+
var _a;
|
|
35
|
+
const mapJson = process.env.JWT_PUBLIC_KEYS;
|
|
36
|
+
if (kid && mapJson) {
|
|
37
|
+
try {
|
|
38
|
+
const map = JSON.parse(mapJson);
|
|
39
|
+
const value = map[kid];
|
|
40
|
+
if (value) {
|
|
41
|
+
return value.includes("-----BEGIN")
|
|
42
|
+
? value
|
|
43
|
+
: Buffer.from(value, "base64").toString("utf8");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error("JWT_PUBLIC_KEYS is set but not valid JSON; falling back to JWT_PUBLIC_KEY", err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return (_a = process.env.JWT_PUBLIC_KEY) !== null && _a !== void 0 ? _a : null;
|
|
51
|
+
}
|
|
8
52
|
const currentUser = (req, res, next) => {
|
|
9
53
|
var _a;
|
|
10
54
|
if (!((_a = req.session) === null || _a === void 0 ? void 0 : _a.jwt)) {
|
|
11
55
|
return next();
|
|
12
56
|
}
|
|
13
57
|
try {
|
|
14
|
-
|
|
58
|
+
// Decode without verifying to read the `kid` header so we can
|
|
59
|
+
// pick the right public key. The actual signature check happens
|
|
60
|
+
// in the verify() call below — decode is just for routing.
|
|
61
|
+
const decoded = jsonwebtoken_1.default.decode(req.session.jwt, { complete: true });
|
|
62
|
+
const headerKid = decoded && typeof decoded !== "string"
|
|
63
|
+
? decoded.header.kid
|
|
64
|
+
: undefined;
|
|
65
|
+
const publicKey = lookupPublicKey(headerKid);
|
|
15
66
|
if (!publicKey) {
|
|
16
|
-
console.error("
|
|
67
|
+
console.error(`JWT verify: no public key configured (kid="${headerKid !== null && headerKid !== void 0 ? headerKid : "none"}")`);
|
|
17
68
|
return next();
|
|
18
69
|
}
|
|
19
70
|
const payload = jsonwebtoken_1.default.verify(req.session.jwt, publicKey, {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
declare global {
|
|
3
|
+
namespace Express {
|
|
4
|
+
interface Request {
|
|
5
|
+
/** Stable id for this request, set by requestId middleware. */
|
|
6
|
+
id?: string;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export declare const requestId: (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestId = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
// Request-ID middleware.
|
|
6
|
+
//
|
|
7
|
+
// Accepts an `X-Request-Id` header from the caller (e.g. the
|
|
8
|
+
// reservation-frontend's axiosClient, an upstream proxy, or another
|
|
9
|
+
// service forwarding context). When absent, generates a fresh UUID.
|
|
10
|
+
// Always echoes the chosen id back in the response header so the
|
|
11
|
+
// client can record it for later support requests.
|
|
12
|
+
//
|
|
13
|
+
// Why this matters: without per-request ids, log lines from
|
|
14
|
+
// concurrent requests interleave with no way to tell which "auth
|
|
15
|
+
// failed" log goes with which "checkout 500'd" log. With ids, every
|
|
16
|
+
// pino log line emitted during the request lifetime carries the
|
|
17
|
+
// same id (via pino-http's auto-child-logger), and the response
|
|
18
|
+
// header lets you grep one id across every service's logs to
|
|
19
|
+
// reconstruct the full causal chain.
|
|
20
|
+
//
|
|
21
|
+
// Mount BEFORE pino-http so the request id is available when
|
|
22
|
+
// pino-http creates the per-request child logger.
|
|
23
|
+
const HEADER = "x-request-id";
|
|
24
|
+
// Permissive — UUIDs, ULIDs, kubectl-style request IDs all fit. We
|
|
25
|
+
// just want to reject obviously-malicious inputs that would mess
|
|
26
|
+
// with our log files (newlines, control chars, absurd lengths).
|
|
27
|
+
const ID_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
|
|
28
|
+
const requestId = (req, res, next) => {
|
|
29
|
+
const incoming = req.get(HEADER);
|
|
30
|
+
const id = incoming && ID_REGEX.test(incoming) ? incoming : (0, crypto_1.randomUUID)();
|
|
31
|
+
req.id = id;
|
|
32
|
+
res.setHeader("X-Request-Id", id);
|
|
33
|
+
next();
|
|
34
|
+
};
|
|
35
|
+
exports.requestId = requestId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epztickets/common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.66.0",
|
|
4
4
|
"main": "./build/index.js",
|
|
5
5
|
"types": "./build/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"express": "^4.21.2",
|
|
34
34
|
"express-validator": "^7.2.1",
|
|
35
35
|
"jsonwebtoken": "^9.0.2",
|
|
36
|
-
"nats": "^2.29.3"
|
|
36
|
+
"nats": "^2.29.3",
|
|
37
|
+
"pino": "^9.5.0",
|
|
38
|
+
"pino-http": "^10.3.0"
|
|
37
39
|
}
|
|
38
40
|
}
|