@crowdin/app-project-module 3.1.0 → 3.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/out/middlewares/crowdin-client.d.ts +4 -2
- package/out/middlewares/crowdin-client.js +51 -14
- package/out/middlewares/integration-credentials.js +34 -12
- package/out/modules/integration/handlers/crowdin-files.js +34 -1
- package/out/modules/integration/types.d.ts +1 -0
- package/out/types.d.ts +1 -0
- package/out/util/connection.d.ts +5 -0
- package/out/util/connection.js +7 -0
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import Crowdin from '@crowdin/crowdin-api-client';
|
|
1
|
+
import type Crowdin from '@crowdin/crowdin-api-client';
|
|
2
2
|
import { Response } from 'express';
|
|
3
3
|
import { Config, CrowdinClientRequest, CrowdinContextInfo, SubscriptionInfo } from '../types';
|
|
4
4
|
import { LogErrorFunction, LogFunction } from '../util/logger';
|
|
5
|
-
export declare function prepareCrowdinRequest({ jwtToken, config, optional, checkSubscriptionExpiration, moduleKey, }: {
|
|
5
|
+
export declare function prepareCrowdinRequest({ jwtToken, config, optional, checkSubscriptionExpiration, moduleKey, crowdinToken, projectId, }: {
|
|
6
6
|
jwtToken: string;
|
|
7
7
|
config: Config;
|
|
8
8
|
optional: boolean;
|
|
9
9
|
checkSubscriptionExpiration: boolean;
|
|
10
10
|
moduleKey: string[] | string | undefined;
|
|
11
|
+
crowdinToken?: string;
|
|
12
|
+
projectId?: number;
|
|
11
13
|
}): Promise<{
|
|
12
14
|
context: CrowdinContextInfo;
|
|
13
15
|
logInfo: LogFunction;
|
|
@@ -20,8 +20,17 @@ const logger_1 = require("../util/logger");
|
|
|
20
20
|
const subscription_1 = require("../util/subscription");
|
|
21
21
|
const integration_access_denied_1 = require("./integration-access-denied");
|
|
22
22
|
const token_1 = require("../util/app-functions/token");
|
|
23
|
+
function assertModuleKey(moduleKey, jwtModule) {
|
|
24
|
+
if (!moduleKey || !jwtModule) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const matches = typeof moduleKey === 'string' ? moduleKey === jwtModule : moduleKey.includes(jwtModule);
|
|
28
|
+
if (!matches) {
|
|
29
|
+
throw new Error("Module key doesn't match");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
23
32
|
function prepareCrowdinRequest(_a) {
|
|
24
|
-
return __awaiter(this, arguments, void 0, function* ({ jwtToken, config, optional = false, checkSubscriptionExpiration = true, moduleKey, }) {
|
|
33
|
+
return __awaiter(this, arguments, void 0, function* ({ jwtToken, config, optional = false, checkSubscriptionExpiration = true, moduleKey, crowdinToken, projectId, }) {
|
|
25
34
|
(0, logger_1.log)('Validating jwt token from incoming request');
|
|
26
35
|
const jwtPayload = yield (0, token_1.validateJwtToken)(jwtToken, config.clientSecret, config.jwtValidationOptions);
|
|
27
36
|
const context = {
|
|
@@ -32,6 +41,29 @@ function prepareCrowdinRequest(_a) {
|
|
|
32
41
|
};
|
|
33
42
|
const logInfo = (0, logger_1.withContext)(context);
|
|
34
43
|
const logError = (0, logger_1.withContextError)(context);
|
|
44
|
+
if (crowdinToken) {
|
|
45
|
+
logInfo('Using caller-supplied Crowdin token');
|
|
46
|
+
assertModuleKey(moduleKey, jwtPayload.module);
|
|
47
|
+
const client = yield (0, connection_1.buildCrowdinClientFromPat)({
|
|
48
|
+
config,
|
|
49
|
+
token: crowdinToken,
|
|
50
|
+
// For enterprise the org-specific URL implicitly restricts the token to this org.
|
|
51
|
+
// For Crowdin.com (no domain) we fall back to project-level access validation below.
|
|
52
|
+
organization: jwtPayload.domain || undefined,
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
yield client.usersApi.getAuthenticatedUser();
|
|
56
|
+
// For non-enterprise (no domain) there is no org-scoped URL, so validate by
|
|
57
|
+
// checking that the PAT has access to the project named in the JWT / request.
|
|
58
|
+
if (!jwtPayload.domain && projectId) {
|
|
59
|
+
yield client.projectsGroupsApi.getProject(projectId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (_b) {
|
|
63
|
+
throw new util_1.CodeError('Supplied crowdinToken is invalid or does not belong to this organization', 403);
|
|
64
|
+
}
|
|
65
|
+
return { context, client, logInfo, logError };
|
|
66
|
+
}
|
|
35
67
|
logInfo('Loading crowdin credentials');
|
|
36
68
|
const credentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(context.crowdinId);
|
|
37
69
|
if (!credentials) {
|
|
@@ -40,14 +72,7 @@ function prepareCrowdinRequest(_a) {
|
|
|
40
72
|
}
|
|
41
73
|
throw new Error("Can't find organization by id");
|
|
42
74
|
}
|
|
43
|
-
|
|
44
|
-
if (typeof moduleKey === 'string' && moduleKey !== jwtPayload.module) {
|
|
45
|
-
throw new Error("Module key doesn't match");
|
|
46
|
-
}
|
|
47
|
-
if (Array.isArray(moduleKey) && !moduleKey.includes(jwtPayload.module)) {
|
|
48
|
-
throw new Error("Module key doesn't match");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
75
|
+
assertModuleKey(moduleKey, jwtPayload.module);
|
|
51
76
|
logInfo('Building crowdin client instance');
|
|
52
77
|
const { client, token } = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew: true, context });
|
|
53
78
|
let subscriptionInfo;
|
|
@@ -67,22 +92,25 @@ function prepareCrowdinRequest(_a) {
|
|
|
67
92
|
}
|
|
68
93
|
function handle({ config, optional = false, checkSubscriptionExpiration = true, moduleKey, isIntegration = false, isLogout = false, }) {
|
|
69
94
|
return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
70
|
-
var _a, _b;
|
|
95
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
71
96
|
const jwtToken = getToken(req);
|
|
72
97
|
if (!jwtToken) {
|
|
73
98
|
(0, logger_1.temporaryErrorDebug)('Access denied: crowdin-client', req);
|
|
74
99
|
return res.status(403).send({ error: 'Access denied' });
|
|
75
100
|
}
|
|
76
101
|
try {
|
|
102
|
+
const isAgentCall = req.isApiCall && !!((_a = req.body) === null || _a === void 0 ? void 0 : _a.crowdinToken);
|
|
77
103
|
const data = yield prepareCrowdinRequest({
|
|
78
104
|
jwtToken,
|
|
79
105
|
config,
|
|
80
106
|
optional,
|
|
81
107
|
checkSubscriptionExpiration,
|
|
82
108
|
moduleKey,
|
|
109
|
+
crowdinToken: isAgentCall ? req.body.crowdinToken : undefined,
|
|
110
|
+
projectId: isAgentCall ? (_b = req.body) === null || _b === void 0 ? void 0 : _b.projectId : undefined,
|
|
83
111
|
});
|
|
84
|
-
if ((
|
|
85
|
-
if (req.isApiCall && !((
|
|
112
|
+
if ((_c = config.api) === null || _c === void 0 ? void 0 : _c.default) {
|
|
113
|
+
if (req.isApiCall && !((_d = req.body) === null || _d === void 0 ? void 0 : _d.projectId)) {
|
|
86
114
|
return res.status(400).json({
|
|
87
115
|
error: {
|
|
88
116
|
message: 'Missing required parameter: projectId',
|
|
@@ -91,7 +119,10 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
|
|
|
91
119
|
}
|
|
92
120
|
data.context = (0, api_1.updateCrowdinContext)(req, data.context);
|
|
93
121
|
}
|
|
94
|
-
|
|
122
|
+
// Skip integration access check when the caller supplies their own credentials —
|
|
123
|
+
// integration-credentials middleware handles those via the injected path.
|
|
124
|
+
const hasInjectedIntegrationCreds = req.isApiCall && !!((_e = req.body) === null || _e === void 0 ? void 0 : _e.integrationCredentials);
|
|
125
|
+
if (isIntegration && data.client && !hasInjectedIntegrationCreds) {
|
|
95
126
|
const result = yield (0, connection_1.resolveIntegrationAccess)(data.context.clientId);
|
|
96
127
|
if (!result.integrationCredentials && !isLogout) {
|
|
97
128
|
const owners = yield (0, integration_access_denied_1.getIntegrationManagedBy)({
|
|
@@ -121,12 +152,18 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
|
|
|
121
152
|
req.subscriptionInfo = data.subscriptionInfo;
|
|
122
153
|
req.logInfo = data.logInfo;
|
|
123
154
|
req.logError = data.logError;
|
|
155
|
+
// Strip secrets from req.body so no downstream handler ever sees raw tokens.
|
|
156
|
+
// crowdinToken has been consumed; move integrationCredentials to a typed slot
|
|
157
|
+
// so the integration-credentials middleware can still pick it up.
|
|
158
|
+
req.injectedIntegrationCredentials = (_f = req.body) === null || _f === void 0 ? void 0 : _f.integrationCredentials;
|
|
159
|
+
(_g = req.body) === null || _g === void 0 ? true : delete _g.crowdinToken;
|
|
160
|
+
(_h = req.body) === null || _h === void 0 ? true : delete _h.integrationCredentials;
|
|
124
161
|
next();
|
|
125
162
|
}
|
|
126
163
|
catch (e) {
|
|
127
164
|
const errorMessage = (0, logger_1.getErrorMessage)(e);
|
|
128
165
|
if (e instanceof util_1.CodeError) {
|
|
129
|
-
return res.status(402).send({ message: errorMessage || 'Error', code: e.code });
|
|
166
|
+
return res.status((_j = e.code) !== null && _j !== void 0 ? _j : 402).send({ message: errorMessage || 'Error', code: e.code });
|
|
130
167
|
}
|
|
131
168
|
(0, logger_1.logError)(e);
|
|
132
169
|
return res.status(403).send({ error: errorMessage || 'Error' });
|
|
@@ -15,9 +15,42 @@ const storage_1 = require("../storage");
|
|
|
15
15
|
const util_1 = require("../util");
|
|
16
16
|
const connection_1 = require("../util/connection");
|
|
17
17
|
const integration_access_denied_1 = require("./integration-access-denied");
|
|
18
|
+
function loadIntegrationSettings(clientId, integration, req) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
|
|
21
|
+
let settings = (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) ? JSON.parse(integrationConfig.config) || {} : {};
|
|
22
|
+
if (integration.normalizeSettings) {
|
|
23
|
+
settings = yield integration.normalizeSettings({
|
|
24
|
+
settings,
|
|
25
|
+
credentials: req.integrationCredentials,
|
|
26
|
+
client: req.crowdinApiClient,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return settings;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
18
32
|
function handle({ config, integration, optional = false, isLogout = false, }) {
|
|
19
33
|
return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
20
34
|
const isApiCall = req === null || req === void 0 ? void 0 : req.isApiCall;
|
|
35
|
+
// crowdin-client middleware moves integrationCredentials out of req.body before next(),
|
|
36
|
+
// so secrets are never visible to handlers. Read from the typed slot it left behind.
|
|
37
|
+
const rawInjected = req.injectedIntegrationCredentials;
|
|
38
|
+
if (isApiCall && rawInjected) {
|
|
39
|
+
delete req.injectedIntegrationCredentials;
|
|
40
|
+
req.logInfo('Using caller-supplied integration credentials');
|
|
41
|
+
req.integrationCredentials = typeof rawInjected === 'string' ? { accessToken: rawInjected } : rawInjected;
|
|
42
|
+
if (integration.checkConnection) {
|
|
43
|
+
try {
|
|
44
|
+
yield integration.checkConnection(req.integrationCredentials);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return res.status(401).json({ error: { message: 'Supplied integrationCredentials are invalid' } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const clientId = req.crowdinContext.clientId;
|
|
51
|
+
req.integrationSettings = yield loadIntegrationSettings(clientId, integration, req);
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
21
54
|
req.logInfo(`Loading integration credentials for client ${req.crowdinContext.clientId}`);
|
|
22
55
|
const { integrationCredentials: resolvedIntegrationCredentials, projectIntegrationCredentials, projectId, clientId: resolvedClientId, ownerIds, } = yield (0, connection_1.resolveIntegrationAccess)(req.crowdinContext.clientId);
|
|
23
56
|
let integrationCredentials = resolvedIntegrationCredentials;
|
|
@@ -79,18 +112,7 @@ function handle({ config, integration, optional = false, isLogout = false, }) {
|
|
|
79
112
|
throw new util_1.CodeError(message, 401);
|
|
80
113
|
}
|
|
81
114
|
}
|
|
82
|
-
|
|
83
|
-
if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) {
|
|
84
|
-
let integrationSettings = JSON.parse(integrationConfig.config) || {};
|
|
85
|
-
if (integration.normalizeSettings) {
|
|
86
|
-
integrationSettings = yield integration.normalizeSettings({
|
|
87
|
-
settings: integrationSettings,
|
|
88
|
-
credentials: req.integrationCredentials,
|
|
89
|
-
client: req.crowdinApiClient,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
req.integrationSettings = integrationSettings;
|
|
93
|
-
}
|
|
115
|
+
req.integrationSettings = yield loadIntegrationSettings(clientId, integration, req);
|
|
94
116
|
next();
|
|
95
117
|
}));
|
|
96
118
|
}
|
|
@@ -13,11 +13,24 @@ exports.default = handle;
|
|
|
13
13
|
const util_1 = require("../../../util");
|
|
14
14
|
const defaults_1 = require("../util/defaults");
|
|
15
15
|
const logger_1 = require("../../../util/logger");
|
|
16
|
+
const types_1 = require("../types");
|
|
16
17
|
const files_1 = require("../util/files");
|
|
18
|
+
const storage_1 = require("../../../storage");
|
|
17
19
|
function handle(config, integration) {
|
|
18
20
|
return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
19
|
-
var _a;
|
|
21
|
+
var _a, _b;
|
|
20
22
|
const mode = (_a = req.query) === null || _a === void 0 ? void 0 : _a.mode;
|
|
23
|
+
const externalId = (_b = req.query) === null || _b === void 0 ? void 0 : _b.externalId;
|
|
24
|
+
// Reverse lookup: given an integration file ID, return the mapped Crowdin file entry.
|
|
25
|
+
if (externalId) {
|
|
26
|
+
const syncedData = yield (0, storage_1.getStorage)().getSyncedData(req.crowdinContext.clientId, req.crowdinContext.crowdinId, types_1.Provider.INTEGRATION);
|
|
27
|
+
const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
|
|
28
|
+
const entry = syncedFiles.find((f) => f.id === externalId);
|
|
29
|
+
if (!(entry === null || entry === void 0 ? void 0 : entry.crowdinFileId)) {
|
|
30
|
+
return res.status(404).json({ error: { message: 'No Crowdin file mapped to this external ID' } });
|
|
31
|
+
}
|
|
32
|
+
return res.json({ id: String(entry.crowdinFileId), externalFileId: externalId });
|
|
33
|
+
}
|
|
21
34
|
req.logInfo(`Loading crowdin files${mode ? ` (mode: ${mode})` : ''}`);
|
|
22
35
|
if (integration.getCrowdinFiles) {
|
|
23
36
|
const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, req.crowdinContext.jwtPayload.context.project_id);
|
|
@@ -41,6 +54,26 @@ function handle(config, integration) {
|
|
|
41
54
|
files,
|
|
42
55
|
});
|
|
43
56
|
}
|
|
57
|
+
// Path B: populate externalFileId from stored integrationFileId→crowdinFileId mapping
|
|
58
|
+
// for files where the app developer hasn't supplied it directly (Path A).
|
|
59
|
+
// A leaf is either a typed file or explicitly flagged with nodeType '1';
|
|
60
|
+
// folders (nodeType '0') and branches (nodeType '2') without a type are skipped.
|
|
61
|
+
const isFileLeaf = (f) => f.type !== undefined || f.nodeType === '1';
|
|
62
|
+
const needsMapping = files.some((f) => isFileLeaf(f) && !f.externalFileId);
|
|
63
|
+
if (needsMapping) {
|
|
64
|
+
const syncedData = yield (0, storage_1.getStorage)().getSyncedData(req.crowdinContext.clientId, req.crowdinContext.crowdinId, types_1.Provider.INTEGRATION);
|
|
65
|
+
if (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) {
|
|
66
|
+
const syncedFiles = JSON.parse(syncedData.files);
|
|
67
|
+
const byCrowdinFileId = new Map(syncedFiles
|
|
68
|
+
.filter((e) => e.crowdinFileId !== null && e.crowdinFileId !== undefined)
|
|
69
|
+
.map((e) => [String(e.crowdinFileId), e.id]));
|
|
70
|
+
if (byCrowdinFileId.size > 0) {
|
|
71
|
+
files = files.map((f) => !isFileLeaf(f) || f.externalFileId || !byCrowdinFileId.has(f.id)
|
|
72
|
+
? f
|
|
73
|
+
: Object.assign(Object.assign({}, f), { externalFileId: byCrowdinFileId.get(f.id) }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
44
77
|
req.logInfo(`Returning ${files.length} items`);
|
|
45
78
|
res.send(files);
|
|
46
79
|
}
|
package/out/types.d.ts
CHANGED
package/out/util/connection.d.ts
CHANGED
|
@@ -17,5 +17,10 @@ export interface IntegrationAccessResult {
|
|
|
17
17
|
clientId: string;
|
|
18
18
|
ownerIds: number[];
|
|
19
19
|
}
|
|
20
|
+
export declare function buildCrowdinClientFromPat({ config, token, organization, }: {
|
|
21
|
+
config: Config;
|
|
22
|
+
token: string;
|
|
23
|
+
organization?: string;
|
|
24
|
+
}): Promise<Crowdin>;
|
|
20
25
|
export declare function resolveIntegrationAccess(clientId: string): Promise<IntegrationAccessResult>;
|
|
21
26
|
export declare function prepareIntegrationCredentials(config: Config, integration: IntegrationLogic, integrationCredentials: IntegrationCredentials): Promise<any>;
|
package/out/util/connection.js
CHANGED
|
@@ -46,6 +46,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.prepareCrowdinClient = prepareCrowdinClient;
|
|
49
|
+
exports.buildCrowdinClientFromPat = buildCrowdinClientFromPat;
|
|
49
50
|
exports.resolveIntegrationAccess = resolveIntegrationAccess;
|
|
50
51
|
exports.prepareIntegrationCredentials = prepareIntegrationCredentials;
|
|
51
52
|
const crowdin_api_client_1 = __importDefault(require("@crowdin/crowdin-api-client"));
|
|
@@ -256,6 +257,12 @@ function prepareCrowdinClient(_a) {
|
|
|
256
257
|
};
|
|
257
258
|
});
|
|
258
259
|
}
|
|
260
|
+
function buildCrowdinClientFromPat(_a) {
|
|
261
|
+
return __awaiter(this, arguments, void 0, function* ({ config, token, organization, }) {
|
|
262
|
+
var _b;
|
|
263
|
+
return new crowdin_api_client_1.default({ token, organization, apiDomain: (_b = config.crowdinUrls) === null || _b === void 0 ? void 0 : _b.apiDomain });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
259
266
|
function resolveIntegrationAccess(clientId) {
|
|
260
267
|
return __awaiter(this, void 0, void 0, function* () {
|
|
261
268
|
const { organization, projectId, userId } = (0, token_1.parseCrowdinId)(clientId);
|
package/package.json
CHANGED