@crowdin/app-project-module 2.1.0 → 2.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/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { getRequestCredentialsMasker, postRequestCredentialsMasker, maskKey };
6
6
  export * from './types';
7
7
  export { Cron } from './util/cron';
8
8
  export { express };
9
+ export { reportInsight as reportAdvisorInsight } from './modules/advisor-inspector/report-insight';
9
10
  export declare const metadataStore: CrowdinMetadataStore;
10
11
  export declare function createApp(clientConfig: ClientConfig): void;
11
12
  export declare function addCrowdinEndpoints(app: Express, clientConfig: Config | ClientConfig): CrowdinAppUtilities;
package/out/index.js CHANGED
@@ -48,7 +48,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
48
48
  return (mod && mod.__esModule) ? mod : { "default": mod };
49
49
  };
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
- exports.metadataStore = exports.express = exports.maskKey = exports.postRequestCredentialsMasker = exports.getRequestCredentialsMasker = void 0;
51
+ exports.metadataStore = exports.reportAdvisorInsight = exports.express = exports.maskKey = exports.postRequestCredentialsMasker = exports.getRequestCredentialsMasker = void 0;
52
52
  exports.createApp = createApp;
53
53
  exports.addCrowdinEndpoints = addCrowdinEndpoints;
54
54
  const logsFormatter = __importStar(require("@crowdin/logs-formatter"));
@@ -90,6 +90,7 @@ const spellCheckApp = __importStar(require("./modules/custom-spell-check"));
90
90
  const aiProvider = __importStar(require("./modules/ai-provider"));
91
91
  const aiPromptProvider = __importStar(require("./modules/ai-prompt-provider"));
92
92
  const editorRightPanelApp = __importStar(require("./modules/editor-right-panel"));
93
+ const advisorInspector = __importStar(require("./modules/advisor-inspector"));
93
94
  const externalQaCheck = __importStar(require("./modules/external-qa-check"));
94
95
  const fileProcessingApps = __importStar(require("./modules/file-processing"));
95
96
  const integrationApp = __importStar(require("./modules/integration"));
@@ -110,6 +111,8 @@ const automationAction = __importStar(require("./modules/automation-action"));
110
111
  const authGuard = __importStar(require("./modules/auth-guard"));
111
112
  const subscription_1 = require("./util/subscription");
112
113
  __exportStar(require("./types"), exports);
