@crowdin/app-project-module 1.14.0 → 1.15.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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Masks JWTs, bearer tokens, passwords, API keys, and other secrets in log records
3
+ * before they leave the process (stdout / Sentry). Registered with @crowdin/logs-formatter
4
+ * at SDK bootstrap so every Crowdin app built on this SDK benefits without per-app changes.
5
+ *
6
+ * NOTE: a separate field-value masker lives at src/util/credentials-masker.ts (maskKey) —
7
+ * it masks ALL but last 3 chars with '*', operates on form-data fields, and is wired into
8
+ * request/response middleware. The two are intentionally distinct: this sanitizer uses
9
+ * "first 4 chars + ***" on log records, and the field masker uses "***...XYZ" on form
10
+ * payloads. Do not mix them.
11
+ */
12
+ import type { LogRecord, Sanitizer } from '@crowdin/logs-formatter';
13
+ export type { LogRecord, Sanitizer };
14
+ export declare const MASK_PLACEHOLDER = "***";
15
+ export declare function maskString(input: string): string;
16
+ export declare function maskUrl(url: string): string;
17
+ export declare function sanitizeValue(value: unknown): unknown;
18
+ export declare function crowdinLogSanitizer(record: LogRecord): LogRecord;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * Masks JWTs, bearer tokens, passwords, API keys, and other secrets in log records
4
+ * before they leave the process (stdout / Sentry). Registered with @crowdin/logs-formatter
5
+ * at SDK bootstrap so every Crowdin app built on this SDK benefits without per-app changes.
6
+ *
7
+ * NOTE: a separate field-value masker lives at src/util/credentials-masker.ts (maskKey) —
8
+ * it masks ALL but last 3 chars with '*', operates on form-data fields, and is wired into
9
+ * request/response middleware. The two are intentionally distinct: this sanitizer uses
10
+ * "first 4 chars + ***" on log records, and the field masker uses "***...XYZ" on form
11
+ * payloads. Do not mix them.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.MASK_PLACEHOLDER = void 0;
15
+ exports.maskString = maskString;
16
+ exports.maskUrl = maskUrl;
17
+ exports.sanitizeValue = sanitizeValue;
18
+ exports.crowdinLogSanitizer = crowdinLogSanitizer;
19
+ const SENSITIVE_KEY_REGEX = /^(jwt|jwt[_-]?token|token|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|client[_-]?secret|password|passwd|pwd|authorization|cookie|set-cookie|x-api-key)$/i;
20
+ const JWT_REGEX = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
21
+ const BEARER_REGEX = /Bearer\s+[A-Za-z0-9._\-+/=]+/gi;
22
+ const URL_USERINFO_REGEX = /(\b[a-z][a-z0-9+.-]*:\/\/)([^@\s/]+):([^@\s]+)@/gi;
23
+ // Matches util.inspect-style "<sensitiveKey>: '<value>'" with single/double/backtick quotes,
24
+ // honoring backslash escapes inside the value (e.g. \' or \" or \\).
25
+ const INSPECTED_FIELD_REGEX = /\b(jwt(?:[_-]?token)?|token|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|client[_-]?secret|password|passwd|pwd|authorization|cookie|set-cookie|x-api-key)\s*:\s*(['"`])((?:\\\2|(?!\2).)*)\2/gi;
26
+ // Sanitizer runs on every log call; skip the regex passes when no candidate token is present.
27
+ const SANITIZE_HINT_REGEX = /eyJ|bearer|:\/\/|password|passwd|pwd|cookie|token|api[_-]?key|secret|authorization|jwt/i;
28
+ const MAX_DEPTH = 10;
29
+ exports.MASK_PLACEHOLDER = '***';
30
+ function maskPrefix4(value) {
31
+ return value.length <= 4 ? exports.MASK_PLACEHOLDER : value.slice(0, 4) + exports.MASK_PLACEHOLDER;
32
+ }
33
+ function maskInspectedFields(input) {
34
+ return input.replace(INSPECTED_FIELD_REGEX, (_, key, quote, value) => {
35
+ const unescaped = value.replace(/\\(.)/g, '$1');
36
+ return `${key}: ${quote}${maskPrefix4(unescaped)}${quote}`;
37
+ });
38
+ }
39
+ function maskString(input) {
40
+ if (typeof input !== 'string' || input.length === 0) {
41
+ return input;
42
+ }
43
+ if (!SANITIZE_HINT_REGEX.test(input)) {
44
+ return input;
45
+ }
46
+ return maskInspectedFields(input)
47
+ .replace(JWT_REGEX, maskPrefix4)
48
+ .replace(BEARER_REGEX, `Bearer ${exports.MASK_PLACEHOLDER}`)
49
+ .replace(URL_USERINFO_REGEX, (_, scheme, user) => `${scheme}${user}:${exports.MASK_PLACEHOLDER}@`);
50
+ }
51
+ function maskValueAsKey(value) {
52
+ return typeof value === 'string' ? maskPrefix4(value) : exports.MASK_PLACEHOLDER;
53
+ }
54
+ function maskUrl(url) {
55
+ if (typeof url !== 'string') {
56
+ return url;
57
+ }
58
+ const queryIndex = url.indexOf('?');
59
+ if (queryIndex === -1) {
60
+ return maskString(url);
61
+ }
62
+ const head = url.slice(0, queryIndex);
63
+ const tail = url.slice(queryIndex + 1);
64
+ const [queryPart, ...fragmentParts] = tail.split('#');
65
+ const fragment = fragmentParts.length ? '#' + fragmentParts.join('#') : '';
66
+ const maskedQuery = queryPart
67
+ .split('&')
68
+ .map((pair) => {
69
+ const eq = pair.indexOf('=');
70
+ if (eq === -1) {
71
+ return pair;
72
+ }
73
+ const key = pair.slice(0, eq);
74
+ const value = pair.slice(eq + 1);
75
+ const decodedKey = safeDecode(key);
76
+ if (SENSITIVE_KEY_REGEX.test(decodedKey)) {
77
+ return `${key}=${maskValueAsKey(safeDecode(value))}`;
78
+ }
79
+ return `${key}=${maskString(value)}`;
80
+ })
81
+ .join('&');
82
+ return `${maskString(head)}?${maskedQuery}${fragment}`;
83
+ }
84
+ function safeDecode(value) {
85
+ try {
86
+ return decodeURIComponent(value);
87
+ }
88
+ catch (_a) {
89
+ return value;
90
+ }
91
+ }
92
+ function sanitizeValue(value) {
93
+ return sanitizeValueImpl(value, 0, new WeakSet());
94
+ }
95
+ const HANDLED_ERROR_KEYS = new Set(['name', 'message', 'stack']);
96
+ function sanitizeValueImpl(value, depth, seen) {
97
+ if (value === null || value === undefined) {
98
+ return value;
99
+ }
100
+ if (depth > MAX_DEPTH) {
101
+ return '[Truncated: max depth]';
102
+ }
103
+ if (typeof value === 'string') {
104
+ return maskString(value);
105
+ }
106
+ if (typeof value !== 'object') {
107
+ return value;
108
+ }
109
+ if (seen.has(value)) {
110
+ return '[Circular]';
111
+ }
112
+ seen.add(value);
113
+ if (Array.isArray(value)) {
114
+ return value.map((item) => sanitizeValueImpl(item, depth + 1, seen));
115
+ }
116
+ if (value instanceof Error) {
117
+ const out = {
118
+ name: value.name,
119
+ message: maskString(value.message),
120
+ };
121
+ if (value.stack) {
122
+ out.stack = maskString(value.stack);
123
+ }
124
+ for (const key of Object.keys(value)) {
125
+ if (HANDLED_ERROR_KEYS.has(key)) {
126
+ continue;
127
+ }
128
+ out[key] = sanitizeField(key, value[key], depth, seen);
129
+ }
130
+ return out;
131
+ }
132
+ const out = {};
133
+ for (const [key, v] of Object.entries(value)) {
134
+ out[key] = sanitizeField(key, v, depth, seen);
135
+ }
136
+ return out;
137
+ }
138
+ function sanitizeField(key, value, depth, seen) {
139
+ if (SENSITIVE_KEY_REGEX.test(key)) {
140
+ return maskValueAsKey(value);
141
+ }
142
+ if ((key === 'url' || key === 'originalUrl' || key === 'href') && typeof value === 'string') {
143
+ return maskUrl(value);
144
+ }
145
+ return sanitizeValueImpl(value, depth + 1, seen);
146
+ }
147
+ // Returns a new LogRecord — never mutates the input. Falls back to the original record
148
+ // on any throw so a sanitizer bug can't bring down the logging pipeline. (logs-formatter
149
+ // also catches throws at the pipeline level; this inner guard is defense in depth.)
150
+ // Typed with concrete LogRecord return (not Sanitizer's broader `LogRecord | void`) so
151
+ // callers and tests get a non-nullable result; still assignable to Sanitizer.
152
+ function crowdinLogSanitizer(record) {
153
+ try {
154
+ return {
155
+ level: record.level,
156
+ message: typeof record.message === 'string' ? maskString(record.message) : record.message,
157
+ rawParams: Array.isArray(record.rawParams)
158
+ ? record.rawParams.map((p) => sanitizeValue(p))
159
+ : record.rawParams,
160
+ context: sanitizeValue(record.context),
161
+ record: record.record && typeof record.record === 'object'
162
+ ? sanitizeValue(record.record)
163
+ : record.record,
164
+ };
165
+ }
166
+ catch (_a) {
167
+ return record;
168
+ }
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Module that generates for you all common endpoints for serving standalone Crowdin App",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
@@ -62,7 +62,7 @@
62
62
  ],
63
63
  "dependencies": {
64
64
  "@crowdin/crowdin-api-client": "^1.55.0",
65
- "@crowdin/logs-formatter": "^2.2.0",
65
+ "@crowdin/logs-formatter": "^2.3.0",
66
66
  "@godaddy/terminus": "^4.12.1",
67
67
  "ajv": "^8.17.1",
68
68
  "amqplib": "^0.10.9",