@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 +166 -0
- package/dist/index.cjs.js +10 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/plugin.cjs.js +29 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/router.cjs.js +114 -0
- package/dist/router.cjs.js.map +1 -0
- package/dist/service/MappingStore.cjs.js +53 -0
- package/dist/service/MappingStore.cjs.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +1 -0
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 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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"}
|
|
@@ -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';
|