114
+ var report_insight_1 = require("./modules/advisor-inspector/report-insight");
115
+ Object.defineProperty(exports, "reportAdvisorInsight", { enumerable: true, get: function () { return report_insight_1.reportInsight; } });
113
116
  exports.metadataStore = {
114
117
  getMetadata: (id) => {
115
118
  return storage.getStorage().getMetadata(id);
@@ -257,6 +260,7 @@ function addCrowdinEndpoints(app, clientConfig) {
257
260
  aiProvider.register({ config, app });
258
261
  aiPromptProvider.register({ config, app });
259
262
  externalQaCheck.register({ config, app });
263
+ advisorInspector.register({ config, app });
260
264
  webhooks.register({ config, app });
261
265
  workflowStepType.register({ config, app });
262
266
  aiRequestProcessors.register({ config, app });
@@ -0,0 +1,4 @@
1
+ import { Response } from 'express';
2
+ import { AdvisorInspectorModule } from '../types';
3
+ import { CrowdinClientRequest } from '../../../types';
4
+ export default function handle(inspector: AdvisorInspectorModule, moduleKey: string): (req: import("express").Request | CrowdinClientRequest, res: Response, next: Function) => void;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.default = handle;
13
+ const util_1 = require("../../../util");
14
+ const logger_1 = require("../../../util/logger");
15
+ function handle(inspector, moduleKey) {
16
+ return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
17
+ const body = req.body;
18
+ (0, logger_1.log)(`Received advisor-inspector recheck nudge: ${JSON.stringify(body)}`);
19
+ // Ack first — BE expects a fast 2xx; the callback runs after the response flushes.
20
+ res.send({ data: { accepted: true } });
21
+ // Capture context locals BEFORE scheduling — Express may recycle `req` post-response.
22
+ const projectId = body.projectId;
23
+ const inspectorId = body.inspectorKey;
24
+ const crowdinId = req.crowdinContext.crowdinId;
25
+ const context = req.crowdinContext;
26
+ const client = req.crowdinApiClient;
27
+ // queueMicrotask is universal (works on Node AND Cloudflare Workers; setImmediate is Node-only).
28
+ queueMicrotask(() => __awaiter(this, void 0, void 0, function* () {
29
+ try {
30
+ yield inspector.onRecheck({
31
+ projectId,
32
+ moduleKey,
33
+ inspectorId,
34
+ crowdinId,
35
+ client,
36
+ context,
37
+ });
38
+ }
39
+ catch (e) {
40
+ (0, logger_1.log)(`advisor-inspector onRecheck error: ${(0, logger_1.getErrorMessage)(e)}`);
41
+ }
42
+ }));
43
+ }));
44
+ }
@@ -0,0 +1,6 @@
1
+ import { Express } from 'express';
2
+ import { Config } from '../../types';
3
+ export declare function register({ config, app }: {
4
+ config: Config;
5
+ app: Express;
6
+ }): void;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.register = register;
7
+ const crowdin_client_1 = require("../../middlewares/crowdin-client");
8
+ const json_response_1 = require("../../middlewares/json-response");
9
+ const render_ui_module_1 = __importDefault(require("../../middlewares/render-ui-module"));
10
+ const ui_module_1 = __importDefault(require("../../middlewares/ui-module"));
11
+ const recheck_1 = __importDefault(require("./handlers/recheck"));
12
+ const key_1 = require("./key");
13
+ function register({ config, app }) {
14
+ if (!config.advisorInspector) {
15
+ return;
16
+ }
17
+ const items = config.advisorInspector;
18
+ const isSingle = items.length === 1;
19
+ for (const [index, inspector] of items.entries()) {
20
+ const key = (0, key_1.resolveAdvisorInspectorKey)(config, inspector, index, isSingle);
21
+ const base = `/advisor-inspector/${key}`;
22
+ // Recheck nudge endpoint (BE → app, fire-and-forget).
23
+ // checkSubscriptionExpiration=false: nudges are platform traffic; 402-ing here
24
+ // would mask the async flow. App-paid features should gate inside onRecheck.
25
+ app.post(`${base}/recheck`, json_response_1.handle, (0, crowdin_client_1.handle)({
26
+ config,
27
+ optional: false,
28
+ checkSubscriptionExpiration: false,
29
+ moduleKey: key,
30
+ }), (0, recheck_1.default)(inspector, key));
31
+ if (inspector.uiPath || inspector.formSchema) {
32
+ app.use(`${base}/iframe`, (0, ui_module_1.default)({ config, moduleType: key }), (0, render_ui_module_1.default)(Object.assign(Object.assign({}, inspector), { key }), config));
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,4 @@
1
+ import { AdvisorInspectorModule } from './types';
2
+ export declare function resolveAdvisorInspectorKey(config: {
3
+ identifier: string;
4
+ }, item: AdvisorInspectorModule, index: number, isSingle: boolean): string;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAdvisorInspectorKey = resolveAdvisorInspectorKey;
4
+ const normalize_module_1 = require("../../util/normalize-module");
5
+ function resolveAdvisorInspectorKey(config, item, index, isSingle) {
6
+ return (0, normalize_module_1.resolveInstanceKey)({
7
+ identifier: config.identifier,
8
+ suffix: 'advisor-inspector',
9
+ item,
10
+ index,
11
+ isSingle,
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ import { Config } from '../../types';
2
+ import { ReportInsightArgs } from './types';
3
+ export declare function reportInsight(config: Config, args: ReportInsightArgs): Promise<void>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.reportInsight = reportInsight;
16
+ const axios_1 = __importDefault(require("axios"));
17
+ const storage_1 = require("../../storage");
18
+ const connection_1 = require("../../util/connection");
19
+ const logger_1 = require("../../util/logger");
20
+ function buildBody(args) {
21
+ var _a, _b;
22
+ return Object.assign(Object.assign(Object.assign({ outcome: args.outcome }, (args.checkedAt !== undefined && { checkedAt: args.checkedAt })), { metrics: (_a = args.metrics) !== null && _a !== void 0 ? _a : [], recommendations: (_b = args.recommendations) !== null && _b !== void 0 ? _b : [] }), (args.payload !== undefined && { payload: args.payload }));
23
+ }
24
+ function put(url_1, body_1, token_1) {
25
+ return __awaiter(this, arguments, void 0, function* (url, body, token, timeoutMs = 300000) {
26
+ yield axios_1.default.put(url, body, {
27
+ headers: { Authorization: `Bearer ${token}` },
28
+ timeout: timeoutMs,
29
+ });
30
+ });
31
+ }
32
+ function isUnauthorized(e) {
33
+ const response = e === null || e === void 0 ? void 0 : e.response;
34
+ return !!response && response.status === 401;
35
+ }
36
+ function reportInsight(config, args) {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ const credentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(args.crowdinId);
39
+ if (!credentials) {
40
+ throw new Error(`reportInsight: no credentials for crowdinId=${args.crowdinId}`);
41
+ }
42
+ const body = buildBody(args);
43
+ const initial = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew: true });
44
+ let token = initial.token;
45
+ const url = `${initial.client.url}/projects/${args.projectId}` +
46
+ `/applications/${config.identifier}` +
47
+ `/modules/${args.moduleKey}/advisors/insights`;
48
+ try {
49
+ yield put(url, body, token, args.timeoutMs);
50
+ }
51
+ catch (e) {
52
+ if (isUnauthorized(e)) {
53
+ ({ token } = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew: true }));
54
+ yield put(url, body, token, args.timeoutMs);
55
+ return;
56
+ }
57
+ (0, logger_1.log)(`reportInsight PUT failed: ${(0, logger_1.getErrorMessage)(e)}`);
58
+ throw e;
59
+ }
60
+ });
61
+ }
@@ -0,0 +1,59 @@
1
+ import Crowdin from '@crowdin/crowdin-api-client';
2
+ import { CrowdinContextInfo, Environments, ModuleKey, UiModule } from '../../types';
3
+ export type AdvisorInspectorSeverity = 'high' | 'medium' | 'low';
4
+ export type AdvisorInspectorRefreshPolicy = 'hourly' | 'daily';
5
+ export type AdvisorInspectorCategory = 'context';
6
+ export type AdvisorInspectorOutcome = 'flagged' | 'clear' | 'not_applicable';
7
+ export interface AdvisorInspectorMetric {
8
+ key: string;
9
+ value: number;
10
+ unit?: 'percent' | 'count';
11
+ /** Comparison baseline shown in the UI */
12
+ threshold?: number;
13
+ tone?: 'default' | 'success' | 'danger';
14
+ checkedAt?: string;
15
+ }
16
+ export interface AdvisorInspectorRecommendation {
17
+ id: string;
18
+ primary?: boolean;
19
+ params?: Record<string, unknown>;
20
+ }
21
+ export interface AdvisorInspectorRecheckArgs {
22
+ projectId: number;
23
+ moduleKey: string;
24
+ /** prefixed, BE-supplied (e.g. 'app:my-app:daily-llm-check') */
25
+ inspectorId: string;
26
+ crowdinId: string;
27
+ client?: Crowdin;
28
+ context: CrowdinContextInfo;
29
+ }
30
+ export interface AdvisorInspectorRecheckRequest {
31
+ projectId: number;
32
+ inspectorKey: string;
33
+ }
34
+ export interface ReportInsightArgs {
35
+ crowdinId: string;
36
+ projectId: number;
37
+ moduleKey: string;
38
+ outcome: AdvisorInspectorOutcome;
39
+ metrics?: AdvisorInspectorMetric[];
40
+ recommendations?: AdvisorInspectorRecommendation[];
41
+ /**
42
+ * Free-form payload surfaced to the FE iframe via `formData`. BE requires an array
43
+ * (PHP-style — indexed or associative) and rejects raw scalars.
44
+ */
45
+ payload?: unknown[] | Record<string, unknown>;
46
+ /** RFC3339; BE defaults to now() and clamps if >1h future / >7d past */
47
+ checkedAt?: string;
48
+ /** PUT request timeout in ms; default 300_000 (5 minutes) */
49
+ timeoutMs?: number;
50
+ }
51
+ export interface AdvisorInspectorModule extends Environments, ModuleKey, UiModule {
52
+ title: string;
53
+ /** Optional summary line; ≤70 chars */
54
+ summary?: string;
55
+ severity: AdvisorInspectorSeverity;
56
+ category: AdvisorInspectorCategory;
57
+ refreshPolicy: AdvisorInspectorRefreshPolicy;
58
+ onRecheck: (args: AdvisorInspectorRecheckArgs) => Promise<void>;
59
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -9,6 +9,7 @@ const api_1 = require("./api/api");
9
9
  const util_2 = require("./workflow-step-type/util");
10
10
  const index_1 = require("./automation-action/util/index");
11
11
  const ai_request_processors_1 = require("./ai-request-processors");
12
+ const key_1 = require("./advisor-inspector/key");
12
13
  function normalizeEnvironments(environments) {
13
14
  if (Array.isArray(environments)) {
14
15
  return environments;
@@ -475,6 +476,20 @@ function handle(config) {
475
476
  return Object.assign(Object.assign({ key, name: item.name || config.name, description: item.description || config.description, runQaCheckUrl: `${base}/validate` }, (item.batchSize ? { getBatchSizeUrl: `${base}/batch-size` } : {})), (uiModule ? { url: `${base}/settings/` + (uiModule.fileName || 'index.html') } : {}));
476
477
  });
477
478
  }
479
+ if (config.advisorInspector) {
480
+ config.advisorInspector = config.advisorInspector.map((item) => (Object.assign({}, item)));
481
+ const items = config.advisorInspector;
482
+ const isSingle = items.length === 1;
483
+ modules['advisor-inspector'] = items.map((item, index) => {
484
+ const key = (0, key_1.resolveAdvisorInspectorKey)(config, item, index, isSingle);
485
+ item.key = key;
486
+ const base = `/advisor-inspector/${key}`;
487
+ const hasIframe = !!(item.uiPath || item.formSchema);
488
+ return Object.assign(Object.assign(Object.assign(Object.assign({ key, name: item.name || config.name, title: item.title }, (item.summary && { summary: item.summary })), { severity: item.severity, category: item.category, refreshPolicy: item.refreshPolicy, recheckUrl: `${base}/recheck` }), (hasIframe && {
489
+ url: `${base}/iframe/${item.fileName || 'index.html'}`,
490
+ })), (item.environments && { environments: normalizeEnvironments(item.environments) }));
491
+ });
492
+ }
478
493
  if (config.workflowStepType) {
479
494
  // prevent possible overrides of the other modules
480
495
  config.workflowStepType = config.workflowStepType.map((workflowStep) => (Object.assign({}, workflowStep)));
package/out/types.d.ts CHANGED
@@ -17,6 +17,7 @@ import { Webhook } from './modules/webhooks/types';
17
17
  import { WorkflowStepTypeModule } from './modules/workflow-step-type/types';
18
18
  import { AiRequestProcessorModule, AiStreamProcessorModule } from './modules/ai-request-processors/types';
19
19
  import { AutomationActionModule } from './modules/automation-action/types';
20
+ import { AdvisorInspectorModule } from './modules/advisor-inspector/types';
20
21
  import { AuthGuardModule } from './modules/auth-guard/types';
21
22
  import { Cron } from './util/cron';
22
23
  import { PostgreStorageConfig } from './storage/drizzle/postgresql';
@@ -276,6 +277,11 @@ export interface ClientConfig extends ImagePath {
276
277
  * qa check module
277
278
  */
278
279
  externalQaCheck?: (ExternalQaCheckModule & ImagePath)[];
280
+ /**
281
+ * Advisor inspector module — app-provided checks surfaced in the Crowdin Advisor.
282
+ * Receives recheck nudges from BE and pushes results via reportAdvisorInsight().
283
+ */
284
+ advisorInspector?: AdvisorInspectorModule[];
279
285
  /**
280
286
  * webhook modules
281
287
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "2.1.0",
3
+ "version": "2.2.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",