@lucaapp/service-utils 1.0.0 → 1.2.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/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export * from './lib/kafka-client';
1
+ export * from './lib/kafka';
2
+ export * from './lib/serviceIdentity';
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./lib/kafka-client"), exports);
17
+ __exportStar(require("./lib/kafka"), exports);
18
+ __exportStar(require("./lib/serviceIdentity"), exports);
@@ -0,0 +1,8 @@
1
+ declare type Consumer = {
2
+ uuid: string;
3
+ username: string;
4
+ createdAt: Date;
5
+ updatedAt: Date;
6
+ deletedAt: Date | null;
7
+ };
8
+ export type { Consumer };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,18 @@
1
+ declare type Operator = {
2
+ uuid: string;
3
+ firstName: string;
4
+ lastName: string;
5
+ email: string;
6
+ username: string;
7
+ activated: boolean;
8
+ phone: string | null;
9
+ businessEntityName: string | null;
10
+ businessEntityStreetName: string | null;
11
+ businessEntityStreetNumber: string | null;
12
+ businessEntityZipCode: string | null;
13
+ businessEntityCity: string | null;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ deletedAt: Date | null;
17
+ };
18
+ export type { Operator };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ declare type Payment = {
2
+ uuid: string;
3
+ locationId: string;
4
+ locationName: string | null;
5
+ table: string | null;
6
+ paymentRequestId: string | null;
7
+ paymentCreatedAt: Date;
8
+ paidAt: Date | null;
9
+ paymentVerifier: string;
10
+ totalAmount: number;
11
+ invoiceAmount: number;
12
+ tipAmount: number;
13
+ fixedFee: number;
14
+ variableFee: number;
15
+ rapydPaymentId: string | null;
16
+ rapydErrorCode: string | null;
17
+ rapydErrorMessage: string | null;
18
+ rapydPaymentStatus: 'ACT' | 'CAN' | 'CLO' | 'ERR' | 'EXP' | 'NEW' | 'REV';
19
+ rapydCustomerId: string | null;
20
+ payoutId: string | null;
21
+ statusActiveAt: Date | null;
22
+ statusCanceledAt: Date | null;
23
+ statusClosedAt: Date | null;
24
+ statusErrorAt: Date | null;
25
+ statusExpiredAt: Date | null;
26
+ statusNewAt: Date | null;
27
+ statusReversedAt?: Date | null;
28
+ };
29
+ export type { Payment };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,20 @@
1
+ import type { Payment } from './events/payment';
2
+ import type { Consumer } from './events/consumer';
3
+ import type { Operator } from './events/operator';
4
+ declare enum KafkaTopic {
5
+ PAYMENTS = "PAYMENTS",
6
+ CONSUMERS = "CONSUMERS",
7
+ OPERATORS = "OPERATORS"
8
+ }
9
+ declare type MessageFormats = {
10
+ [KafkaTopic.PAYMENTS]: Payment;
11
+ [KafkaTopic.CONSUMERS]: Consumer;
12
+ [KafkaTopic.OPERATORS]: Operator;
13
+ };
14
+ declare const MessageIssuer: {
15
+ PAYMENTS: string;
16
+ CONSUMERS: string;
17
+ OPERATORS: string;
18
+ };
19
+ export { KafkaTopic, MessageIssuer };
20
+ export type { MessageFormats };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessageIssuer = exports.KafkaTopic = void 0;
4
+ var KafkaTopic;
5
+ (function (KafkaTopic) {
6
+ KafkaTopic["PAYMENTS"] = "PAYMENTS";
7
+ KafkaTopic["CONSUMERS"] = "CONSUMERS";
8
+ KafkaTopic["OPERATORS"] = "OPERATORS";
9
+ })(KafkaTopic || (KafkaTopic = {}));
10
+ exports.KafkaTopic = KafkaTopic;
11
+ const MessageIssuer = {
12
+ [KafkaTopic.PAYMENTS]: 'backend-pay',
13
+ [KafkaTopic.CONSUMERS]: 'backend',
14
+ [KafkaTopic.OPERATORS]: 'backend',
15
+ };
16
+ exports.MessageIssuer = MessageIssuer;
@@ -0,0 +1,2 @@
1
+ export { KafkaClient } from './kafkaClient';
2
+ export { KafkaTopic } from './events';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KafkaTopic = exports.KafkaClient = void 0;
4
+ var kafkaClient_1 = require("./kafkaClient");
5
+ Object.defineProperty(exports, "KafkaClient", { enumerable: true, get: function () { return kafkaClient_1.KafkaClient; } });
6
+ var events_1 = require("./events");
7
+ Object.defineProperty(exports, "KafkaTopic", { enumerable: true, get: function () { return events_1.KafkaTopic; } });
@@ -0,0 +1,26 @@
1
+ import { Consumer } from 'kafkajs';
2
+ import { Logger } from 'pino';
3
+ import { ServiceIdentity } from '../serviceIdentity';
4
+ import type { EventPayloadHandler, KafkaConfiguration, KafkaEvent, KafkaTopics, KafkaTopicType } from './types';
5
+ declare class KafkaClient {
6
+ private readonly environment;
7
+ private readonly kafkaClient;
8
+ private readonly logger;
9
+ private readonly topicSecrets;
10
+ private readonly admin;
11
+ private readonly producer;
12
+ private readonly serviceIdentity;
13
+ constructor(parentLogger: Logger, kafkaConfig: KafkaConfiguration, topicSecrets: Record<KafkaTopics, string | null>, serviceIdentity: ServiceIdentity);
14
+ connect: () => Promise<void>;
15
+ private getTopic;
16
+ private getTopicSecret;
17
+ private encryptValue;
18
+ private decryptValue;
19
+ private generateSignature;
20
+ private verifySignature;
21
+ private parseValue;
22
+ private ensureTopics;
23
+ consume: <T extends KafkaTopicType>(groupId: string, kafkaTopic: T, handler: EventPayloadHandler<T>, fromBeginning?: boolean) => Promise<Consumer>;
24
+ produce: <T extends KafkaTopicType>(kafkaTopic: T, key: string, value: KafkaEvent<T>) => Promise<void>;
25
+ }
26
+ export { KafkaClient };
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.KafkaClient = void 0;
27
+ const kafkajs_1 = require("kafkajs");
28
+ const jose = __importStar(require("jose"));
29
+ const util_1 = require("util");
30
+ const serviceIdentity_1 = require("../serviceIdentity");
31
+ const events_1 = require("./events");
32
+ const utils_1 = require("../../utils/utils");
33
+ const KEY_ALG = 'ES256';
34
+ const getIssuer = (kafkaTopic) => {
35
+ return events_1.MessageIssuer[kafkaTopic].valueOf();
36
+ };
37
+ class KafkaClient {
38
+ constructor(parentLogger, kafkaConfig, topicSecrets, serviceIdentity) {
39
+ this.connect = async () => {
40
+ try {
41
+ await this.admin.connect();
42
+ await this.producer.connect();
43
+ }
44
+ catch (error) {
45
+ throw (0, utils_1.logAndGetError)(this.logger, 'Unable to connect kafkaClient', error);
46
+ }
47
+ };
48
+ this.getTopic = async (kafkaTopic) => {
49
+ const topic = `${this.environment}_${this.serviceIdentity.identityName}_${kafkaTopic}`;
50
+ await this.ensureTopics(topic);
51
+ return topic;
52
+ };
53
+ this.getTopicSecret = (topic) => {
54
+ const topicSecret = this.topicSecrets[topic];
55
+ if (!topicSecret) {
56
+ throw (0, utils_1.logAndGetError)(this.logger, `No secret present for topic=${topic}`);
57
+ }
58
+ return Buffer.from(topicSecret);
59
+ };
60
+ this.encryptValue = async (topic, value) => {
61
+ if (!value) {
62
+ throw (0, utils_1.logAndGetError)(this.logger, 'Invalid value argument `null | undefined` supplied.');
63
+ }
64
+ const jwe = await new jose.CompactEncrypt(new util_1.TextEncoder().encode(value));
65
+ jwe.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM' });
66
+ return jwe.encrypt(this.getTopicSecret(topic));
67
+ };
68
+ this.decryptValue = async (topic, jwe) => {
69
+ if (!jwe) {
70
+ return null;
71
+ }
72
+ const { plaintext } = await jose.compactDecrypt(jwe, this.getTopicSecret(topic));
73
+ return Buffer.from(plaintext);
74
+ };
75
+ this.generateSignature = async (value) => {
76
+ const privateKey = await this.serviceIdentity.getIdentityPrivateKey();
77
+ return await new jose.CompactSign(new util_1.TextEncoder().encode(value))
78
+ .setProtectedHeader({ alg: KEY_ALG })
79
+ .sign(privateKey);
80
+ };
81
+ this.verifySignature = async (kafkaTopic, value, headers) => {
82
+ if (!headers || !headers.signature) {
83
+ throw (0, utils_1.logAndGetError)(this.logger, 'Unable to verify signature. Expected header not present');
84
+ }
85
+ const issuer = getIssuer(kafkaTopic);
86
+ const { signature } = headers;
87
+ const jwks = await this.serviceIdentity.getRemoteJWKS(issuer);
88
+ if (!jwks) {
89
+ throw (0, utils_1.logAndGetError)(this.logger, `Unable to find jwks for issuer=${issuer}`);
90
+ }
91
+ try {
92
+ const { payload } = await jose.compactVerify(signature, jwks);
93
+ if (payload.toString() !== value?.toString()) {
94
+ // noinspection ExceptionCaughtLocallyJS
95
+ throw (0, utils_1.logAndGetError)(this.logger, `Signed payload does not match signature for event from ${issuer}`);
96
+ }
97
+ else {
98
+ this.logger.debug('Successfully validated signature');
99
+ }
100
+ }
101
+ catch (error) {
102
+ throw (0, utils_1.logAndGetError)(this.logger, `Unable to verify signature for event from ${issuer}`, error);
103
+ }
104
+ };
105
+ this.parseValue = (value) => {
106
+ if (!value) {
107
+ throw (0, utils_1.logAndGetError)(this.logger, 'Unexpected event format `null`');
108
+ }
109
+ return JSON.parse(value.toString());
110
+ };
111
+ this.ensureTopics = async (topic) => {
112
+ const existingTopics = await this.admin.listTopics();
113
+ if (existingTopics.includes(topic)) {
114
+ this.logger.debug(`Topic=${topic} already exists. Not creating.`);
115
+ return;
116
+ }
117
+ await this.admin.createTopics({ topics: [{ topic }] });
118
+ if (this.environment !== serviceIdentity_1.Environment.LOCAL) {
119
+ await this.admin.createPartitions({
120
+ topicPartitions: [{ topic, count: 2 }],
121
+ });
122
+ }
123
+ this.logger.debug(`Topic=${topic} created.`);
124
+ };
125
+ this.consume = async (groupId, kafkaTopic, handler, fromBeginning = false) => {
126
+ const topic = await this.getTopic(kafkaTopic);
127
+ try {
128
+ const consumer = this.kafkaClient.consumer({ groupId });
129
+ await consumer.connect();
130
+ await consumer.subscribe({ topic, fromBeginning });
131
+ await consumer.run({
132
+ autoCommit: true,
133
+ eachMessage: async ({ message }) => {
134
+ const decryptedValue = await this.decryptValue(kafkaTopic, message.value);
135
+ await this.verifySignature(kafkaTopic, decryptedValue, message.headers);
136
+ const value = this.parseValue(decryptedValue);
137
+ await handler({ ...message, value });
138
+ },
139
+ });
140
+ return consumer;
141
+ }
142
+ catch (error) {
143
+ throw (0, utils_1.logAndGetError)(this.logger, `Could not create consumer for groupId=${groupId}, topic=${topic}`, error);
144
+ }
145
+ };
146
+ this.produce = async (kafkaTopic, key, value) => {
147
+ const topic = await this.getTopic(kafkaTopic);
148
+ const serializedValue = JSON.stringify(value);
149
+ const encryptedValue = await this.encryptValue(kafkaTopic, serializedValue);
150
+ const signature = await this.generateSignature(serializedValue);
151
+ try {
152
+ const producerRecord = {
153
+ topic,
154
+ messages: [
155
+ {
156
+ key,
157
+ value: encryptedValue,
158
+ headers: { signature },
159
+ },
160
+ ],
161
+ };
162
+ await this.producer.send(producerRecord);
163
+ this.logger.debug(producerRecord, 'Record sent');
164
+ }
165
+ catch (error) {
166
+ throw (0, utils_1.logAndGetError)(this.logger, `Could not create producer for topic=${topic}`, error);
167
+ }
168
+ };
169
+ this.environment = kafkaConfig.environment;
170
+ this.logger = parentLogger.child({
171
+ kafkaClientId: kafkaConfig.clientId,
172
+ });
173
+ this.serviceIdentity = serviceIdentity;
174
+ this.topicSecrets = topicSecrets;
175
+ try {
176
+ this.kafkaClient = new kafkajs_1.Kafka({
177
+ brokers: [kafkaConfig.broker],
178
+ clientId: kafkaConfig.clientId,
179
+ ...(kafkaConfig?.ssl &&
180
+ kafkaConfig.username &&
181
+ kafkaConfig.password && {
182
+ ssl: {
183
+ rejectUnauthorized: false,
184
+ },
185
+ sasl: {
186
+ mechanism: 'plain',
187
+ username: kafkaConfig.username,
188
+ password: kafkaConfig.password,
189
+ },
190
+ }),
191
+ });
192
+ this.admin = this.kafkaClient.admin();
193
+ this.producer = this.kafkaClient.producer();
194
+ }
195
+ catch (error) {
196
+ throw (0, utils_1.logAndGetError)(this.logger, 'Unable to connect to Kafka', error);
197
+ }
198
+ }
199
+ }
200
+ exports.KafkaClient = KafkaClient;
@@ -0,0 +1,22 @@
1
+ import { KafkaTopic, MessageFormats } from './events';
2
+ import { Environment } from '../serviceIdentity';
3
+ import { KafkaMessage } from 'kafkajs';
4
+ declare type KafkaTopics = keyof typeof KafkaTopic;
5
+ declare type KafkaTopicType = typeof KafkaTopic[KafkaTopics];
6
+ declare type KafkaEvent<T extends KafkaTopicType> = {
7
+ id: string;
8
+ type: 'create' | 'update' | 'destroy';
9
+ entity: MessageFormats[T];
10
+ };
11
+ declare type KafkaConfiguration = {
12
+ environment: Environment;
13
+ broker: string;
14
+ clientId: string;
15
+ username?: string;
16
+ password?: string;
17
+ ssl?: boolean;
18
+ };
19
+ declare type EventPayloadHandler<T extends KafkaTopicType> = (message: Omit<KafkaMessage, 'value'> & {
20
+ value: KafkaEvent<T>;
21
+ }) => Promise<void>;
22
+ export type { KafkaTopics, KafkaTopicType, KafkaEvent, KafkaConfiguration, EventPayloadHandler, };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,19 @@
1
+ declare enum Environment {
2
+ LOCAL = "LOCAL",
3
+ DEV = "DEV",
4
+ QS = "QS",
5
+ AQS = "AQS",
6
+ RELEASE = "RELEASE",
7
+ HOTFIX = "HOTFIX",
8
+ PENTEST = "PENTEST",
9
+ P1 = "P1",
10
+ P2 = "P2",
11
+ P3 = "P3",
12
+ P4 = "P4",
13
+ P5 = "P5",
14
+ P6 = "P6",
15
+ DEMO = "DEMO",
16
+ PREPROD = "PREPROD",
17
+ PROD = "PROD"
18
+ }
19
+ export { Environment };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Environment = void 0;
4
+ var Environment;
5
+ (function (Environment) {
6
+ Environment["LOCAL"] = "LOCAL";
7
+ Environment["DEV"] = "DEV";
8
+ Environment["QS"] = "QS";
9
+ Environment["AQS"] = "AQS";
10
+ Environment["RELEASE"] = "RELEASE";
11
+ Environment["HOTFIX"] = "HOTFIX";
12
+ Environment["PENTEST"] = "PENTEST";
13
+ Environment["P1"] = "P1";
14
+ Environment["P2"] = "P2";
15
+ Environment["P3"] = "P3";
16
+ Environment["P4"] = "P4";
17
+ Environment["P5"] = "P5";
18
+ Environment["P6"] = "P6";
19
+ Environment["DEMO"] = "DEMO";
20
+ Environment["PREPROD"] = "PREPROD";
21
+ Environment["PROD"] = "PROD";
22
+ })(Environment || (Environment = {}));
23
+ exports.Environment = Environment;
@@ -0,0 +1,2 @@
1
+ export { ServiceIdentity } from './serviceIdentity';
2
+ export { Environment } from './environment';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Environment = exports.ServiceIdentity = void 0;
4
+ var serviceIdentity_1 = require("./serviceIdentity");
5
+ Object.defineProperty(exports, "ServiceIdentity", { enumerable: true, get: function () { return serviceIdentity_1.ServiceIdentity; } });
6
+ var environment_1 = require("./environment");
7
+ Object.defineProperty(exports, "Environment", { enumerable: true, get: function () { return environment_1.Environment; } });
@@ -0,0 +1,23 @@
1
+ import * as jose from 'jose';
2
+ import { HttpMethod } from 'types/http';
3
+ import { Json } from 'types/json';
4
+ import { RequestHandler } from 'express';
5
+ declare class ServiceIdentity {
6
+ readonly identityName: string;
7
+ readonly identityKid: string;
8
+ private readonly identityPrivateKey;
9
+ private readonly identityPublicKey;
10
+ private readonly jwksCache;
11
+ private readonly keyCache;
12
+ private readonly axiosClient;
13
+ constructor(identityName: string, identityKid: string, identityPrivateKey: string, identityPublicKey: string);
14
+ getRemoteJWKS: (service: string) => import("jose/dist/types/types").GetKeyFunction<jose.JWSHeaderParameters, jose.FlattenedJWSInput>;
15
+ private getJwtVerifyOptions;
16
+ getIdentityPublicKey: () => Promise<jose.KeyLike>;
17
+ getIdentityPrivateKey: () => Promise<jose.KeyLike>;
18
+ callService: <T>(service: string, method: HttpMethod, url: string, data?: Json) => Promise<T>;
19
+ private getIdentityJWKS;
20
+ requireServiceIdentity: (service: string) => RequestHandler;
21
+ identityJWKSRoute: RequestHandler;
22
+ }
23
+ export { ServiceIdentity };
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.ServiceIdentity = void 0;
30
+ const jose = __importStar(require("jose"));
31
+ const url_1 = require("url");
32
+ const boom_1 = require("@hapi/boom");
33
+ const moment_1 = __importDefault(require("moment"));
34
+ const axios_1 = __importDefault(require("axios"));
35
+ const JWT_ALGORITHM = 'ES256';
36
+ const JWT_HEADER_NAME = 'X-Identity';
37
+ const JWT_ALLOWED_ALGORITHMS = [JWT_ALGORITHM];
38
+ const JWT_NBF = '0s';
39
+ const JWT_EXP = '1m';
40
+ const JWT_CLOCK_TOLERANCE = moment_1.default.duration(1, 'minute').asSeconds();
41
+ class ServiceIdentity {
42
+ constructor(identityName, identityKid, identityPrivateKey, identityPublicKey) {
43
+ this.jwksCache = {};
44
+ this.keyCache = {};
45
+ this.getRemoteJWKS = (service) => {
46
+ if (this.jwksCache[String(service)])
47
+ return this.jwksCache[String(service)];
48
+ const jwks = jose.createRemoteJWKSet(new url_1.URL(`http://${service}:8080/identity/jwks`));
49
+ this.jwksCache[String(service)] = jwks;
50
+ return jwks;
51
+ };
52
+ this.getJwtVerifyOptions = (service) => ({
53
+ issuer: service,
54
+ clockTolerance: JWT_CLOCK_TOLERANCE,
55
+ algorithms: JWT_ALLOWED_ALGORITHMS,
56
+ audience: this.identityName,
57
+ });
58
+ this.getIdentityPublicKey = async () => {
59
+ if (!this.keyCache.public) {
60
+ this.keyCache.public = await jose.importSPKI(this.identityPublicKey, JWT_ALGORITHM);
61
+ }
62
+ return this.keyCache.public;
63
+ };
64
+ this.getIdentityPrivateKey = async () => {
65
+ if (!this.keyCache.private) {
66
+ this.keyCache.private = await jose.importPKCS8(this.identityPrivateKey, JWT_ALGORITHM);
67
+ }
68
+ return this.keyCache.private;
69
+ };
70
+ this.callService = async (service, method, url, data) => {
71
+ const jwt = await new jose.SignJWT({
72
+ method,
73
+ url,
74
+ })
75
+ .setProtectedHeader({
76
+ alg: JWT_ALGORITHM,
77
+ typ: 'JWT',
78
+ kid: this.identityKid,
79
+ })
80
+ .setIssuer(this.identityName)
81
+ .setAudience(service)
82
+ .setIssuedAt()
83
+ .setNotBefore(JWT_NBF)
84
+ .setExpirationTime(JWT_EXP)
85
+ .sign(await this.getIdentityPrivateKey());
86
+ const request = {
87
+ headers: {
88
+ [JWT_HEADER_NAME]: jwt,
89
+ },
90
+ baseURL: `http://${service}:8080/`,
91
+ url,
92
+ method,
93
+ data,
94
+ };
95
+ const response = await this.axiosClient.request(request);
96
+ return response.data;
97
+ };
98
+ this.getIdentityJWKS = async () => {
99
+ const publicKey = await this.getIdentityPublicKey();
100
+ const jwk = await jose.exportJWK(publicKey);
101
+ jwk.kid = this.identityKid;
102
+ return { keys: [jwk] };
103
+ };
104
+ this.requireServiceIdentity = (service) => async (request, response, next) => {
105
+ const jwt = request.header(JWT_HEADER_NAME);
106
+ if (typeof jwt !== 'string') {
107
+ throw (0, boom_1.unauthorized)();
108
+ }
109
+ const { payload } = await jose.jwtVerify(jwt, this.getRemoteJWKS(service), this.getJwtVerifyOptions(service));
110
+ if (request.originalUrl !== payload.url)
111
+ throw (0, boom_1.forbidden)(`${request.originalUrl} !== ${payload.url}`);
112
+ if (request.method !== payload.method)
113
+ throw (0, boom_1.forbidden)(`${request.method} !== ${payload.method}`);
114
+ next();
115
+ };
116
+ this.identityJWKSRoute = async (request, response) => {
117
+ response.send(await this.getIdentityJWKS());
118
+ };
119
+ this.identityName = identityName;
120
+ this.identityKid = identityKid;
121
+ this.identityPrivateKey = identityPrivateKey;
122
+ this.identityPublicKey = identityPublicKey;
123
+ this.axiosClient = axios_1.default.create({ proxy: false });
124
+ }
125
+ }
126
+ exports.ServiceIdentity = ServiceIdentity;
@@ -0,0 +1 @@
1
+ export declare type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'PATCH' | 'OPTIONS' | 'TRACE' | 'CONNECT';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export declare type Literal = boolean | null | number | string;
2
+ export declare type Json = Literal | {
3
+ [key: string]: Json;
4
+ } | Json[];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import { Logger } from 'pino';
2
+ declare const logAndGetError: (logger: Logger, message: string, error?: unknown) => Error;
3
+ export { logAndGetError };
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logAndGetError = void 0;
4
+ const logAndGetError = (logger, message, error) => {
5
+ logger.error(error, message);
6
+ return Error(message);
7
+ };
8
+ exports.logAndGetError = logAndGetError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaapp/service-utils",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
@@ -18,7 +18,9 @@
18
18
  "audit": "improved-yarn-audit --ignore-dev-deps"
