@acarmisc/backstage-plugin-litellm-backend 0.8.2 → 0.9.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/dist/index.cjs.js +8060 -15
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.js +26 -0
- package/dist/plugin.js +12 -2
- package/dist/provisioning.d.ts +6 -1
- package/dist/provisioning.js +16 -0
- package/dist/router.d.ts +2 -1
- package/dist/router.js +52 -1
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { resolveUserId, resolveUserProfile, toLiteLLMUserId, getOrProvisionUser,
|
|
|
7
7
|
export type { BackstageUserProfile } from './provisioning';
|
|
8
8
|
export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys, bridgeGenerateKey, } from './bridge';
|
|
9
9
|
export type { BridgeClaims, TokenVerifier, BridgeConfig, KeycloakJWTVerifierOptions, } from './bridge';
|
|
10
|
+
export { litellmPermissions, litellmKeyCreatePermission, litellmKeyRevokePermission, litellmKeyManagePermission, litellmAuditReadPermission, } from './permissions';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
|
|
17
|
+
exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
|
|
18
18
|
var plugin_1 = require("./plugin");
|
|
19
19
|
Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
|
|
20
20
|
var plugin_2 = require("./plugin");
|
|
@@ -43,3 +43,9 @@ Object.defineProperty(exports, "resolveBridgeUserId", { enumerable: true, get: f
|
|
|
43
43
|
Object.defineProperty(exports, "getOrProvisionUserFromClaims", { enumerable: true, get: function () { return bridge_1.getOrProvisionUserFromClaims; } });
|
|
44
44
|
Object.defineProperty(exports, "bridgeListKeys", { enumerable: true, get: function () { return bridge_1.bridgeListKeys; } });
|
|
45
45
|
Object.defineProperty(exports, "bridgeGenerateKey", { enumerable: true, get: function () { return bridge_1.bridgeGenerateKey; } });
|
|
46
|
+
var permissions_1 = require("./permissions");
|
|
47
|
+
Object.defineProperty(exports, "litellmPermissions", { enumerable: true, get: function () { return permissions_1.litellmPermissions; } });
|
|
48
|
+
Object.defineProperty(exports, "litellmKeyCreatePermission", { enumerable: true, get: function () { return permissions_1.litellmKeyCreatePermission; } });
|
|
49
|
+
Object.defineProperty(exports, "litellmKeyRevokePermission", { enumerable: true, get: function () { return permissions_1.litellmKeyRevokePermission; } });
|
|
50
|
+
Object.defineProperty(exports, "litellmKeyManagePermission", { enumerable: true, get: function () { return permissions_1.litellmKeyManagePermission; } });
|
|
51
|
+
Object.defineProperty(exports, "litellmAuditReadPermission", { enumerable: true, get: function () { return permissions_1.litellmAuditReadPermission; } });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const litellmKeyCreatePermission: import("@backstage/plugin-permission-common").BasicPermission;
|
|
2
|
+
export declare const litellmKeyRevokePermission: import("@backstage/plugin-permission-common").BasicPermission;
|
|
3
|
+
export declare const litellmKeyManagePermission: import("@backstage/plugin-permission-common").BasicPermission;
|
|
4
|
+
export declare const litellmAuditReadPermission: import("@backstage/plugin-permission-common").BasicPermission;
|
|
5
|
+
export declare const litellmPermissions: import("@backstage/plugin-permission-common").BasicPermission[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.litellmPermissions = exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = void 0;
|
|
4
|
+
const plugin_permission_common_1 = require("@backstage/plugin-permission-common");
|
|
5
|
+
exports.litellmKeyCreatePermission = (0, plugin_permission_common_1.createPermission)({
|
|
6
|
+
name: 'litellm.key.create',
|
|
7
|
+
attributes: { action: 'create' },
|
|
8
|
+
});
|
|
9
|
+
exports.litellmKeyRevokePermission = (0, plugin_permission_common_1.createPermission)({
|
|
10
|
+
name: 'litellm.key.revoke',
|
|
11
|
+
attributes: { action: 'delete' },
|
|
12
|
+
});
|
|
13
|
+
exports.litellmKeyManagePermission = (0, plugin_permission_common_1.createPermission)({
|
|
14
|
+
name: 'litellm.key.manage',
|
|
15
|
+
attributes: { action: 'update' },
|
|
16
|
+
});
|
|
17
|
+
exports.litellmAuditReadPermission = (0, plugin_permission_common_1.createPermission)({
|
|
18
|
+
name: 'litellm.audit.read',
|
|
19
|
+
attributes: { action: 'read' },
|
|
20
|
+
});
|
|
21
|
+
exports.litellmPermissions = [
|
|
22
|
+
exports.litellmKeyCreatePermission,
|
|
23
|
+
exports.litellmKeyRevokePermission,
|
|
24
|
+
exports.litellmKeyManagePermission,
|
|
25
|
+
exports.litellmAuditReadPermission,
|
|
26
|
+
];
|
package/dist/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.litellmPlugin = void 0;
|
|
|
4
4
|
const backend_plugin_api_1 = require("@backstage/backend-plugin-api");
|
|
5
5
|
const router_1 = require("./router");
|
|
6
6
|
const bridge_1 = require("./bridge");
|
|
7
|
+
const permissions_1 = require("./permissions");
|
|
7
8
|
exports.litellmPlugin = (0, backend_plugin_api_1.createBackendPlugin)({
|
|
8
9
|
pluginId: 'litellm',
|
|
9
10
|
register(reg) {
|
|
@@ -14,9 +15,18 @@ exports.litellmPlugin = (0, backend_plugin_api_1.createBackendPlugin)({
|
|
|
14
15
|
logger: backend_plugin_api_1.coreServices.logger,
|
|
15
16
|
auth: backend_plugin_api_1.coreServices.auth,
|
|
16
17
|
discovery: backend_plugin_api_1.coreServices.discovery,
|
|
18
|
+
permissions: backend_plugin_api_1.coreServices.permissions,
|
|
19
|
+
permissionsRegistry: backend_plugin_api_1.coreServices.permissionsRegistry,
|
|
17
20
|
},
|
|
18
|
-
async init({ httpRouter, config, logger, auth, discovery }) {
|
|
19
|
-
|
|
21
|
+
async init({ httpRouter, config, logger, auth, discovery, permissions, permissionsRegistry, }) {
|
|
22
|
+
permissionsRegistry.addPermissions(permissions_1.litellmPermissions);
|
|
23
|
+
const router = await (0, router_1.createRouter)({
|
|
24
|
+
config,
|
|
25
|
+
logger,
|
|
26
|
+
auth,
|
|
27
|
+
discovery,
|
|
28
|
+
permissions,
|
|
29
|
+
});
|
|
20
30
|
httpRouter.use(router);
|
|
21
31
|
// The CLI bridge verifies raw Keycloak JWTs itself (it does NOT use a
|
|
22
32
|
// Backstage-issued token). Exempt its routes from the default auth
|
package/dist/provisioning.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Config } from '@backstage/config';
|
|
2
|
-
import { AuthService } from '@backstage/backend-plugin-api';
|
|
2
|
+
import { AuthService, BackstageCredentials } from '@backstage/backend-plugin-api';
|
|
3
3
|
import { CatalogClient } from '@backstage/catalog-client';
|
|
4
4
|
import { Request } from 'express';
|
|
5
5
|
import { LiteLLMClient } from './client';
|
|
@@ -45,6 +45,11 @@ export declare function readProvisioningDefaults(config: Config): {
|
|
|
45
45
|
* the request carries no user credential (service-to-service calls).
|
|
46
46
|
*/
|
|
47
47
|
export declare function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Like resolveUserId, but returns the raw BackstageCredentials object
|
|
50
|
+
* (rather than just the entity ref) for passing into PermissionsService.authorize().
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveCredentials(req: Request, auth: AuthService): Promise<BackstageCredentials | undefined>;
|
|
48
53
|
/**
|
|
49
54
|
* Profile data extracted from a Backstage Catalog User entity, used to
|
|
50
55
|
* populate user_email / user_alias on the LiteLLM record.
|
package/dist/provisioning.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.readRoleConfigs = readRoleConfigs;
|
|
|
6
6
|
exports.applyRoleOverrides = applyRoleOverrides;
|
|
7
7
|
exports.readProvisioningDefaults = readProvisioningDefaults;
|
|
8
8
|
exports.resolveUserId = resolveUserId;
|
|
9
|
+
exports.resolveCredentials = resolveCredentials;
|
|
9
10
|
exports.resolveUserProfile = resolveUserProfile;
|
|
10
11
|
exports.provisionUser = provisionUser;
|
|
11
12
|
exports.getOrProvisionUser = getOrProvisionUser;
|
|
@@ -116,6 +117,21 @@ async function resolveUserId(req, auth) {
|
|
|
116
117
|
}
|
|
117
118
|
return undefined;
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Like resolveUserId, but returns the raw BackstageCredentials object
|
|
122
|
+
* (rather than just the entity ref) for passing into PermissionsService.authorize().
|
|
123
|
+
*/
|
|
124
|
+
async function resolveCredentials(req, auth) {
|
|
125
|
+
const rawToken = req.headers.authorization?.slice(7);
|
|
126
|
+
if (!rawToken)
|
|
127
|
+
return undefined;
|
|
128
|
+
try {
|
|
129
|
+
return await auth.authenticate(rawToken);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
119
135
|
/**
|
|
120
136
|
* Looks up the catalog User entity for the authenticated user and returns
|
|
121
137
|
* the profile block. Returns an empty object when the user has no catalog
|
package/dist/router.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { Config } from '@backstage/config';
|
|
3
|
-
import { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';
|
|
3
|
+
import { AuthService, DiscoveryService, PermissionsService } from '@backstage/backend-plugin-api';
|
|
4
4
|
import { LiteLLMClient } from './client';
|
|
5
5
|
import { ProvisioningError } from './provisioning';
|
|
6
6
|
import { TokenVerifier } from './bridge';
|
|
@@ -10,6 +10,7 @@ export interface RouterOptions {
|
|
|
10
10
|
logger: any;
|
|
11
11
|
auth: AuthService;
|
|
12
12
|
discovery: DiscoveryService;
|
|
13
|
+
permissions: PermissionsService;
|
|
13
14
|
/** Override the LiteLLM client (tests). Defaults to one built from config. */
|
|
14
15
|
client?: LiteLLMClient;
|
|
15
16
|
/** Override the bridge token verifier (tests). Defaults to a Keycloak JWKS verifier. */
|
package/dist/router.js
CHANGED
|
@@ -36,14 +36,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ProvisioningError = void 0;
|
|
37
37
|
exports.createRouter = createRouter;
|
|
38
38
|
const express_1 = __importStar(require("express"));
|
|
39
|
+
const plugin_permission_common_1 = require("@backstage/plugin-permission-common");
|
|
39
40
|
const catalog_client_1 = require("@backstage/catalog-client");
|
|
40
41
|
const client_1 = require("./client");
|
|
41
42
|
const openapi_1 = require("./openapi");
|
|
42
43
|
const provisioning_1 = require("./provisioning");
|
|
43
44
|
Object.defineProperty(exports, "ProvisioningError", { enumerable: true, get: function () { return provisioning_1.ProvisioningError; } });
|
|
44
45
|
const bridge_1 = require("./bridge");
|
|
46
|
+
const permissions_1 = require("./permissions");
|
|
45
47
|
async function createRouter(options) {
|
|
46
|
-
const { config, logger, auth, discovery } = options;
|
|
48
|
+
const { config, logger, auth, discovery, permissions } = options;
|
|
47
49
|
const baseUrl = config.getString('litellm.baseUrl');
|
|
48
50
|
const masterKey = config.getString('litellm.masterKey');
|
|
49
51
|
const userIdDomain = config.getOptionalString('litellm.userIdDomain');
|
|
@@ -212,6 +214,27 @@ async function createRouter(options) {
|
|
|
212
214
|
}
|
|
213
215
|
return false;
|
|
214
216
|
}
|
|
217
|
+
// ── Permission-framework guard ───────────────────────────────────────────
|
|
218
|
+
//
|
|
219
|
+
// Coarse-grained "is this identity allowed to do this at all" check, layered
|
|
220
|
+
// on top of (not replacing) the ownership guard above. With no permission
|
|
221
|
+
// policy installed (the default for a fresh Backstage instance) this always
|
|
222
|
+
// resolves ALLOW, so behavior is unchanged until an operator wires up
|
|
223
|
+
// @backstage-community/plugin-rbac or a custom PermissionPolicy.
|
|
224
|
+
async function assertPermission(req, permission) {
|
|
225
|
+
const credentials = await (0, provisioning_1.resolveCredentials)(req, auth);
|
|
226
|
+
if (!credentials)
|
|
227
|
+
return false;
|
|
228
|
+
const [decision] = await permissions.authorize([{ permission }], {
|
|
229
|
+
credentials,
|
|
230
|
+
});
|
|
231
|
+
return decision.result === plugin_permission_common_1.AuthorizeResult.ALLOW;
|
|
232
|
+
}
|
|
233
|
+
function sendPermissionDenied(res, permission) {
|
|
234
|
+
res.status(403).json({
|
|
235
|
+
error: `Access denied: missing permission "${permission.name}"`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
215
238
|
router.post('/keys/generate', async (req, res) => {
|
|
216
239
|
try {
|
|
217
240
|
// Only alias is hard-required. max_budget is optional: a positive
|
|
@@ -236,6 +259,10 @@ async function createRouter(options) {
|
|
|
236
259
|
});
|
|
237
260
|
return;
|
|
238
261
|
}
|
|
262
|
+
if (!(await assertPermission(req, permissions_1.litellmKeyCreatePermission))) {
|
|
263
|
+
sendPermissionDenied(res, permissions_1.litellmKeyCreatePermission);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
239
266
|
const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
|
|
240
267
|
const resolvedUserId = tokenEntityRef
|
|
241
268
|
? (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain)
|
|
@@ -314,6 +341,10 @@ async function createRouter(options) {
|
|
|
314
341
|
res.status(400).json({ error: 'keyId is required' });
|
|
315
342
|
return;
|
|
316
343
|
}
|
|
344
|
+
if (!(await assertPermission(req, permissions_1.litellmKeyManagePermission))) {
|
|
345
|
+
sendPermissionDenied(res, permissions_1.litellmKeyManagePermission);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
317
348
|
const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
|
|
318
349
|
const request = { ...req.body, key: keyId };
|
|
319
350
|
const result = await client.updateKey(request);
|
|
@@ -334,6 +365,10 @@ async function createRouter(options) {
|
|
|
334
365
|
res.status(400).json({ error: 'keyId is required' });
|
|
335
366
|
return;
|
|
336
367
|
}
|
|
368
|
+
if (!(await assertPermission(req, permissions_1.litellmKeyRevokePermission))) {
|
|
369
|
+
sendPermissionDenied(res, permissions_1.litellmKeyRevokePermission);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
337
372
|
const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
|
|
338
373
|
await client.deleteKeys({ keys: [keyId] });
|
|
339
374
|
logger.info({ action: 'key.delete', userId: tokenEntityRef ?? 'unknown', keyId });
|
|
@@ -349,6 +384,10 @@ async function createRouter(options) {
|
|
|
349
384
|
router.post('/keys/:keyId/block', async (req, res) => {
|
|
350
385
|
try {
|
|
351
386
|
const { keyId } = req.params;
|
|
387
|
+
if (!(await assertPermission(req, permissions_1.litellmKeyManagePermission))) {
|
|
388
|
+
sendPermissionDenied(res, permissions_1.litellmKeyManagePermission);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
352
391
|
const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
|
|
353
392
|
await client.blockKey(keyId);
|
|
354
393
|
logger.info({ action: 'key.block', userId: tokenEntityRef ?? 'unknown', keyId });
|
|
@@ -364,6 +403,10 @@ async function createRouter(options) {
|
|
|
364
403
|
router.post('/keys/:keyId/unblock', async (req, res) => {
|
|
365
404
|
try {
|
|
366
405
|
const { keyId } = req.params;
|
|
406
|
+
if (!(await assertPermission(req, permissions_1.litellmKeyManagePermission))) {
|
|
407
|
+
sendPermissionDenied(res, permissions_1.litellmKeyManagePermission);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
367
410
|
const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
|
|
368
411
|
await client.unblockKey(keyId);
|
|
369
412
|
logger.info({ action: 'key.unblock', userId: tokenEntityRef ?? 'unknown', keyId });
|
|
@@ -379,6 +422,10 @@ async function createRouter(options) {
|
|
|
379
422
|
router.post('/keys/:keyId/reset_spend', async (req, res) => {
|
|
380
423
|
try {
|
|
381
424
|
const { keyId } = req.params;
|
|
425
|
+
if (!(await assertPermission(req, permissions_1.litellmKeyManagePermission))) {
|
|
426
|
+
sendPermissionDenied(res, permissions_1.litellmKeyManagePermission);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
382
429
|
const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
|
|
383
430
|
await client.resetKeySpend(keyId);
|
|
384
431
|
logger.info({ action: 'key.reset_spend', userId: tokenEntityRef ?? 'unknown', keyId });
|
|
@@ -406,6 +453,10 @@ async function createRouter(options) {
|
|
|
406
453
|
res.status(403).json({ error: 'Access denied: not a member of the audit group' });
|
|
407
454
|
return;
|
|
408
455
|
}
|
|
456
|
+
if (!(await assertPermission(req, permissions_1.litellmAuditReadPermission))) {
|
|
457
|
+
sendPermissionDenied(res, permissions_1.litellmAuditReadPermission);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
409
460
|
try {
|
|
410
461
|
const { page, page_size, start_date, end_date, action, table_name, changed_by } = req.query;
|
|
411
462
|
const result = await client.getAuditLogs({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acarmisc/backstage-plugin-litellm-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The Backstage backend plugin for LiteLLM governance",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"build": "node build.js && tsc -p tsconfig.json",
|
|
32
32
|
"lint": "eslint src --ext .ts",
|
|
33
33
|
"test": "tsc -p tsconfig.test.json && node --test dist-test/**/*.test.js dist-test/*.test.js",
|
|
34
|
+
"api-report": "npm run build && api-extractor run --local --verbose",
|
|
34
35
|
"prepack": "npm run build",
|
|
35
36
|
"postpack": ""
|
|
36
37
|
},
|
|
@@ -39,16 +40,18 @@
|
|
|
39
40
|
"@backstage/catalog-client": "^1.9.0",
|
|
40
41
|
"@backstage/catalog-model": "^1.7.0",
|
|
41
42
|
"@backstage/config": "^1.3.0",
|
|
43
|
+
"@backstage/plugin-permission-common": "^0.9.10",
|
|
42
44
|
"@backstage/types": "^1.2.0",
|
|
43
45
|
"express": "^4.18.2",
|
|
44
46
|
"jose": "^5.9.6"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@backstage/cli": "^0.36.2",
|
|
50
|
+
"@microsoft/api-extractor": "^7.59.0",
|
|
48
51
|
"@types/express": "^4.17.25",
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
49
53
|
"esbuild": "^0.28.0",
|
|
50
|
-
"typescript": "^5.9.3"
|
|
51
|
-
"@types/node": "^20.0.0"
|
|
54
|
+
"typescript": "^5.9.3"
|
|
52
55
|
},
|
|
53
56
|
"configSchema": "config.d.ts"
|
|
54
57
|
}
|