@crowdin/app-project-module 2.10.0 → 2.11.1

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.
@@ -28,6 +28,7 @@ export declare class DrizzleStorage implements Storage {
28
28
  updateIntegrationManagers(id: string, managers: string): Promise<void>;
29
29
  getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
30
30
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
31
+ getIntegrationCredentialsByProject(organization: string, projectId: number): Promise<IntegrationCredentials[]>;
31
32
  deleteIntegrationCredentials(id: string): Promise<void>;
32
33
  deleteAllIntegrationCredentials(crowdinId: string): Promise<void>;
33
34
  saveMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
@@ -13,6 +13,7 @@ exports.DrizzleStorage = void 0;
13
13
  const crypto_1 = require("crypto");
14
14
  const drizzle_orm_1 = require("drizzle-orm");
15
15
  const types_1 = require("../../modules/integration/util/types");
16
+ const token_1 = require("../../util/app-functions/token");
16
17
  const d1_1 = require("./d1");
17
18
  const types_2 = require("./d1/types");
18
19
  const postgresql_1 = require("./postgresql");
@@ -274,6 +275,37 @@ class DrizzleStorage {
274
275
  }));
275
276
  });
276
277
  }
278
+ getIntegrationCredentialsByProject(organization, projectId) {
279
+ return __awaiter(this, void 0, void 0, function* () {
280
+ yield this.migrationPromise;
281
+ // id format: "{organization}__{projectId}__{userId}"
282
+ // parseCrowdinId is the authoritative parser; used as a post-filter in both paths.
283
+ const filterByProjectId = (item) => {
284
+ const { organization: itemOrganization, projectId: itemProjectId } = (0, token_1.parseCrowdinId)(item.id);
285
+ return itemOrganization === organization && itemProjectId === projectId;
286
+ };
287
+ // Org names with underscores make the LIKE pre-filter unreliable (SQL treats _ as a
288
+ // single-char wildcard), so fall back to a full org-level load + JS filter.
289
+ if (organization.includes('_')) {
290
+ const all = yield this.getAllIntegrationCredentials(organization);
291
+ return all.filter(filterByProjectId);
292
+ }
293
+ // LIKE narrows the DB scan to rows whose id starts with the right org and projectId prefix.
294
+ const res = yield this.db.query.integrationCredentials.findMany({
295
+ where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(this.schema.integrationCredentials.crowdin_id, organization), (0, drizzle_orm_1.like)(this.schema.integrationCredentials.id, `${organization}__${projectId}__%`)),
296
+ });
297
+ return (res
298
+ .map((item) => ({
299
+ id: item.id,
300
+ credentials: item.credentials,
301
+ crowdinId: item.crowdin_id,
302
+ managers: item.managers,
303
+ }))
304
+ // SQL LIKE treats _ as a single-char wildcard, so "org__1__%" also matches "org__10__456".
305
+ // parseCrowdinId gives the exact match LIKE cannot guarantee.
306
+ .filter(filterByProjectId));
307
+ });
308
+ }
277
309
  deleteIntegrationCredentials(id) {
278
310
  return __awaiter(this, void 0, void 0, function* () {
279
311
  yield this.migrationPromise;
@@ -28,6 +28,7 @@ export interface Storage {
28
28
  updateIntegrationManagers(id: string, managers: string): Promise<void>;
29
29
  getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
30
30
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
31
+ getIntegrationCredentialsByProject(organization: string, projectId: number): Promise<IntegrationCredentials[]>;
31
32
  deleteIntegrationCredentials(id: string): Promise<void>;
32
33
  deleteAllIntegrationCredentials(crowdinId: string): Promise<void>;
33
34
  saveMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
package/out/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import Crowdin from '@crowdin/crowdin-api-client';
2
+ import type { AppStorage } from '@crowdin/apps-storage';
2
3
  import { Request } from 'express';
3
4
  import { ContextContent } from './modules/context-menu/types';
4
5
  import { CustomMTLogic } from './modules/custom-mt/types';
@@ -393,7 +394,8 @@ export declare enum Scope {
393
394
  AI_PROVIDERS = "ai.provider",
394
395
  AI_PROMPTS = "ai.prompt",
395
396
  AI_PROXIES = "ai.proxy",
396
- APPLICATIONS = "application"
397
+ APPLICATIONS = "application",
398
+ APPLICATION_STORAGE = "application.storage"
397
399
  }
398
400
  export interface CrowdinClientRequest extends Request {
399
401
  crowdinApiClient: Crowdin;
@@ -478,6 +480,18 @@ export interface CrowdinAppUtilities extends CrowdinMetadataStore {
478
480
  }) => (req: any, res: any, next: any) => void;
479
481
  storage: Storage;
480
482
  cron: Cron;
483
+ /**
484
+ * Crowdin-hosted storage for this app installation (the `kv` key-value space).
485
+ * Unlike `storage` (the app's own database), this data lives on the Crowdin side
486
+ * and is shared across the installation. Requires the `crowdin_app` authentication
487
+ * type (the default) - the storage endpoints accept only the application's own
488
+ * token, so every other authentication type is rejected. `kv.user.*` records are
489
+ * private to the identity behind the stored credentials.
490
+ * Optionally enumerate the app's module keys in the type parameter
491
+ * (`crowdinStorage<'main' | 'reports'>(...)`) to get the full
492
+ * `module:{moduleKey}:` prefixes suggested in key positions.
493
+ */
494
+ crowdinStorage: <M extends string = never>(crowdinId: string) => Promise<AppStorage<M>>;
481
495
  }
482
496
  export interface CrowdinMetadataStore {
483
497
  saveMetadata: (options: {
package/out/types.js CHANGED
@@ -36,6 +36,7 @@ var Scope;
36
36
  Scope["AI_PROMPTS"] = "ai.prompt";
37
37
  Scope["AI_PROXIES"] = "ai.proxy";
38
38
  Scope["APPLICATIONS"] = "application";
39
+ Scope["APPLICATION_STORAGE"] = "application.storage";
39
40
  })(Scope || (exports.Scope = Scope = {}));
40
41
  var AccountType;
41
42
  (function (AccountType) {
@@ -266,10 +266,7 @@ function resolveIntegrationAccess(clientId) {
266
266
  const ownerIds = [];
267
267
  let projectIntegrationCredentials = [];
268
268
  if (!integrationCredentials) {
269
- projectIntegrationCredentials = (yield (0, storage_1.getStorage)().getAllIntegrationCredentials(organization)).filter((item) => {
270
- const { organization: itemOrganization, projectId: itemProjectId } = (0, token_1.parseCrowdinId)(item.id);
271
- return itemOrganization === organization && itemProjectId === projectId;
272
- });
269
+ projectIntegrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentialsByProject(organization, projectId);
273
270
  for (const credentials of projectIntegrationCredentials) {
274
271
  ownerIds.push((0, token_1.parseCrowdinId)(credentials.id).userId);
275
272
  if (credentials.managers) {
@@ -0,0 +1,7 @@
1
+ import { type AppStorage, type StorageTransport } from '@crowdin/apps-storage';
2
+ import { Config } from '../types';
3
+ export declare function createCrowdinStorageTransport({ config, crowdinId, }: {
4
+ config: Config;
5
+ crowdinId: string;
6
+ }): StorageTransport;
7
+ export declare function createCrowdinStorage<M extends string = never>(config: Config, crowdinId: string): Promise<AppStorage<M>>;
@@ -0,0 +1,78 @@
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.createCrowdinStorageTransport = createCrowdinStorageTransport;
16
+ exports.createCrowdinStorage = createCrowdinStorage;
17
+ const axios_1 = __importDefault(require("axios"));
18
+ const apps_storage_1 = require("@crowdin/apps-storage");
19
+ const storage_1 = require("../storage");
20
+ const types_1 = require("../types");
21
+ const connection_1 = require("./connection");
22
+ function loadCredentials(crowdinId) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ const credentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(crowdinId);
25
+ if (!credentials) {
26
+ throw new Error('Failed to find Crowdin credentials');
27
+ }
28
+ return credentials;
29
+ });
30
+ }
31
+ function createCrowdinStorageTransport({ config, crowdinId, }) {
32
+ const send = (request, token, apiUrl) => __awaiter(this, void 0, void 0, function* () {
33
+ const url = `${apiUrl}/applications/${encodeURIComponent(config.identifier)}/storage/${request.path}`;
34
+ return axios_1.default.request({
35
+ method: request.method,
36
+ url,
37
+ params: request.query,
38
+ data: request.body,
39
+ headers: { Authorization: `Bearer ${token}` },
40
+ validateStatus: () => true,
41
+ });
42
+ });
43
+ return (request) => __awaiter(this, void 0, void 0, function* () {
44
+ const initial = yield (0, connection_1.prepareCrowdinClient)({
45
+ config,
46
+ credentials: yield loadCredentials(crowdinId),
47
+ autoRenew: true,
48
+ });
49
+ let response = yield send(request, initial.token, initial.client.url);
50
+ if (response.status === 401) {
51
+ const refreshed = yield (0, connection_1.prepareCrowdinClient)({
52
+ config,
53
+ credentials: yield loadCredentials(crowdinId),
54
+ autoRenew: true,
55
+ });
56
+ response = yield send(request, refreshed.token, refreshed.client.url);
57
+ }
58
+ return { status: response.status, body: response.data };
59
+ });
60
+ }
61
+ function createCrowdinStorage(config, crowdinId) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ const authenticationType = config.authenticationType || types_1.AuthenticationType.APP;
64
+ if (authenticationType !== types_1.AuthenticationType.APP) {
65
+ throw new Error(`Crowdin storage is not available for ${authenticationType} authentication because the storage endpoints accept only the application's own token`);
66
+ }
67
+ yield loadCredentials(crowdinId);
68
+ return (0, apps_storage_1.createStorage)({
69
+ transport: createCrowdinStorageTransport({ config, crowdinId }),
70
+ getUserId: () => __awaiter(this, void 0, void 0, function* () {
71
+ const credentials = yield loadCredentials(crowdinId);
72
+ return credentials.userId !== null && credentials.userId !== undefined
73
+ ? String(credentials.userId)
74
+ : undefined;
75
+ }),
76
+ });
77
+ });
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "2.10.0",
3
+ "version": "2.11.1",
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",
@@ -57,8 +57,9 @@
57
57
  "out/**/*"
58
58
  ],
59
59
  "dependencies": {
60
+ "@crowdin/apps-storage": "0.x",
60
61
  "@crowdin/crowdin-api-client": "^1.56.1",
61
- "@crowdin/logs-formatter": "^3.0.0",
62
+ "@crowdin/logs-formatter": "^3.1.0",
62
63
  "@godaddy/terminus": "^4.12.1",
63
64
  "ajv": "^8.20.0",
64
65
  "amqplib": "^2.0.1",