@cryptexlabs/codex-nodejs-common 0.16.3 → 0.16.6

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.
Files changed (45) hide show
  1. package/WARP.md +28 -0
  2. package/coverage/clover.xml +102 -0
  3. package/coverage/coverage-final.json +3 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/client/client.ts.html +121 -0
  7. package/coverage/lcov-report/client/index.html +116 -0
  8. package/coverage/lcov-report/context/context.builder.ts.html +685 -0
  9. package/coverage/lcov-report/context/index.html +116 -0
  10. package/coverage/lcov-report/favicon.png +0 -0
  11. package/coverage/lcov-report/index.html +131 -0
  12. package/coverage/lcov-report/prettify.css +1 -0
  13. package/coverage/lcov-report/prettify.js +2 -0
  14. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  15. package/coverage/lcov-report/sorter.js +196 -0
  16. package/coverage/lcov.info +160 -0
  17. package/lib/package.json +2 -2
  18. package/lib/src/auth/authf.guard.d.ts +1 -0
  19. package/lib/src/auth/authf.guard.js +22 -11
  20. package/lib/src/auth/authf.guard.js.map +1 -1
  21. package/lib/src/client/client.d.ts +1 -0
  22. package/lib/src/client/client.js.map +1 -1
  23. package/lib/src/config/default-config.d.ts +1 -0
  24. package/lib/src/config/default-config.js +5 -0
  25. package/lib/src/config/default-config.js.map +1 -1
  26. package/lib/src/context/context.builder.d.ts +1 -0
  27. package/lib/src/context/context.builder.js +12 -0
  28. package/lib/src/context/context.builder.js.map +1 -1
  29. package/lib/src/filter/app-http-exception-filter.d.ts +4 -1
  30. package/lib/src/filter/app-http-exception-filter.js +51 -29
  31. package/lib/src/filter/app-http-exception-filter.js.map +1 -1
  32. package/lib/src/middleware/api-headers-validation.middleware.js +2 -2
  33. package/lib/src/middleware/api-headers-validation.middleware.js.map +1 -1
  34. package/lib/src/middleware/query-authorization.middleware.js +1 -1
  35. package/lib/src/middleware/query-authorization.middleware.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/auth/authf.guard.ts +26 -11
  38. package/src/client/client.spec.ts +17 -0
  39. package/src/client/client.ts +2 -0
  40. package/src/config/default-config.ts +6 -0
  41. package/src/context/context.builder.spec.ts +231 -0
  42. package/src/context/context.builder.ts +13 -0
  43. package/src/filter/app-http-exception-filter.ts +67 -36
  44. package/src/middleware/api-headers-validation.middleware.ts +1 -1
  45. package/src/middleware/query-authorization.middleware.ts +1 -1
