@karimov-labs/backstage-plugin-devxp-backend 1.0.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/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @karimov-labs/backstage-plugin-devxp-backend
2
+
3
+ Backstage backend plugin that powers the **Developer Intelligence** dashboard. It stores masked developer identity mappings, exposes REST endpoints for hashing/unmasking, and accepts bulk CSV uploads — all backed by the Backstage-managed database (SQLite or PostgreSQL).
4
+
5
+ This plugin is the backend counterpart to [`@karimov-labs/backstage-plugin-devxp`](https://www.npmjs.com/package/@karimov-labs/backstage-plugin-devxp).
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - Persists `masked_name ↔ real_name` pairs in the Backstage database using Knex migrations
12
+ - SHA-256 hashing algorithm compatible with [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer): `SHA-256(salt + realName)`, truncated to 16 hex characters
13
+ - REST API for hashing, unmasking, bulk CSV upload, and listing/deleting mappings
14
+ - Reads salt and API credentials from `app-config.yaml` (never exposes them to the frontend)
15
+ - Runs unauthenticated (access policy: `allow: 'unauthenticated'`) so the frontend can reach it without an extra auth token
16
+
17
+ ---
18
+
19
+ ## Requirements
20
+
21
+ | Dependency | Version |
22
+ |---|---|
23
+ | Backstage | >= 1.30 |
24
+ | Node.js | >= 18 |
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ### 1. Install the package
31
+
32
+ ```bash
33
+ # yarn (Backstage default)
34
+ yarn workspace backend add @karimov-labs/backstage-plugin-devxp-backend
35
+
36
+ # npm
37
+ npm install @karimov-labs/backstage-plugin-devxp-backend
38
+ ```
39
+
40
+ ---
41
+
42
+ ### 2. Register the plugin
43
+
44
+ Edit `packages/backend/src/index.ts`:
45
+
46
+ ```ts
47
+ import { createBackend } from '@backstage/backend-defaults';
48
+
49
+ const backend = createBackend();
50
+
51
+ // ... other plugins
52
+
53
+ backend.add(import('@karimov-labs/backstage-plugin-devxp-backend'));
54
+
55
+ backend.start();
56
+ ```
57
+
58
+ ---
59
+
60
+ ### 3. Configure app-config.yaml
61
+
62
+ ```yaml
63
+ devxp:
64
+ # Salt used for SHA-256 hashing — must match the salt used in dev-xp-analyzer
65
+ salt: ${DEVXP_SALT}
66
+
67
+ # Whether developer names are masked in the analytics tool
68
+ masked: true
69
+
70
+ # dev-xp-analyzer API credentials (optional)
71
+ apiToken: ${DEVXP_API_TOKEN}
72
+ apiEndpoint: ${DEVXP_API_ENDPOINT}
73
+ projectId: ${DEVXP_PROJECT_ID}
74
+ ```
75
+
76
+ Set the corresponding environment variables before starting Backstage:
77
+
78
+ ```bash
79
+ export DEVXP_SALT="your-secret-salt"
80
+ export DEVXP_API_TOKEN="your-api-token" # optional
81
+ export DEVXP_API_ENDPOINT="https://..." # optional
82
+ export DEVXP_PROJECT_ID="your-project-id" # optional
83
+ ```
84
+
85
+ > **Security note:** `salt` and `apiToken` are read only on the backend and are never serialised into any response.
86
+
87
+ ---
88
+
89
+ ### 4. Database
90
+
91
+ The plugin uses the standard Backstage database service. It automatically creates the `devxp_developer_mappings` table on startup — no manual migration is needed.
92
+
93
+ ```
94
+ devxp_developer_mappings
95
+ ├── id INTEGER PRIMARY KEY AUTOINCREMENT
96
+ ├── masked_name TEXT UNIQUE NOT NULL
97
+ ├── real_name TEXT NOT NULL
98
+ └── created_at DATETIME DEFAULT CURRENT_TIMESTAMP
99
+ ```
100
+
101
+ Works with both **SQLite** (development) and **PostgreSQL** (production).
102
+
103
+ ---
104
+
105
+ ## REST API
106
+
107
+ All endpoints are mounted at `/api/devxp/`.
108
+
109
+ | Method | Path | Description |
110
+ |---|---|---|
111
+ | `GET` | `/health` | Health check — returns `{ status: 'ok' }` |
112
+ | `GET` | `/config` | Non-sensitive configuration status (booleans — never exposes secret values) |
113
+ | `GET` | `/mappings` | List all stored `masked_name ↔ real_name` pairs |
114
+ | `POST` | `/mappings/upload` | Upload CSV text; hashes each line and upserts mappings |
115
+ | `POST` | `/mappings/delete` | Delete a single mapping by `maskedName` |
116
+ | `POST` | `/unmask` | Look up a real name from a masked name |
117
+ | `POST` | `/hash` | Compute the masked hash for a given real name |
118
+
119
+ ### Example: upload CSV
120
+
121
+ ```bash
122
+ curl -X POST http://localhost:7007/api/devxp/mappings/upload \
123
+ -H 'Content-Type: application/json' \
124
+ -d '{"csvContent": "Alice Smith\nBob Jones\nCarol White"}'
125
+ ```
126
+
127
+ ### Example: unmask
128
+
129
+ ```bash
130
+ curl -X POST http://localhost:7007/api/devxp/mappings/unmask \
131
+ -H 'Content-Type: application/json' \
132
+ -d '{"maskedName": "a3f2c1d4e5b67890"}'
133
+ # → { "maskedName": "a3f2c1d4e5b67890", "realName": "Alice Smith" }
134
+ ```
135
+
136
+ ### Example: hash
137
+
138
+ ```bash
139
+ curl -X POST http://localhost:7007/api/devxp/hash \
140
+ -H 'Content-Type: application/json' \
141
+ -d '{"realName": "Alice Smith"}'
142
+ # → { "realName": "Alice Smith", "maskedName": "a3f2c1d4e5b67890" }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Hashing algorithm
148
+
149
+ ```ts
150
+ import { createHash } from 'crypto';
151
+
152
+ function hashUsername(salt: string, username: string): string {
153
+ return createHash('sha256')
154
+ .update(salt + username)
155
+ .digest('hex')
156
+ .substring(0, 16);
157
+ }
158
+ ```
159
+
160
+ This matches the algorithm used in [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer). Make sure the same `salt` value is configured in both systems.
161
+
162
+ ---
163
+
164
+ ## License
165
+
166
+ Apache-2.0
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var plugin = require('./plugin.cjs.js');
6
+
7
+
8
+
9
+ exports.default = plugin.devxpPlugin;
10
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,5 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+
3
+ declare const devxpPlugin: _backstage_backend_plugin_api.BackendFeature;
4
+
5
+ export { devxpPlugin as default };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var router = require('./router.cjs.js');
5
+
6
+ const devxpPlugin = backendPluginApi.createBackendPlugin({
7
+ pluginId: "devxp",
8
+ register(env) {
9
+ env.registerInit({
10
+ deps: {
11
+ httpRouter: backendPluginApi.coreServices.httpRouter,
12
+ config: backendPluginApi.coreServices.rootConfig,
13
+ database: backendPluginApi.coreServices.database,
14
+ logger: backendPluginApi.coreServices.logger
15
+ },
16
+ async init({ httpRouter, config, database, logger }) {
17
+ const router$1 = await router.createRouter({ config, database, logger });
18
+ httpRouter.use(router$1);
19
+ httpRouter.addAuthPolicy({
20
+ path: "/",
21
+ allow: "unauthenticated"
22
+ });
23
+ }
24
+ });
25
+ }
26
+ });
27
+
28
+ exports.devxpPlugin = devxpPlugin;
29
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n createBackendPlugin,\n coreServices,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const devxpPlugin = createBackendPlugin({\n pluginId: 'devxp',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n database: coreServices.database,\n logger: coreServices.logger,\n },\n async init({ httpRouter, config, database, logger }) {\n const router = await createRouter({ config, database, logger });\n httpRouter.use(router as any);\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","router","createRouter"],"mappings":";;;;;AAMO,MAAM,cAAcA,oCAAA,CAAoB;AAAA,EAC7C,QAAA,EAAU,OAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,QAAQA,6BAAA,CAAa;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,YAAY,MAAA,EAAQ,QAAA,EAAU,QAAO,EAAG;AACnD,QAAA,MAAMC,WAAS,MAAMC,mBAAA,CAAa,EAAE,MAAA,EAAQ,QAAA,EAAU,QAAQ,CAAA;AAC9D,QAAA,UAAA,CAAW,IAAID,QAAa,CAAA;AAC5B,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var express = require('express');
5
+ var MappingStore = require('./service/MappingStore.cjs.js');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
10
+
11
+ function hashUsername(salt, username) {
12
+ const hash = crypto.createHash("sha256").update(`${salt}${username}`).digest("hex");
13
+ return hash.substring(0, 16);
14
+ }
15
+ async function createRouter(options) {
16
+ const { config, database, logger } = options;
17
+ const salt = config.getOptionalString("devxp.salt") ?? "";
18
+ const masked = config.getOptionalBoolean("devxp.masked") ?? true;
19
+ const apiEndpoint = config.getOptionalString("devxp.apiEndpoint") ?? "";
20
+ const apiToken = config.getOptionalString("devxp.apiToken") ?? "";
21
+ const projectId = config.getOptionalString("devxp.projectId") ?? "";
22
+ const knex = await database.getClient();
23
+ const store = new MappingStore.MappingStore(knex);
24
+ await store.initialize();
25
+ logger.info("DevXP backend plugin initialized");
26
+ const router = express.Router();
27
+ router.use(express__default.default.json({ limit: "10mb" }));
28
+ router.get("/health", async (_req, res) => {
29
+ res.json({ status: "ok" });
30
+ });
31
+ router.get("/config", async (_req, res) => {
32
+ const mappingCount = await store.count();
33
+ res.json({
34
+ masked,
35
+ saltConfigured: salt.length > 0,
36
+ apiEndpointConfigured: apiEndpoint.length > 0,
37
+ apiTokenConfigured: apiToken.length > 0,
38
+ projectIdConfigured: projectId.length > 0,
39
+ mappingCount
40
+ });
41
+ });
42
+ router.get("/mappings", async (_req, res) => {
43
+ const mappings = await store.getAll();
44
+ res.json({ mappings });
45
+ });
46
+ router.post("/mappings/upload", async (req, res) => {
47
+ const { csvContent } = req.body;
48
+ if (!csvContent || typeof csvContent !== "string") {
49
+ res.status(400).json({ error: "csvContent is required as a string" });
50
+ return;
51
+ }
52
+ if (!salt) {
53
+ res.status(400).json({ error: "Salt is not configured. Set devxp.salt in app-config.yaml" });
54
+ return;
55
+ }
56
+ const lines = csvContent.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
57
+ const startIndex = lines.length > 0 && lines[0].toLowerCase() === "name" ? 1 : 0;
58
+ const mappings = [];
59
+ for (let i = startIndex; i < lines.length; i++) {
60
+ const realName = lines[i];
61
+ const maskedName = hashUsername(salt, realName);
62
+ mappings.push({ maskedName, realName });
63
+ }
64
+ const count = await store.upsertBatch(mappings);
65
+ logger.info(`DevXP: Uploaded ${count} developer mappings`);
66
+ res.json({
67
+ message: `Successfully processed ${count} developer name mappings`,
68
+ count
69
+ });
70
+ });
71
+ router.post("/mappings/delete", async (req, res) => {
72
+ const { maskedName } = req.body;
73
+ if (!maskedName || typeof maskedName !== "string") {
74
+ res.status(400).json({ error: "maskedName is required" });
75
+ return;
76
+ }
77
+ const deleted = await store.deleteByMaskedName(maskedName);
78
+ if (deleted) {
79
+ res.json({ message: "Mapping deleted" });
80
+ } else {
81
+ res.status(404).json({ error: "Mapping not found" });
82
+ }
83
+ });
84
+ router.post("/unmask", async (req, res) => {
85
+ const { maskedName } = req.body;
86
+ if (!maskedName || typeof maskedName !== "string") {
87
+ res.status(400).json({ error: "maskedName is required" });
88
+ return;
89
+ }
90
+ const mapping = await store.getByMaskedName(maskedName);
91
+ if (mapping) {
92
+ res.json({ maskedName, realName: mapping.real_name });
93
+ } else {
94
+ res.json({ maskedName, realName: null, message: "No mapping found for this masked name" });
95
+ }
96
+ });
97
+ router.post("/hash", async (req, res) => {
98
+ const { realName } = req.body;
99
+ if (!realName || typeof realName !== "string") {
100
+ res.status(400).json({ error: "realName is required" });
101
+ return;
102
+ }
103
+ if (!salt) {
104
+ res.status(400).json({ error: "Salt is not configured. Set devxp.salt in app-config.yaml" });
105
+ return;
106
+ }
107
+ const maskedName = hashUsername(salt, realName);
108
+ res.json({ realName, maskedName });
109
+ });
110
+ return router;
111
+ }
112
+
113
+ exports.createRouter = createRouter;
114
+ //# sourceMappingURL=router.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import { createHash } from 'crypto';\nimport { Router } from 'express';\nimport express from 'express';\nimport { Config } from '@backstage/config';\nimport { LoggerService, DatabaseService } from '@backstage/backend-plugin-api';\nimport { MappingStore } from './service/MappingStore';\n\n/**\n * Hash a username using the same algorithm as dev-xp-analyzer:\n * SHA-256 of (salt + username), truncated to first 16 hex chars.\n */\nfunction hashUsername(salt: string, username: string): string {\n const hash = createHash('sha256')\n .update(`${salt}${username}`)\n .digest('hex');\n return hash.substring(0, 16);\n}\n\ninterface RouterOptions {\n config: Config;\n database: DatabaseService;\n logger: LoggerService;\n}\n\nexport async function createRouter(options: RouterOptions): Promise<express.Router> {\n const { config, database, logger } = options;\n\n const salt = config.getOptionalString('devxp.salt') ?? '';\n const masked = config.getOptionalBoolean('devxp.masked') ?? true;\n const apiEndpoint = config.getOptionalString('devxp.apiEndpoint') ?? '';\n const apiToken = config.getOptionalString('devxp.apiToken') ?? '';\n const projectId = config.getOptionalString('devxp.projectId') ?? '';\n\n const knex = await database.getClient();\n const store = new MappingStore(knex);\n await store.initialize();\n\n logger.info('DevXP backend plugin initialized');\n\n const router = Router();\n router.use(express.json({ limit: '10mb' }));\n\n // Health check\n router.get('/health', async (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n // Return non-sensitive configuration status\n router.get('/config', async (_req, res) => {\n const mappingCount = await store.count();\n res.json({\n masked,\n saltConfigured: salt.length > 0,\n apiEndpointConfigured: apiEndpoint.length > 0,\n apiTokenConfigured: apiToken.length > 0,\n projectIdConfigured: projectId.length > 0,\n mappingCount,\n });\n });\n\n // List all developer mappings\n router.get('/mappings', async (_req, res) => {\n const mappings = await store.getAll();\n res.json({ mappings });\n });\n\n // Upload CSV content with developer names, compute hashes, store mappings\n router.post('/mappings/upload', async (req, res) => {\n const { csvContent } = req.body;\n if (!csvContent || typeof csvContent !== 'string') {\n res.status(400).json({ error: 'csvContent is required as a string' });\n return;\n }\n\n if (!salt) {\n res.status(400).json({ error: 'Salt is not configured. Set devxp.salt in app-config.yaml' });\n return;\n }\n\n const lines = csvContent\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(line => line.length > 0);\n\n // Skip header row if it looks like one\n const startIndex = lines.length > 0 && lines[0].toLowerCase() === 'name' ? 1 : 0;\n\n const mappings: { maskedName: string; realName: string }[] = [];\n for (let i = startIndex; i < lines.length; i++) {\n const realName = lines[i];\n const maskedName = hashUsername(salt, realName);\n mappings.push({ maskedName, realName });\n }\n\n const count = await store.upsertBatch(mappings);\n logger.info(`DevXP: Uploaded ${count} developer mappings`);\n\n res.json({\n message: `Successfully processed ${count} developer name mappings`,\n count,\n });\n });\n\n // Delete a mapping by masked name\n router.post('/mappings/delete', async (req, res) => {\n const { maskedName } = req.body;\n if (!maskedName || typeof maskedName !== 'string') {\n res.status(400).json({ error: 'maskedName is required' });\n return;\n }\n\n const deleted = await store.deleteByMaskedName(maskedName);\n if (deleted) {\n res.json({ message: 'Mapping deleted' });\n } else {\n res.status(404).json({ error: 'Mapping not found' });\n }\n });\n\n // Unmask: given a masked name, return the real name\n router.post('/unmask', async (req, res) => {\n const { maskedName } = req.body;\n if (!maskedName || typeof maskedName !== 'string') {\n res.status(400).json({ error: 'maskedName is required' });\n return;\n }\n\n const mapping = await store.getByMaskedName(maskedName);\n if (mapping) {\n res.json({ maskedName, realName: mapping.real_name });\n } else {\n res.json({ maskedName, realName: null, message: 'No mapping found for this masked name' });\n }\n });\n\n // Hash: given a real name, compute the masked hash\n router.post('/hash', async (req, res) => {\n const { realName } = req.body;\n if (!realName || typeof realName !== 'string') {\n res.status(400).json({ error: 'realName is required' });\n return;\n }\n\n if (!salt) {\n res.status(400).json({ error: 'Salt is not configured. Set devxp.salt in app-config.yaml' });\n return;\n }\n\n const maskedName = hashUsername(salt, realName);\n res.json({ realName, maskedName });\n });\n\n return router;\n}\n"],"names":["createHash","MappingStore","Router","express"],"mappings":";;;;;;;;;;AAWA,SAAS,YAAA,CAAa,MAAc,QAAA,EAA0B;AAC5D,EAAA,MAAM,IAAA,GAAOA,iBAAA,CAAW,QAAQ,CAAA,CAC7B,MAAA,CAAO,CAAA,EAAG,IAAI,CAAA,EAAG,QAAQ,CAAA,CAAE,CAAA,CAC3B,MAAA,CAAO,KAAK,CAAA;AACf,EAAA,OAAO,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,EAAE,CAAA;AAC7B;AAQA,eAAsB,aAAa,OAAA,EAAiD;AAClF,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAO,GAAI,OAAA;AAErC,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,iBAAA,CAAkB,YAAY,CAAA,IAAK,EAAA;AACvD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,kBAAA,CAAmB,cAAc,CAAA,IAAK,IAAA;AAC5D,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,iBAAA,CAAkB,mBAAmB,CAAA,IAAK,EAAA;AACrE,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,iBAAA,CAAkB,gBAAgB,CAAA,IAAK,EAAA;AAC/D,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,iBAAA,CAAkB,iBAAiB,CAAA,IAAK,EAAA;AAEjE,EAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,SAAA,EAAU;AACtC,EAAA,MAAM,KAAA,GAAQ,IAAIC,yBAAA,CAAa,IAAI,CAAA;AACnC,EAAA,MAAM,MAAM,UAAA,EAAW;AAEvB,EAAA,MAAA,CAAO,KAAK,kCAAkC,CAAA;AAE9C,EAAA,MAAM,SAASC,cAAA,EAAO;AACtB,EAAA,MAAA,CAAO,IAAIC,wBAAA,CAAQ,IAAA,CAAK,EAAE,KAAA,EAAO,MAAA,EAAQ,CAAC,CAAA;AAG1C,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,OAAO,IAAA,EAAM,GAAA,KAAQ;AACzC,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAC3B,CAAC,CAAA;AAGD,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,OAAO,IAAA,EAAM,GAAA,KAAQ;AACzC,IAAA,MAAM,YAAA,GAAe,MAAM,KAAA,CAAM,KAAA,EAAM;AACvC,IAAA,GAAA,CAAI,IAAA,CAAK;AAAA,MACP,MAAA;AAAA,MACA,cAAA,EAAgB,KAAK,MAAA,GAAS,CAAA;AAAA,MAC9B,qBAAA,EAAuB,YAAY,MAAA,GAAS,CAAA;AAAA,MAC5C,kBAAA,EAAoB,SAAS,MAAA,GAAS,CAAA;AAAA,MACtC,mBAAA,EAAqB,UAAU,MAAA,GAAS,CAAA;AAAA,MACxC;AAAA,KACD,CAAA;AAAA,EACH,CAAC,CAAA;AAGD,EAAA,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,OAAO,IAAA,EAAM,GAAA,KAAQ;AAC3C,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,MAAA,EAAO;AACpC,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,QAAA,EAAU,CAAA;AAAA,EACvB,CAAC,CAAA;AAGD,EAAA,MAAA,CAAO,IAAA,CAAK,kBAAA,EAAoB,OAAO,GAAA,EAAK,GAAA,KAAQ;AAClD,IAAA,MAAM,EAAE,UAAA,EAAW,GAAI,GAAA,CAAI,IAAA;AAC3B,IAAA,IAAI,CAAC,UAAA,IAAc,OAAO,UAAA,KAAe,QAAA,EAAU;AACjD,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,sCAAsC,CAAA;AACpE,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,6DAA6D,CAAA;AAC3F,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,KAAA,GAAQ,UAAA,CACX,KAAA,CAAM,OAAO,EACb,GAAA,CAAI,CAAA,IAAA,KAAQ,IAAA,CAAK,IAAA,EAAM,CAAA,CACvB,MAAA,CAAO,CAAA,IAAA,KAAQ,IAAA,CAAK,SAAS,CAAC,CAAA;AAGjC,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,KAAA,CAAM,CAAC,CAAA,CAAE,WAAA,EAAY,KAAM,MAAA,GAAS,CAAA,GAAI,CAAA;AAE/E,IAAA,MAAM,WAAuD,EAAC;AAC9D,IAAA,KAAA,IAAS,CAAA,GAAI,UAAA,EAAY,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AAC9C,MAAA,MAAM,QAAA,GAAW,MAAM,CAAC,CAAA;AACxB,MAAA,MAAM,UAAA,GAAa,YAAA,CAAa,IAAA,EAAM,QAAQ,CAAA;AAC9C,MAAA,QAAA,CAAS,IAAA,CAAK,EAAE,UAAA,EAAY,QAAA,EAAU,CAAA;AAAA,IACxC;AAEA,IAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,WAAA,CAAY,QAAQ,CAAA;AAC9C,IAAA,MAAA,CAAO,IAAA,CAAK,CAAA,gBAAA,EAAmB,KAAK,CAAA,mBAAA,CAAqB,CAAA;AAEzD,IAAA,GAAA,CAAI,IAAA,CAAK;AAAA,MACP,OAAA,EAAS,0BAA0B,KAAK,CAAA,wBAAA,CAAA;AAAA,MACxC;AAAA,KACD,CAAA;AAAA,EACH,CAAC,CAAA;AAGD,EAAA,MAAA,CAAO,IAAA,CAAK,kBAAA,EAAoB,OAAO,GAAA,EAAK,GAAA,KAAQ;AAClD,IAAA,MAAM,EAAE,UAAA,EAAW,GAAI,GAAA,CAAI,IAAA;AAC3B,IAAA,IAAI,CAAC,UAAA,IAAc,OAAO,UAAA,KAAe,QAAA,EAAU;AACjD,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,0BAA0B,CAAA;AACxD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,kBAAA,CAAmB,UAAU,CAAA;AACzD,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,OAAA,EAAS,iBAAA,EAAmB,CAAA;AAAA,IACzC,CAAA,MAAO;AACL,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,qBAAqB,CAAA;AAAA,IACrD;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,MAAA,CAAO,IAAA,CAAK,SAAA,EAAW,OAAO,GAAA,EAAK,GAAA,KAAQ;AACzC,IAAA,MAAM,EAAE,UAAA,EAAW,GAAI,GAAA,CAAI,IAAA;AAC3B,IAAA,IAAI,CAAC,UAAA,IAAc,OAAO,UAAA,KAAe,QAAA,EAAU;AACjD,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,0BAA0B,CAAA;AACxD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,eAAA,CAAgB,UAAU,CAAA;AACtD,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,GAAA,CAAI,KAAK,EAAE,UAAA,EAAY,QAAA,EAAU,OAAA,CAAQ,WAAW,CAAA;AAAA,IACtD,CAAA,MAAO;AACL,MAAA,GAAA,CAAI,KAAK,EAAE,UAAA,EAAY,UAAU,IAAA,EAAM,OAAA,EAAS,yCAAyC,CAAA;AAAA,IAC3F;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,OAAO,GAAA,EAAK,GAAA,KAAQ;AACvC,IAAA,MAAM,EAAE,QAAA,EAAS,GAAI,GAAA,CAAI,IAAA;AACzB,IAAA,IAAI,CAAC,QAAA,IAAY,OAAO,QAAA,KAAa,QAAA,EAAU;AAC7C,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,wBAAwB,CAAA;AACtD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,6DAA6D,CAAA;AAC3F,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,UAAA,GAAa,YAAA,CAAa,IAAA,EAAM,QAAQ,CAAA;AAC9C,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,QAAA,EAAU,UAAA,EAAY,CAAA;AAAA,EACnC,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ class MappingStore {
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ async initialize() {
8
+ if (!await this.db.schema.hasTable("devxp_developer_mappings")) {
9
+ await this.db.schema.createTable("devxp_developer_mappings", (table) => {
10
+ table.increments("id").primary();
11
+ table.string("masked_name", 255).notNullable().unique();
12
+ table.string("real_name", 255).notNullable();
13
+ table.timestamp("created_at").defaultTo(this.db.fn.now());
14
+ });
15
+ }
16
+ }
17
+ async getAll() {
18
+ return this.db("devxp_developer_mappings").select("*").orderBy("created_at", "desc");
19
+ }
20
+ async getByMaskedName(maskedName) {
21
+ return this.db("devxp_developer_mappings").where("masked_name", maskedName).first();
22
+ }
23
+ async upsert(maskedName, realName) {
24
+ const existing = await this.getByMaskedName(maskedName);
25
+ if (existing) {
26
+ await this.db("devxp_developer_mappings").where("masked_name", maskedName).update({ real_name: realName });
27
+ } else {
28
+ await this.db("devxp_developer_mappings").insert({
29
+ masked_name: maskedName,
30
+ real_name: realName
31
+ });
32
+ }
33
+ }
34
+ async upsertBatch(mappings) {
35
+ let count = 0;
36
+ for (const mapping of mappings) {
37
+ await this.upsert(mapping.maskedName, mapping.realName);
38
+ count++;
39
+ }
40
+ return count;
41
+ }
42
+ async deleteByMaskedName(maskedName) {
43
+ const deleted = await this.db("devxp_developer_mappings").where("masked_name", maskedName).delete();
44
+ return deleted > 0;
45
+ }
46
+ async count() {
47
+ const result = await this.db("devxp_developer_mappings").count("id as count").first();
48
+ return Number(result?.count ?? 0);
49
+ }
50
+ }
51
+
52
+ exports.MappingStore = MappingStore;
53
+ //# sourceMappingURL=MappingStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MappingStore.cjs.js","sources":["../../src/service/MappingStore.ts"],"sourcesContent":["import { Knex } from 'knex';\n\nexport interface DeveloperMapping {\n id: number;\n masked_name: string;\n real_name: string;\n created_at: string;\n}\n\nexport class MappingStore {\n constructor(private readonly db: Knex) {}\n\n async initialize(): Promise<void> {\n if (!(await this.db.schema.hasTable('devxp_developer_mappings'))) {\n await this.db.schema.createTable('devxp_developer_mappings', table => {\n table.increments('id').primary();\n table.string('masked_name', 255).notNullable().unique();\n table.string('real_name', 255).notNullable();\n table.timestamp('created_at').defaultTo(this.db.fn.now());\n });\n }\n }\n\n async getAll(): Promise<DeveloperMapping[]> {\n return this.db<DeveloperMapping>('devxp_developer_mappings')\n .select('*')\n .orderBy('created_at', 'desc');\n }\n\n async getByMaskedName(maskedName: string): Promise<DeveloperMapping | undefined> {\n return this.db<DeveloperMapping>('devxp_developer_mappings')\n .where('masked_name', maskedName)\n .first();\n }\n\n async upsert(maskedName: string, realName: string): Promise<void> {\n const existing = await this.getByMaskedName(maskedName);\n if (existing) {\n await this.db('devxp_developer_mappings')\n .where('masked_name', maskedName)\n .update({ real_name: realName });\n } else {\n await this.db('devxp_developer_mappings').insert({\n masked_name: maskedName,\n real_name: realName,\n });\n }\n }\n\n async upsertBatch(mappings: { maskedName: string; realName: string }[]): Promise<number> {\n let count = 0;\n for (const mapping of mappings) {\n await this.upsert(mapping.maskedName, mapping.realName);\n count++;\n }\n return count;\n }\n\n async deleteByMaskedName(maskedName: string): Promise<boolean> {\n const deleted = await this.db('devxp_developer_mappings')\n .where('masked_name', maskedName)\n .delete();\n return deleted > 0;\n }\n\n async count(): Promise<number> {\n const result = await this.db('devxp_developer_mappings').count('id as count').first();\n return Number(result?.count ?? 0);\n }\n}\n"],"names":[],"mappings":";;AASO,MAAM,YAAA,CAAa;AAAA,EACxB,YAA6B,EAAA,EAAU;AAAV,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAAA,EAAW;AAAA,EAExC,MAAM,UAAA,GAA4B;AAChC,IAAA,IAAI,CAAE,MAAM,IAAA,CAAK,GAAG,MAAA,CAAO,QAAA,CAAS,0BAA0B,CAAA,EAAI;AAChE,MAAA,MAAM,IAAA,CAAK,EAAA,CAAG,MAAA,CAAO,WAAA,CAAY,4BAA4B,CAAA,KAAA,KAAS;AACpE,QAAA,KAAA,CAAM,UAAA,CAAW,IAAI,CAAA,CAAE,OAAA,EAAQ;AAC/B,QAAA,KAAA,CAAM,OAAO,aAAA,EAAe,GAAG,CAAA,CAAE,WAAA,GAAc,MAAA,EAAO;AACtD,QAAA,KAAA,CAAM,MAAA,CAAO,WAAA,EAAa,GAAG,CAAA,CAAE,WAAA,EAAY;AAC3C,QAAA,KAAA,CAAM,SAAA,CAAU,YAAY,CAAA,CAAE,SAAA,CAAU,KAAK,EAAA,CAAG,EAAA,CAAG,KAAK,CAAA;AAAA,MAC1D,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,MAAA,GAAsC;AAC1C,IAAA,OAAO,IAAA,CAAK,GAAqB,0BAA0B,CAAA,CACxD,OAAO,GAAG,CAAA,CACV,OAAA,CAAQ,YAAA,EAAc,MAAM,CAAA;AAAA,EACjC;AAAA,EAEA,MAAM,gBAAgB,UAAA,EAA2D;AAC/E,IAAA,OAAO,IAAA,CAAK,GAAqB,0BAA0B,CAAA,CACxD,MAAM,aAAA,EAAe,UAAU,EAC/B,KAAA,EAAM;AAAA,EACX;AAAA,EAEA,MAAM,MAAA,CAAO,UAAA,EAAoB,QAAA,EAAiC;AAChE,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,eAAA,CAAgB,UAAU,CAAA;AACtD,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,IAAA,CAAK,EAAA,CAAG,0BAA0B,CAAA,CACrC,KAAA,CAAM,aAAA,EAAe,UAAU,CAAA,CAC/B,MAAA,CAAO,EAAE,SAAA,EAAW,QAAA,EAAU,CAAA;AAAA,IACnC,CAAA,MAAO;AACL,MAAA,MAAM,IAAA,CAAK,EAAA,CAAG,0BAA0B,CAAA,CAAE,MAAA,CAAO;AAAA,QAC/C,WAAA,EAAa,UAAA;AAAA,QACb,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,QAAA,EAAuE;AACvF,IAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,IAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,MAAA,MAAM,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,UAAA,EAAY,QAAQ,QAAQ,CAAA;AACtD,MAAA,KAAA,EAAA;AAAA,IACF;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,MAAM,mBAAmB,UAAA,EAAsC;AAC7D,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,EAAA,CAAG,0BAA0B,EACrD,KAAA,CAAM,aAAA,EAAe,UAAU,CAAA,CAC/B,MAAA,EAAO;AACV,IAAA,OAAO,OAAA,GAAU,CAAA;AAAA,EACnB;AAAA,EAEA,MAAM,KAAA,GAAyB;AAC7B,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,EAAA,CAAG,0BAA0B,CAAA,CAAE,KAAA,CAAM,aAAa,CAAA,CAAE,KAAA,EAAM;AACpF,IAAA,OAAO,MAAA,CAAO,MAAA,EAAQ,KAAA,IAAS,CAAC,CAAA;AAAA,EAClC;AACF;;;;"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@karimov-labs/backstage-plugin-devxp-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backstage backend plugin for developer intelligence — SHA-256 identity hashing, developer name mappings, and CSV ingestion.",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "license": "Apache-2.0",
8
+ "author": "karimov-labs",
9
+ "homepage": "https://github.com/karimov-labs/backstage-plugin-devxp#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/karimov-labs/backstage-plugin-devxp.git",
13
+ "directory": "plugins/devxp-backend"
14
+ },
15
+ "keywords": [
16
+ "backstage",
17
+ "plugin",
18
+ "backend",
19
+ "developer-intelligence",
20
+ "devxp",
21
+ "identity-masking"
22
+ ],
23
+ "backstage": {
24
+ "role": "backend-plugin",
25
+ "pluginId": "devxp"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "main": "dist/index.cjs.js",
30
+ "types": "dist/index.d.ts"
31
+ },
32
+ "scripts": {
33
+ "start": "backstage-cli package start",
34
+ "build": "backstage-cli package build",
35
+ "lint": "backstage-cli package lint",
36
+ "test": "backstage-cli package test",
37
+ "clean": "backstage-cli package clean"
38
+ },
39
+ "dependencies": {
40
+ "@backstage/backend-plugin-api": "^1.2.0",
41
+ "@backstage/config": "^1.3.6",
42
+ "express": "^4.18.0",
43
+ "knex": "^3.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@backstage/cli": "^0.36.0",
47
+ "@types/express": "*"
48
+ },
49
+ "files": [
50
+ "dist"
51
+ ]
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { devxpPlugin as default } from './plugin';