19
19
  },
20
20
  "devDependencies": {
21
+ "@types/express": "4.17.13",
21
22
  "@types/jest": "^28.1.5",
23
+ "@types/pino": "^7.0.5",
22
24
  "@typescript-eslint/eslint-plugin": "^5.30.6",
23
25
  "@typescript-eslint/parser": "^5.30.6",
24
26
  "conventional-changelog-conventionalcommits": "^5.0.0",
@@ -33,6 +35,10 @@
33
35
  },
34
36
  "dependencies": {
35
37
  "@types/express": "4.17.13",
36
- "kafkajs": "2.1.0"
38
+ "@hapi/boom": "^10.0.0",
39
+ "axios": "^0.27.2",
40
+ "jose": "4.9.2",
41
+ "kafkajs": "2.1.0",
42
+ "moment": "^2.29.4"
37
43
  }
38
44
  }
@@ -1,4 +0,0 @@
1
- import { Message } from 'kafkajs';
2
- declare const initializeKafkaClient: () => Promise<void>;
3
- declare const sendKafkaMessage: (message: Message) => Promise<void>;
4
- export { initializeKafkaClient, sendKafkaMessage };
@@ -1,36 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sendKafkaMessage = exports.initializeKafkaClient = void 0;
4
- const kafkajs_1 = require("kafkajs");
5
- const groupId = 'test-group';
6
- let producer;
7
- let consumer;
8
- const initializeKafkaClient = async () => {
9
- const kafka = new kafkajs_1.Kafka({
10
- clientId: 'backend',
11
- brokers: ['kafka:9092'],
12
- });
13
- producer = kafka.producer();
14
- consumer = kafka.consumer({ groupId });
15
- await producer.connect();
16
- await consumer.connect();
17
- await consumer.subscribe({ topic: groupId, fromBeginning: true });
18
- await consumer.run({
19
- eachMessage: async ({ topic, partition, message }) => {
20
- console.log({
21
- topic,
22
- partition,
23
- offset: message.offset,
24
- value: message.value?.toString(),
25
- }, 'Message consumed');
26
- },
27
- });
28
- };
29
- exports.initializeKafkaClient = initializeKafkaClient;
30
- const sendKafkaMessage = async (message) => {
31
- await producer.send({
32
- topic: groupId,
33
- messages: [message],
34
- });
35
- };
36
- exports.sendKafkaMessage = sendKafkaMessage;