@@ -0,0 +1,231 @@
1
+ import {
2
+ ApiMetaHeadersInterface,
3
+ ClientInterface,
4
+ Locale,
5
+ LocaleI18nInterface,
6
+ MessageContextInterface,
7
+ MessageMetaInterface,
8
+ } from "@cryptexlabs/codex-data-model";
9
+ import { LoggerService } from "@nestjs/common";
10
+ import { Context } from "./context";
11
+ import { ContextBuilder } from "./context.builder";
12
+ import { DefaultConfig } from "../config";
13
+ import { instance, mock, when } from "ts-mockito";
14
+
15
+ interface I18nApiStub {
16
+ getCatalog: () => Record<string, unknown>;
17
+ }
18
+
19
+ describe("ContextBuilder", () => {
20
+ let loggerMock: LoggerService;
21
+ let configMock: DefaultConfig;
22
+ let client: ClientInterface;
23
+ let messageContext: MessageContextInterface;
24
+ let i18nData: i18nAPI;
25
+
26
+ beforeEach(() => {
27
+ const logger = mock<LoggerService>();
28
+ loggerMock = instance(logger);
29
+
30
+ const config = mock(DefaultConfig);
31
+ when(config.appName).thenReturn("app");
32
+ when(config.appVersion).thenReturn("1.0.0");
33
+ when(config.environmentName).thenReturn("env");
34
+ when(config.clientId).thenReturn("client-id");
35
+ configMock = instance(config);
36
+
37
+ client = {
38
+ id: "id",
39
+ version: "0.0.1",
40
+ name: "client",
41
+ variant: "variant",
42
+ };
43
+ messageContext = { category: "default", id: "none" };
44
+ const i18nStub: I18nApiStub = {
45
+ getCatalog: () => ({ "en-US": {} }),
46
+ };
47
+ i18nData = (i18nStub as unknown) as i18nAPI;
48
+ });
49
+
50
+ const buildBuilder = () =>
51
+ new ContextBuilder(
52
+ loggerMock,
53
+ configMock,
54
+ client,
55
+ messageContext,
56
+ i18nData
57
+ );
58
+
59
+ it("returns now override when provided", () => {
60
+ const builder = buildBuilder();
61
+
62
+ const before = Date.now();
63
+ const initialNow = builder.now().getTime();
64
+ expect(initialNow).toBeGreaterThanOrEqual(before);
65
+
66
+ const override = new Date("2024-02-02T12:00:00Z");
67
+ builder.setNow(override);
68
+
69
+ expect(builder.now()).toBe(override);
70
+ });
71
+
72
+ it("clones state with build including agent", () => {
73
+ const builder = buildBuilder();
74
+ builder.client.agent = "ua/1.0";
75
+
76
+ const clone = builder.build();
77
+
78
+ expect(clone).not.toBe(builder);
79
+ expect(clone.client.agent).toBe("ua/1.0");
80
+ expect(clone.now()).toBeInstanceOf(Date);
81
+ });
82
+
83
+ it("sets client fields including agent from headers", () => {
84
+ const builder = buildBuilder();
85
+ const headers: ApiMetaHeadersInterface & {
86
+ "x-forwarded-uri": string;
87
+ "user-agent": string;
88
+ "x-timezone": string;
89
+ } = {
90
+ "x-correlation-id": "corr-1",
91
+ "accept-language": "en-US",
92
+ "x-started": "2025-01-01T10:00:00Z",
93
+ "x-created": "2025-01-01T10:00:00Z",
94
+ "x-context-category": "cat",
95
+ "x-context-id": "ctx-id",
96
+ "x-client-id": "client-1",
97
+ "x-client-name": "client-name",
98
+ "x-client-version": "1.2.3",
99
+ "x-client-variant": "variant-x",
100
+ "x-forwarded-uri": "/some/path",
101
+ "x-now": "2025-01-01T11:00:00Z",
102
+ "x-timezone": "UTC",
103
+ "user-agent": "UnitTestAgent/1.0",
104
+ };
105
+
106
+ const resultBuilder = builder.setMetaFromHeaders(headers);
107
+ const context = resultBuilder.getResult();
108
+
109
+ expect(context.client.agent).toBe("UnitTestAgent/1.0");
110
+ expect(context.client.id).toBe("client-1");
111
+ expect(context.client.name).toBe("client-name");
112
+ expect(context.client.version).toBe("1.2.3");
113
+ expect(context.client.variant).toBe("variant-x");
114
+ expect(context.path).toBe("/some/path");
115
+ expect(context.now().toISOString()).toBe("2025-01-01T11:00:00.000Z");
116
+ expect(context.timezone).toBe("UTC");
117
+ expect(resultBuilder).toBe(builder);
118
+ });
119
+
120
+ it("allows overriding i18n data", () => {
121
+ const builder = buildBuilder();
122
+ const customI18n: I18nApiStub = {
123
+ getCatalog: () => ({ "en-US": { greeting: "hi" } }),
124
+ };
125
+
126
+ const returned = builder.setI18nData((customI18n as unknown) as i18nAPI);
127
+
128
+ expect(returned).toBe(builder);
129
+ expect(() => builder.getResult()).not.toThrow();
130
+ });
131
+
132
+ it("leaves client fields unchanged when optional headers missing", () => {
133
+ const builder = buildBuilder();
134
+ const minimalHeaders: ApiMetaHeadersInterface = {
135
+ "x-correlation-id": "corr-2",
136
+ "accept-language": "en-US",
137
+ "x-started": "2024-03-01T10:00:00Z",
138
+ "x-created": "2024-03-01T10:00:00Z",
139
+ "x-context-category": "default",
140
+ "x-context-id": "none",
141
+ "x-client-id": "id",
142
+ "x-client-name": "client",
143
+ "x-client-version": "0.0.1",
144
+ "x-client-variant": "variant",
145
+ };
146
+
147
+ builder.setMetaFromHeaders(minimalHeaders);
148
+ const context = builder.getResult();
149
+
150
+ expect(context.client.agent).toBeUndefined();
151
+ expect(context.path).toBeUndefined();
152
+ expect(context.timezone).toBeUndefined();
153
+ });
154
+
155
+ it("sets values from headers for new messages", () => {
156
+ const builder = buildBuilder();
157
+ const headers: ApiMetaHeadersInterface & { "x-forwarded-uri": string } = {
158
+ "x-correlation-id": "corr-3",
159
+ "accept-language": "en-US",
160
+ "x-started": "2024-04-01T10:00:00Z",
161
+ "x-created": "2024-04-01T10:00:00Z",
162
+ "x-context-category": "custom",
163
+ "x-context-id": "ctx-3",
164
+ "x-client-id": "id",
165
+ "x-client-name": "client",
166
+ "x-client-version": "0.0.1",
167
+ "x-client-variant": "variant",
168
+ "x-forwarded-uri": "/another/path",
169
+ };
170
+
171
+ builder.setMetaFromHeadersForNewMessage(headers);
172
+ const context = builder.getResult();
173
+
174
+ expect(context.correlationId).toBe("corr-3");
175
+ expect(context.messageContext.category).toBe("custom");
176
+ expect(context.messageContext.id).toBe("ctx-3");
177
+ expect(context.path).toBe("/another/path");
178
+ });
179
+
180
+ it("copies values from existing context", () => {
181
+ const baseBuilder = buildBuilder();
182
+ baseBuilder
183
+ .setCorrelationId("corr-4")
184
+ .setStarted("2024-05-01T10:00:00Z")
185
+ .setLocale(new Locale("en", "US"));
186
+ baseBuilder.client.agent = "CopiedAgent";
187
+ const sourceContext = baseBuilder.getResult();
188
+
189
+ const targetBuilder = buildBuilder().setMetaFromContext(sourceContext);
190
+ const result = targetBuilder.getResult();
191
+
192
+ expect(result.correlationId).toBe("corr-4");
193
+ expect(result.messageContext.id).toBe(sourceContext.messageContext.id);
194
+ expect(result.client.agent).toBe("CopiedAgent");
195
+ });
196
+
197
+ it("sets values from message meta", () => {
198
+ const builder = buildBuilder();
199
+ const meta: MessageMetaInterface = {
200
+ correlationId: "corr-5",
201
+ time: {
202
+ started: "2024-06-01T10:00:00Z",
203
+ created: "2024-06-01T10:00:00Z",
204
+ },
205
+ context: { category: "meta", id: "meta-id" },
206
+ client,
207
+ type: "type",
208
+ schemaVersion: "0.1.0",
209
+ locale: new Locale("fr", "CA") as LocaleI18nInterface,
210
+ };
211
+
212
+ builder.setMeta(meta);
213
+ const context = builder.getResult();
214
+
215
+ expect(context.correlationId).toBe("corr-5");
216
+ expect(context.locale.language).toBe("fr");
217
+ expect(context.started.toISOString()).toBe("2024-06-01T10:00:00.000Z");
218
+ });
219
+
220
+ it("applies defaults when values are missing", () => {
221
+ const builder = buildBuilder();
222
+ builder.setNow(new Date("2024-07-01T11:00:00Z"));
223
+
224
+ const context = builder.getResult();
225
+
226
+ expect(context.correlationId).toHaveLength(36);
227
+ expect(context.locale.i18n).toBe("en-US");
228
+ expect(context.started).toBeInstanceOf(Date);
229
+ expect(context.now().toISOString()).toBe("2024-07-01T11:00:00.000Z");
230
+ });
231
+ });
@@ -51,6 +51,7 @@ export class ContextBuilder {
51
51
  version: this.client.version,
52
52
  id: this.client.id,
53
53
  variant: this.client.variant,
54
+ agent: this.client.agent,
54
55
  },
