@crowdin/app-project-module 1.13.2 → 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.
- package/out/index.js +4 -0
- package/out/modules/chat/index.d.ts +6 -0
- package/out/modules/chat/index.js +21 -0
- package/out/modules/integration/util/defaults.js +6 -0
- package/out/modules/manifest.js +7 -0
- package/out/static/ui/form.bundle.js +825 -312
- package/out/static/ui/form.bundle.js.map +1 -1
- package/out/types.d.ts +6 -0
- package/out/util/log-sanitizer.d.ts +18 -0
- package/out/util/log-sanitizer.js +169 -0
- package/package.json +3 -3
package/out/types.d.ts
CHANGED
|
@@ -179,6 +179,10 @@ export interface ClientConfig extends ImagePath {
|
|
|
179
179
|
* modal module
|
|
180
180
|
*/
|
|
181
181
|
modal?: OneOrMany<ModalModule>;
|
|
182
|
+
/**
|
|
183
|
+
* chat module
|
|
184
|
+
*/
|
|
185
|
+
chat?: ChatModule[];
|
|
182
186
|
/**
|
|
183
187
|
* Install hook
|
|
184
188
|
*/
|
|
@@ -580,6 +584,8 @@ export declare enum storageFiles {
|
|
|
580
584
|
}
|
|
581
585
|
export interface ModalModule extends ModuleContent, UiModule, Environments {
|
|
582
586
|
}
|
|
587
|
+
export interface ChatModule extends ModuleContent, UiModule, Environments {
|
|
588
|
+
}
|
|
583
589
|
export interface ContextModule extends ContextContent, UiModule, Environments {
|
|
584
590
|
}
|
|
585
591
|
export interface FileStore {
|
|
@@ -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.
|
|
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.
|
|
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",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"@types/better-sqlite3": "^7.6.13",
|
|
112
112
|
"@types/cors": "^2.8.19",
|
|
113
113
|
"@types/express": "^5.0.3",
|
|
114
|
-
"@types/jest": "^
|
|
114
|
+
"@types/jest": "^30.0.0",
|
|
115
115
|
"@types/jsonwebtoken": "^9.0.10",
|
|
116
116
|
"@types/minimatch": "^5.1.2",
|
|
117
117
|
"@types/node": "^20",
|