@crowdin/app-project-module 2.9.0 → 2.10.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.d.ts +2 -0
- package/out/index.js +11 -4
- package/out/modules/integration/util/context.d.ts +11 -0
- package/out/modules/integration/util/context.js +17 -0
- package/out/modules/integration/util/cron.js +77 -59
- package/out/modules/integration/util/job.js +99 -80
- package/out/modules/integration/util/webhooks.d.ts +11 -0
- package/out/modules/integration/util/webhooks.js +35 -56
- package/out/static/css/styles.css +2 -2
- package/out/static/ui/about.bundle.js.map +1 -1
- package/out/static/ui/error.bundle.js.map +1 -1
- package/out/static/ui/form.bundle.js +87 -120
- package/out/static/ui/form.bundle.js.map +1 -1
- package/out/static/ui/install.bundle.js.map +1 -1
- package/out/static/ui/login.bundle.js.map +1 -1
- package/out/static/ui/main.bundle.js.map +1 -1
- package/out/static/ui/oauth.bundle.js.map +1 -1
- package/out/static/ui/subscription.bundle.js.map +1 -1
- package/out/util/connection.js +89 -56
- package/out/util/cron.d.ts +0 -2
- package/out/util/cron.js +18 -56
- package/out/util/index.d.ts +1 -0
- package/out/util/index.js +6 -0
- package/out/util/logger.d.ts +23 -1
- package/out/util/logger.js +36 -38
- package/package.json +11 -6
package/out/util/connection.js
CHANGED
|
@@ -302,13 +302,16 @@ function prepareIntegrationCredentials(config, integration, integrationCredentia
|
|
|
302
302
|
const performRefreshTokenRequest = (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.performRefreshTokenRequest) || (integrationLogin === null || integrationLogin === void 0 ? void 0 : integrationLogin.performRefreshTokenRequest);
|
|
303
303
|
credentials.tokenProvider = {
|
|
304
304
|
getToken: (...args_1) => __awaiter(this, [...args_1], void 0, function* (force = false) {
|
|
305
|
-
var _a, _b, _c, _d;
|
|
306
305
|
const { expireIn } = credentials;
|
|
307
306
|
//2 min as an extra buffer
|
|
308
307
|
const isExpired = !expireIn || expireIn - 120 < Date.now() / 1000;
|
|
309
308
|
if (!force && !isExpired) {
|
|
310
309
|
return credentials.accessToken;
|
|
311
310
|
}
|
|
311
|
+
const existingRefresh = _integrationRefreshInFlight.get(integrationCredentials.id);
|
|
312
|
+
if (existingRefresh) {
|
|
313
|
+
return existingRefresh;
|
|
314
|
+
}
|
|
312
315
|
(0, logger_1.log)(force
|
|
313
316
|
? 'Force refreshing integration credentials'
|
|
314
317
|
: 'Integration credentials have expired during operation. Requesting a new credentials');
|
|
@@ -334,50 +337,62 @@ function prepareIntegrationCredentials(config, integration, integrationCredentia
|
|
|
334
337
|
}
|
|
335
338
|
return null;
|
|
336
339
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
newCredentials
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
freshCredentials.
|
|
350
|
-
(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
340
|
+
const refreshPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
341
|
+
var _a, _b, _c, _d;
|
|
342
|
+
let newCredentials;
|
|
343
|
+
try {
|
|
344
|
+
newCredentials = yield doRefresh(credentials);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
// In-memory refresh token may be stale (rotated by another process/worker,
|
|
348
|
+
// or the token itself expired). Try fetching fresh credentials from DB.
|
|
349
|
+
(0, logger_1.log)('Token refresh failed. Attempting to fetch fresh credentials from the database');
|
|
350
|
+
const freshStoredCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(integrationCredentials.id);
|
|
351
|
+
if (freshStoredCredentials) {
|
|
352
|
+
const freshCredentials = JSON.parse((0, _1.decryptData)(config, freshStoredCredentials.credentials));
|
|
353
|
+
if (freshCredentials.refreshToken &&
|
|
354
|
+
freshCredentials.refreshToken !== credentials.refreshToken) {
|
|
355
|
+
(0, logger_1.log)('Found a newer refresh token in the database, retrying token refresh');
|
|
356
|
+
try {
|
|
357
|
+
newCredentials = yield doRefresh(freshCredentials);
|
|
358
|
+
credentials.refreshToken = freshCredentials.refreshToken;
|
|
359
|
+
}
|
|
360
|
+
catch (retryError) {
|
|
361
|
+
(0, logger_1.logError)(retryError);
|
|
362
|
+
throw retryError;
|
|
363
|
+
}
|
|
354
364
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
throw retryError;
|
|
365
|
+
else {
|
|
366
|
+
throw e;
|
|
358
367
|
}
|
|
359
368
|
}
|
|
360
369
|
else {
|
|
361
370
|
throw e;
|
|
362
371
|
}
|
|
363
372
|
}
|
|
364
|
-
|
|
365
|
-
|
|
373
|
+
if (!newCredentials) {
|
|
374
|
+
return credentials.accessToken;
|
|
366
375
|
}
|
|
367
|
-
|
|
368
|
-
|
|
376
|
+
credentials.accessToken = newCredentials[((_a = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _a === void 0 ? void 0 : _a.accessToken) || 'access_token'];
|
|
377
|
+
credentials.expireIn =
|
|
378
|
+
Number(newCredentials[((_b = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _b === void 0 ? void 0 : _b.expiresIn) || 'expires_in']) +
|
|
379
|
+
Date.now() / 1000;
|
|
380
|
+
if (newCredentials[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.refreshToken) || 'refresh_token']) {
|
|
381
|
+
credentials.refreshToken =
|
|
382
|
+
newCredentials[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.refreshToken) || 'refresh_token'];
|
|
383
|
+
}
|
|
384
|
+
(0, logger_1.log)('Saving updated integration credentials in the database');
|
|
385
|
+
yield (0, storage_1.getStorage)().updateIntegrationCredentials(integrationCredentials.id, (0, _1.encryptData)(config, JSON.stringify(credentials)));
|
|
369
386
|
return credentials.accessToken;
|
|
387
|
+
}))();
|
|
388
|
+
if (_integrationRefreshInFlight.size < 1000) {
|
|
389
|
+
_integrationRefreshInFlight.set(integrationCredentials.id, refreshPromise);
|
|
390
|
+
refreshPromise.finally(() => _integrationRefreshInFlight.delete(integrationCredentials.id));
|
|
370
391
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
Number(newCredentials[((_b = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _b === void 0 ? void 0 : _b.expiresIn) || 'expires_in']) + Date.now() / 1000;
|
|
374
|
-
if (newCredentials[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.refreshToken) || 'refresh_token']) {
|
|
375
|
-
credentials.refreshToken =
|
|
376
|
-
newCredentials[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.refreshToken) || 'refresh_token'];
|
|
392
|
+
else {
|
|
393
|
+
(0, logger_1.log)('Warning: _integrationRefreshInFlight map reached 1000 entries; skipping deduplication for this refresh');
|
|
377
394
|
}
|
|
378
|
-
|
|
379
|
-
yield (0, storage_1.getStorage)().updateIntegrationCredentials(integrationCredentials.id, (0, _1.encryptData)(config, JSON.stringify(credentials)));
|
|
380
|
-
return credentials.accessToken;
|
|
395
|
+
return refreshPromise;
|
|
381
396
|
}),
|
|
382
397
|
refreshToken: () => {
|
|
383
398
|
return credentials.tokenProvider.getToken(true);
|
|
@@ -389,30 +404,48 @@ function prepareIntegrationCredentials(config, integration, integrationCredentia
|
|
|
389
404
|
return credentials;
|
|
390
405
|
});
|
|
391
406
|
}
|
|
407
|
+
// This map is used to deduplicate concurrent refresh requests for the same Crowdin credentials.
|
|
408
|
+
// Keyed by credentials.id. Assumes a given credentials.id is always used with the same config
|
|
409
|
+
// (i.e. clientId/clientSecret are 1:1 with the credential). Concurrent callers with the same id
|
|
410
|
+
// share one in-flight refresh and all receive the same result.
|
|
411
|
+
const _crowdinRefreshInFlight = new Map();
|
|
412
|
+
const _integrationRefreshInFlight = new Map();
|
|
392
413
|
function refreshCrowdinCreds(_a) {
|
|
393
414
|
return __awaiter(this, arguments, void 0, function* ({ config, credentials }) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
415
|
+
const existing = _crowdinRefreshInFlight.get(credentials.id);
|
|
416
|
+
if (existing) {
|
|
417
|
+
return existing;
|
|
418
|
+
}
|
|
419
|
+
const connectionPromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
420
|
+
(0, logger_1.log)('Crowdin credentials have expired. Requesting new credentials');
|
|
421
|
+
const newCredentials = yield refreshToken(config, credentials);
|
|
422
|
+
(0, logger_1.log)('Saving updated crowdin credentials in the database');
|
|
423
|
+
const newCrowdinCredentials = {
|
|
424
|
+
id: credentials.id,
|
|
425
|
+
appSecret: credentials.appSecret,
|
|
426
|
+
domain: credentials.domain,
|
|
427
|
+
userId: credentials.userId,
|
|
428
|
+
agentId: credentials.agentId,
|
|
429
|
+
organizationId: credentials.organizationId,
|
|
430
|
+
baseUrl: credentials.baseUrl,
|
|
431
|
+
refreshToken: newCredentials.refreshToken,
|
|
432
|
+
accessToken: newCredentials.accessToken,
|
|
433
|
+
expire: (Date.now() / 1000 + newCredentials.expiresIn).toString(),
|
|
434
|
+
type: credentials.type,
|
|
435
|
+
};
|
|
436
|
+
yield (0, storage_1.getStorage)().updateCrowdinCredentials(newCrowdinCredentials);
|
|
437
|
+
const token = (0, _1.decryptData)(config, newCredentials.accessToken);
|
|
438
|
+
return { credentials: newCrowdinCredentials, token };
|
|
439
|
+
}))();
|
|
440
|
+
if (_crowdinRefreshInFlight.size < 1000) {
|
|
441
|
+
_crowdinRefreshInFlight.set(credentials.id, connectionPromise);
|
|
442
|
+
connectionPromise.finally(() => _crowdinRefreshInFlight.delete(credentials.id));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
// should not really happen, but just in case, we don't want to block the refresh if the map is too big
|
|
446
|
+
(0, logger_1.log)('Warning: _crowdinRefreshInFlight map reached 1000 entries; skipping deduplication for this refresh');
|
|
447
|
+
}
|
|
448
|
+
return connectionPromise;
|
|
416
449
|
});
|
|
417
450
|
}
|
|
418
451
|
function refreshToken(config, credentials) {
|
package/out/util/cron.d.ts
CHANGED
package/out/util/cron.js
CHANGED
|
@@ -1,72 +1,25 @@
|
|
|
1
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 () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.NodeCron = void 0;
|
|
37
4
|
exports.initialize = initialize;
|
|
38
5
|
exports.getCron = getCron;
|
|
6
|
+
const logs_formatter_1 = require("@crowdin/logs-formatter");
|
|
39
7
|
const logger_1 = require("./logger");
|
|
40
8
|
/**
|
|
41
9
|
* Default Node.js cron handler that uses node-cron
|
|
42
10
|
*/
|
|
43
11
|
class NodeCron {
|
|
44
12
|
constructor() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Dynamically import node-cron
|
|
48
|
-
Promise.resolve().then(() => __importStar(require('node-cron'))).then((nodeCron) => {
|
|
13
|
+
try {
|
|
14
|
+
const nodeCron = require('node-cron');
|
|
49
15
|
this.cron = nodeCron.default || nodeCron;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.pendingSchedules.forEach(({ expression, task }) => {
|
|
53
|
-
this.cron.schedule(expression, task);
|
|
54
|
-
});
|
|
55
|
-
this.pendingSchedules = [];
|
|
56
|
-
})
|
|
57
|
-
.catch((error) => {
|
|
58
|
-
console.error('Failed to load node-cron:', error);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
59
18
|
throw new Error('node-cron is required for the default cron. Please install it or provide a custom cron in config.');
|
|
60
|
-
}
|
|
19
|
+
}
|
|
61
20
|
}
|
|
62
21
|
schedule(expression, task) {
|
|
63
|
-
|
|
64
|
-
this.cron.schedule(expression, task);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
// Queue the task until node-cron is loaded
|
|
68
|
-
this.pendingSchedules.push({ expression, task });
|
|
69
|
-
}
|
|
22
|
+
this.cron.schedule(expression, task);
|
|
70
23
|
}
|
|
71
24
|
}
|
|
72
25
|
exports.NodeCron = NodeCron;
|
|
@@ -78,13 +31,22 @@ let cron;
|
|
|
78
31
|
function initialize(config) {
|
|
79
32
|
if (config.cron) {
|
|
80
33
|
(0, logger_1.log)('Using custom cron implementation');
|
|
81
|
-
cron = config.cron;
|
|
34
|
+
cron = withContextScope(config.cron);
|
|
82
35
|
}
|
|
83
36
|
else {
|
|
84
37
|
(0, logger_1.log)('Using default NodeCron implementation');
|
|
85
|
-
cron = new NodeCron();
|
|
38
|
+
cron = withContextScope(new NodeCron());
|
|
86
39
|
}
|
|
87
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Gives every scheduled run its own log context scope, isolating it from requests and other runs.
|
|
43
|
+
* A run that itself fans out across tenants opens per-branch scopes with `runWithContext`.
|
|
44
|
+
*/
|
|
45
|
+
function withContextScope(cron) {
|
|
46
|
+
return {
|
|
47
|
+
schedule: (expression, task) => cron.schedule(expression, () => (0, logs_formatter_1.runWithContext)({}, task)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
88
50
|
/**
|
|
89
51
|
* Get the initialized cron instance
|
|
90
52
|
* @returns The cron instance
|
package/out/util/index.d.ts
CHANGED
|
@@ -32,4 +32,5 @@ export declare function getFormattedDate({ date, userTimezone }: {
|
|
|
32
32
|
}): string;
|
|
33
33
|
export declare function kebabCase(str?: string): string;
|
|
34
34
|
export declare function snakeCase(str?: string): string;
|
|
35
|
+
export declare function getEnvInt(name: string, defaultValue: number): number;
|
|
35
36
|
export declare function uniqBy<T>(array: any, key: keyof T): T[];
|
package/out/util/index.js
CHANGED
|
@@ -61,6 +61,7 @@ exports.validateEmail = validateEmail;
|
|
|
61
61
|
exports.getFormattedDate = getFormattedDate;
|
|
62
62
|
exports.kebabCase = kebabCase;
|
|
63
63
|
exports.snakeCase = snakeCase;
|
|
64
|
+
exports.getEnvInt = getEnvInt;
|
|
64
65
|
exports.uniqBy = uniqBy;
|
|
65
66
|
const crypto = __importStar(require("crypto"));
|
|
66
67
|
const storage_1 = require("../storage");
|
|
@@ -345,6 +346,11 @@ function kebabCase(str) {
|
|
|
345
346
|
function snakeCase(str) {
|
|
346
347
|
return kebabCase(str).replace(/-/g, '_');
|
|
347
348
|
}
|
|
349
|
+
function getEnvInt(name, defaultValue) {
|
|
350
|
+
var _a;
|
|
351
|
+
const parsed = Number.parseInt((_a = process.env[name]) !== null && _a !== void 0 ? _a : '', 10);
|
|
352
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
353
|
+
}
|
|
348
354
|
function uniqBy(array, key) {
|
|
349
355
|
if (!Array.isArray(array)) {
|
|
350
356
|
return [];
|
package/out/util/logger.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AxiosError } from 'axios';
|
|
2
|
-
import {
|
|
2
|
+
import { Context } from '@crowdin/logs-formatter';
|
|
3
|
+
import { Config, CrowdinClientRequest, CrowdinContextInfo, CrowdinCredentials, UnauthorizedConfig } from '../types';
|
|
3
4
|
import { IntegrationRequest } from '../modules/integration/types';
|
|
4
5
|
import { Request } from 'express';
|
|
5
6
|
export type LogFunction = (message: string) => void;
|
|
@@ -11,6 +12,27 @@ export type LogContext = {
|
|
|
11
12
|
};
|
|
12
13
|
export declare function initialize(config: Config | UnauthorizedConfig): void;
|
|
13
14
|
export declare function prepareContext(context: CrowdinContextInfo): LogContext;
|
|
15
|
+
/**
|
|
16
|
+
* Maps a framework object to the log context the formatter's record schema expects.
|
|
17
|
+
* A field the source has no value for is omitted, not `''` - the record would emit `''` verbatim.
|
|
18
|
+
* `appIdentifier` is not mapped: it is set once at startup via the formatter's `setup()`.
|
|
19
|
+
* @param source - A `CrowdinContextInfo` yields the full tenant. `CrowdinCredentials` carry no
|
|
20
|
+
* project, so that source yields the organization-level tenant - the shape the record's
|
|
21
|
+
* organization/user fallbacks read.
|
|
22
|
+
* @returns The mapped context. The `Context` return type is load-bearing: the formatter's
|
|
23
|
+
* `setContext()` takes `any` and drops a wrong shape silently.
|
|
24
|
+
*/
|
|
25
|
+
export declare function logContext(source: CrowdinContextInfo): Context;
|
|
26
|
+
export declare function logContext(source: CrowdinCredentials): Context;
|
|
27
|
+
/**
|
|
28
|
+
* Stamps the active scope's log context with the tenant of a Crowdin context - the one place a
|
|
29
|
+
* Crowdin context reaches the formatter. Guarded, so a disabled formatter is never touched (its
|
|
30
|
+
* out-of-scope warning included). Outside any scope the formatter ignores the write and warns once
|
|
31
|
+
* per call site; open one first with `runWithContext` when the work is not a request.
|
|
32
|
+
* @param context - The Crowdin context of the unit of work being logged.
|
|
33
|
+
* @returns The mapped log context, for callers that carry it onward.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setLogCrowdinContext(context: CrowdinContextInfo): Context;
|
|
14
36
|
export declare function withContext(context: CrowdinContextInfo): LogFunction;
|
|
15
37
|
export declare function withContextError(context: CrowdinContextInfo): LogFunction;
|
|
16
38
|
export declare function log(message: string, context?: CrowdinContextInfo): void;
|
package/out/util/logger.js
CHANGED
|
@@ -45,6 +45,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
45
45
|
exports.AppModuleAggregateError = exports.AppUserModuleError = exports.AppModuleError = void 0;
|
|
46
46
|
exports.initialize = initialize;
|
|
47
47
|
exports.prepareContext = prepareContext;
|
|
48
|
+
exports.logContext = logContext;
|
|
49
|
+
exports.setLogCrowdinContext = setLogCrowdinContext;
|
|
48
50
|
exports.withContext = withContext;
|
|
49
51
|
exports.withContextError = withContextError;
|
|
50
52
|
exports.log = log;
|
|
@@ -58,9 +60,11 @@ const logsFormatter = __importStar(require("@crowdin/logs-formatter"));
|
|
|
58
60
|
const storage_1 = require("../storage");
|
|
59
61
|
let logConfig;
|
|
60
62
|
let onError;
|
|
63
|
+
let formatterDisabled = false;
|
|
61
64
|
function initialize(config) {
|
|
62
65
|
logConfig = config.logger;
|
|
63
66
|
onError = config.onError;
|
|
67
|
+
formatterDisabled = Boolean(config.disableLogsFormatter);
|
|
64
68
|
}
|
|
65
69
|
function prepareContext(context) {
|
|
66
70
|
return {
|
|
@@ -69,6 +73,36 @@ function prepareContext(context) {
|
|
|
69
73
|
projectId: context.jwtPayload.context.project_id,
|
|
70
74
|
};
|
|
71
75
|
}
|
|
76
|
+
function logContext(source) {
|
|
77
|
+
if (!('jwtPayload' in source)) {
|
|
78
|
+
return {
|
|
79
|
+
organization: Object.assign({ id: source.organizationId }, (source.domain && { domain: source.domain })),
|
|
80
|
+
user: { id: source.userId },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const payload = source.jwtPayload.context;
|
|
84
|
+
return {
|
|
85
|
+
project: Object.assign(Object.assign({ id: payload.project_id }, (payload.project_identifier && { identifier: payload.project_identifier })), { organization_id: payload.organization_id, user_id: payload.user_id }),
|
|
86
|
+
user: Object.assign({ id: payload.user_id }, (payload.user_login && { login: payload.user_login })),
|
|
87
|
+
// the cast: the formatter's type insists on `domain`, and an absent one must stay absent
|
|
88
|
+
organization: Object.assign({ id: payload.organization_id }, (payload.organization_domain && { domain: payload.organization_domain })),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Stamps the active scope's log context with the tenant of a Crowdin context - the one place a
|
|
93
|
+
* Crowdin context reaches the formatter. Guarded, so a disabled formatter is never touched (its
|
|
94
|
+
* out-of-scope warning included). Outside any scope the formatter ignores the write and warns once
|
|
95
|
+
* per call site; open one first with `runWithContext` when the work is not a request.
|
|
96
|
+
* @param context - The Crowdin context of the unit of work being logged.
|
|
97
|
+
* @returns The mapped log context, for callers that carry it onward.
|
|
98
|
+
*/
|
|
99
|
+
function setLogCrowdinContext(context) {
|
|
100
|
+
const mapped = logContext(context);
|
|
101
|
+
if (!formatterDisabled) {
|
|
102
|
+
logsFormatter.setContext(mapped);
|
|
103
|
+
}
|
|
104
|
+
return mapped;
|
|
105
|
+
}
|
|
72
106
|
function withContext(context) {
|
|
73
107
|
return (message) => log(message, context);
|
|
74
108
|
}
|
|
@@ -76,7 +110,6 @@ function withContextError(context) {
|
|
|
76
110
|
return (e) => logError(e, context);
|
|
77
111
|
}
|
|
78
112
|
function log(message, context) {
|
|
79
|
-
var _a, _b, _c;
|
|
80
113
|
if (logConfig === null || logConfig === void 0 ? void 0 : logConfig.enabled) {
|
|
81
114
|
if (logConfig.log) {
|
|
82
115
|
logConfig.log({ message, context });
|
|
@@ -84,24 +117,7 @@ function log(message, context) {
|
|
|
84
117
|
else {
|
|
85
118
|
let prefix = `[${new Date().toISOString()}]`;
|
|
86
119
|
if (context) {
|
|
87
|
-
|
|
88
|
-
logsFormatter.setContext({
|
|
89
|
-
appIdentifier: context.appIdentifier || '',
|
|
90
|
-
project: {
|
|
91
|
-
id: context.jwtPayload.context.project_id,
|
|
92
|
-
identifier: (_a = context.jwtPayload.context.project_identifier) !== null && _a !== void 0 ? _a : '',
|
|
93
|
-
organization_id: context.jwtPayload.context.organization_id,
|
|
94
|
-
user_id: context.jwtPayload.context.user_id,
|
|
95
|
-
},
|
|
96
|
-
user: {
|
|
97
|
-
id: context.jwtPayload.context.user_id,
|
|
98
|
-
login: (_b = context.jwtPayload.context.user_login) !== null && _b !== void 0 ? _b : '',
|
|
99
|
-
},
|
|
100
|
-
organization: {
|
|
101
|
-
id: context.jwtPayload.context.organization_id,
|
|
102
|
-
domain: (_c = context.jwtPayload.context.organization_domain) !== null && _c !== void 0 ? _c : '',
|
|
103
|
-
},
|
|
104
|
-
});
|
|
120
|
+
setLogCrowdinContext(context);
|
|
105
121
|
prefix += ` Context [${JSON.stringify(prepareContext(context))}]`;
|
|
106
122
|
}
|
|
107
123
|
// eslint-disable-next-line no-console
|
|
@@ -110,30 +126,12 @@ function log(message, context) {
|
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
function logError(e, context) {
|
|
113
|
-
var _a, _b, _c;
|
|
114
129
|
if (onError) {
|
|
115
130
|
onError({ error: e, context });
|
|
116
131
|
}
|
|
117
132
|
else {
|
|
118
133
|
if (context) {
|
|
119
|
-
|
|
120
|
-
logsFormatter.setContext({
|
|
121
|
-
appIdentifier: context.appIdentifier || '',
|
|
122
|
-
project: {
|
|
123
|
-
id: context.jwtPayload.context.project_id,
|
|
124
|
-
identifier: (_a = context.jwtPayload.context.project_identifier) !== null && _a !== void 0 ? _a : '',
|
|
125
|
-
organization_id: context.jwtPayload.context.organization_id,
|
|
126
|
-
user_id: context.jwtPayload.context.user_id,
|
|
127
|
-
},
|
|
128
|
-
user: {
|
|
129
|
-
id: context.jwtPayload.context.user_id,
|
|
130
|
-
login: (_b = context.jwtPayload.context.user_login) !== null && _b !== void 0 ? _b : '',
|
|
131
|
-
},
|
|
132
|
-
organization: {
|
|
133
|
-
id: context.jwtPayload.context.organization_id,
|
|
134
|
-
domain: (_c = context.jwtPayload.context.organization_domain) !== null && _c !== void 0 ? _c : '',
|
|
135
|
-
},
|
|
136
|
-
});
|
|
134
|
+
setLogCrowdinContext(context);
|
|
137
135
|
}
|
|
138
136
|
if (e instanceof AppModuleAggregateError) {
|
|
139
137
|
if (e.errors && e.errors.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowdin/app-project-module",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"sandbox": "vite --config sandbox/vite.config.ts",
|
|
41
|
-
"build": "
|
|
41
|
+
"build": "pnpm run build-css && pnpm run build-main && pnpm run build-ui",
|
|
42
42
|
"build-css": "tailwindcss -i ./ui/styles/tailwind.css -o ./static/css/styles.css --minify",
|
|
43
|
-
"build-main": "tsc -p ./ && cp -R static out && cp logo.png out &&
|
|
43
|
+
"build-main": "tsc -p ./ && cp -R static out && cp logo.png out && pnpm run copy-migrations",
|
|
44
44
|
"build-ui": "rollup -c rollup-ui.config.mjs",
|
|
45
45
|
"watch-main": "tsc --watch -p ./ && cp -R static out && cp logo.png out",
|
|
46
|
-
"format": "
|
|
46
|
+
"format": "pnpm run prettier && pnpm run lint",
|
|
47
47
|
"lint": "eslint --fix \"{src,tests,ui}/**/*.{js,ts,tsx}\"",
|
|
48
48
|
"prettier": "prettier --config .prettierrc {src,ui}/**/*.{ts,tsx} --write",
|
|
49
49
|
"lint-ci": "eslint \"{src,tests}/**/*.{js,ts}\"",
|
|
@@ -58,10 +58,11 @@
|
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@crowdin/crowdin-api-client": "^1.56.1",
|
|
61
|
-
"@crowdin/logs-formatter": "^
|
|
61
|
+
"@crowdin/logs-formatter": "^3.0.0",
|
|
62
62
|
"@godaddy/terminus": "^4.12.1",
|
|
63
63
|
"ajv": "^8.20.0",
|
|
64
64
|
"amqplib": "^2.0.1",
|
|
65
|
+
"axios": "^1.18.1",
|
|
65
66
|
"better-sqlite3": "^13.0.2",
|
|
66
67
|
"cors": "^2.8.6",
|
|
67
68
|
"drizzle-orm": "^0.45.2",
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
"@cloudflare/workers-types": "^4.20260228.0",
|
|
98
99
|
"@eslint/eslintrc": "^3.3.3",
|
|
99
100
|
"@eslint/js": "^10.0.1",
|
|
101
|
+
"@jest/globals": "^30.4.1",
|
|
100
102
|
"@monaco-editor/react": "^4.7.0",
|
|
101
103
|
"@rjsf/core": "^6.6.2",
|
|
102
104
|
"@rjsf/shadcn": "^6.6.2",
|
|
@@ -111,6 +113,7 @@
|
|
|
111
113
|
"@types/better-sqlite3": "^7.6.13",
|
|
112
114
|
"@types/cors": "^2.8.19",
|
|
113
115
|
"@types/express": "^5.0.3",
|
|
116
|
+
"@types/express-serve-static-core": "^5.1.2",
|
|
114
117
|
"@types/jest": "^30.0.0",
|
|
115
118
|
"@types/jsonwebtoken": "^9.0.10",
|
|
116
119
|
"@types/minimatch": "^5.1.2",
|
|
@@ -118,6 +121,7 @@
|
|
|
118
121
|
"@types/pg": "^8.16.0",
|
|
119
122
|
"@types/react": "^19.2.18",
|
|
120
123
|
"@types/react-dom": "^19.2.4",
|
|
124
|
+
"@types/serve-static": "^2.2.0",
|
|
121
125
|
"@types/swagger-jsdoc": "^6.0.4",
|
|
122
126
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
123
127
|
"@typescript-eslint/parser": "^8.56.0",
|
|
@@ -150,5 +154,6 @@
|
|
|
150
154
|
"bugs": {
|
|
151
155
|
"url": "https://github.com/crowdin/app-project-module/issues"
|
|
152
156
|
},
|
|
153
|
-
"homepage": "https://crowdin.github.io/app-project-module"
|
|
157
|
+
"homepage": "https://crowdin.github.io/app-project-module",
|
|
158
|
+
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee"
|
|
154
159
|
}
|