55
56
  {
56
57
  category: this.messageContext.category,
@@ -81,6 +82,9 @@ export class ContextBuilder {
81
82
  if (headers["x-client-variant"]) {
82
83
  this.client.variant = headers["x-client-variant"];
83
84
  }
85
+ if (headers["user-agent"]) {
86
+ this.client.agent = headers["user-agent"];
87
+ }
84
88
  if (headers["x-forwarded-uri"]) {
85
89
  this._path = headers["x-forwarded-uri"];
86
90
  }
@@ -112,6 +116,15 @@ export class ContextBuilder {
112
116
  return this;
113
117
  }
114
118
 
119
+ public setMetaFromContext(context: Context) {
120
+ this.setCorrelationId(context.correlationId);
121
+ this.setLocale(context.locale);
122
+ this.setStarted(context.started);
123
+ this.messageContext.category = context.messageContext.category;
124
+ this.messageContext.id = context.messageContext.id;
125
+ return this;
126
+ }
127
+
115
128
  public setMeta(meta: MessageMetaInterface) {
116
129
  this.setCorrelationId(meta.correlationId);
117
130
  this.setLocale(new Locale(meta.locale.language, meta.locale.country));
@@ -23,6 +23,9 @@ import { LocaleUtil } from "../util";
23
23
  @Catch()
24
24
  @Injectable()
25
25
  export class AppHttpExceptionFilter extends BaseExceptionFilter {
26
+ private _lastErrorLogTime: number = 0;
27
+ private _suppressedErrorCount: number = 0;
28
+
26
29
  constructor(
27
30
  private readonly fallbackLocale: Locale,
28
31
  @Inject("LOGGER") private readonly fallbackLogger: LoggerService,
@@ -32,12 +35,29 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
32
35
  super(applicationRef);
33
36
  }
34
37
 
35
- private isDebugLevel(): boolean {
38
+ private _isDebugLevel(): boolean {
36
39
  return this.config.logLevels.some(
37
40
  (level) => level.toLowerCase() === "debug"
38
41
  );
39
42
  }
40
43
 
44
+ private _shouldLogError(): boolean {
45
+ if (this._isDebugLevel()) {
46
+ return true;
47
+ }
48
+
49
+ const now = Date.now();
50
+ const timeSinceLastLog = now - this._lastErrorLogTime;
51
+
52
+ if (timeSinceLastLog >= this.config.errorLogIntervalMs) {
53
+ this._lastErrorLogTime = now;
54
+ return true;
55
+ }
56
+
57
+ this._suppressedErrorCount++;
58
+ return false;
59
+ }
60
+
41
61
  catch(exception: any, host: ArgumentsHost) {
42
62
  const ctx = host.switchToHttp();
43
63
  const request = ctx.getRequest<Request>();
@@ -97,43 +117,54 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
97
117
  logger.error(`No stack available for error of type ${typeof exception}`);
98
118
  }
99
119
 
100
- if (developerText) {
101
- if (this.config.loggerType === "pino") {
102
- logger.error(developerText, { body: request.body, stack });
120
+ const shouldLogError = this._shouldLogError();
121
+
122
+ if (shouldLogError) {
123
+ if (developerText) {
124
+ if (this.config.loggerType === "pino") {
125
+ logger.error(developerText, { body: request.body, stack });
126
+ } else {
127
+ logger.error(developerText, {
128
+ request: {
129
+ headers: request.headers,
130
+ body: request.body,
131
+ url: request.url,
132
+ method: request.method,
133
+ stack,
134
+ },
135
+ });
136
+ }
103
137
  } else {
104
- logger.error(developerText, {
105
- request: {
106
- headers: request.headers,
107
- body: request.body,
108
- url: request.url,
109
- method: request.method,
138
+ logger.error(
139
+ [
140
+ "Error not explained",
141
+ "This probably means someone wrote code that throws an error without an error message",
142
+ "This should never happen!",
143
+ ].join(". "),
144
+ {
110
145
  stack,
111
- },
112
- });
146
+ }
147
+ );
113
148
  }
114
- } else {
115
- logger.error(
116
- [
117
- "Error not explained",
118
- "This probably means someone wrote code that throws an error without an error message",
119
- "This should never happen!",
120
- ].join(". "),
121
- {
122
- stack,
123
- }
124
- );
125
- }
126
149
 
127
- // If this is a normal error it means something random occurred and we want the stacktrace
128
- if (
129
- !(
130
- exception instanceof HttpException ||
131
- exception instanceof FriendlyHttpException ||
132
- exception instanceof ContextualHttpException
133
- ) &&
134
- exception.stack
135
- ) {
136
- logger.error(exception.stack);
150
+ // If this is a normal error it means something random occurred and we want the stacktrace
151
+ if (
152
+ !(
153
+ exception instanceof HttpException ||
154
+ exception instanceof FriendlyHttpException ||
155
+ exception instanceof ContextualHttpException
156
+ ) &&
157
+ exception.stack
158
+ ) {
159
+ logger.error(exception.stack);
160
+ }
161
+
162
+ if (this._suppressedErrorCount > 0) {
163
+ logger.error(
164
+ `Suppressed ${this._suppressedErrorCount} error(s) in the last minute due to rate limiting`
165
+ );
166
+ this._suppressedErrorCount = 0;
167
+ }
137
168
  }
138
169
 
139
170
  let status: number;
@@ -174,7 +205,7 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
174
205
  request.url || (request.headers["x-forwarded-uri"] as string);
175
206
 
176
207
  const canShowDeveloperText =
177
- this.isDebugLevel() || (status >= 400 && status < 500);
208
+ this._isDebugLevel() || (status >= 400 && status < 500);
178
209
 
179
210
  const errorHttpResponse = new ErrorHttpResponse(
180
211
  status,
@@ -189,7 +220,7 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
189
220
  : "Error unavailable in this context",
190
221
  userMessage
191
222
  ),
192
- this.isDebugLevel() ? exception.stack || null : null,
223
+ this._isDebugLevel() ? exception.stack || null : null,
193
224
  this.config,
194
225
  correlationId,
195
226
  started,
@@ -211,7 +211,7 @@ export class ApiHeadersValidationMiddleware implements NestMiddleware {
211
211
  })
212
212
  ) {
213
213
  // Ignore header validation for GET requests with non-JSON output
214
- const outputParam = req?.query?.output ?? req?.query?.output;
214
+ const outputParam = req?.query?.output;
215
215
  const isGetNonJson =
216
216
  (req?.method || "").toUpperCase() === "GET" &&
217
217
  outputParam !== undefined &&
@@ -11,7 +11,7 @@ export class QueryAuthorizationMiddleware implements NestMiddleware {
11
11
  use(req: any, res: any, next: () => void): any {
12
12
  const queryAuth = req?.query?.authorization;
13
13
  if (typeof queryAuth === "string" && queryAuth.length > 0) {
14
- req.headers.authorization = queryAuth;
14
+ req.headers.authorization = `Bearer ${queryAuth}`;
15
15
  }
16
16
 
17
17
  next();