@karimov-labs/backstage-plugin-devxp-backend 1.0.2 → 1.1.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 +104 -21
- package/dist/router.cjs.js +72 -0
- package/dist/router.cjs.js.map +1 -1
- package/dist/service/GithubSyncService.cjs.js +151 -0
- package/dist/service/GithubSyncService.cjs.js.map +1 -0
- package/dist/service/GithubSyncStore.cjs.js +59 -0
- package/dist/service/GithubSyncStore.cjs.js.map +1 -0
- package/package.json +4 -6
- package/src/index.ts +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @karimov-labs/backstage-plugin-devxp-backend
|
|
2
2
|
|
|
3
|
-
Backstage backend plugin that powers the **Developer Intelligence** dashboard. It
|
|
3
|
+
Backstage backend plugin that powers the **Developer Intelligence** dashboard. It persists masked developer identity mappings, exposes REST endpoints for hashing/unmasking, accepts bulk CSV uploads, and automatically syncs organization members from GitHub (github.com or GitHub Enterprise Server) using GitHub App credentials — all backed by the Backstage-managed database (SQLite or PostgreSQL).
|
|
4
4
|
|
|
5
5
|
This plugin is the backend counterpart to [`@karimov-labs/backstage-plugin-devxp`](https://www.npmjs.com/package/@karimov-labs/backstage-plugin-devxp).
|
|
6
6
|
|
|
@@ -10,11 +10,17 @@ Built for use with the [DevXP](https://devxp.net) developer analytics platform.
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- Persists `masked_name ↔ real_name` pairs in the Backstage database using Knex
|
|
14
|
-
- SHA-256 hashing
|
|
13
|
+
- Persists `masked_name ↔ real_name` pairs in the Backstage database using Knex
|
|
14
|
+
- SHA-256 hashing compatible with [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer): `SHA-256(salt + realName)`, truncated to 16 hex characters
|
|
15
15
|
- REST API for hashing, unmasking, bulk CSV upload, and listing/deleting mappings
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- **GitHub Organization Auto-Sync** — store GitHub App credentials per organization and sync all members on demand or automatically on frontend page load
|
|
17
|
+
- Supports **github.com** and **GitHub Enterprise Server** (any custom hostname)
|
|
18
|
+
- Uses GitHub App JWT authentication (RS256) — no OAuth token required
|
|
19
|
+
- Paginates through the full member list automatically
|
|
20
|
+
- Per-configuration active/inactive toggle
|
|
21
|
+
- Auto-sync throttled to at most once per 24 hours per backend process
|
|
22
|
+
- Reads salt and API credentials from `app-config.yaml` (never exposes secrets to the browser)
|
|
23
|
+
- Runs unauthenticated (`allow: 'unauthenticated'`) so the frontend can reach it without an extra auth token
|
|
18
24
|
|
|
19
25
|
---
|
|
20
26
|
|
|
@@ -23,7 +29,7 @@ Built for use with the [DevXP](https://devxp.net) developer analytics platform.
|
|
|
23
29
|
| Dependency | Version |
|
|
24
30
|
|---|---|
|
|
25
31
|
| Backstage | >= 1.30 |
|
|
26
|
-
| Node.js | >=
|
|
32
|
+
| Node.js | >= 22 (uses native `fetch` and `crypto`) |
|
|
27
33
|
|
|
28
34
|
---
|
|
29
35
|
|
|
@@ -84,21 +90,43 @@ export DEVXP_API_ENDPOINT="https://..." # optional
|
|
|
84
90
|
export DEVXP_PROJECT_ID="your-project-id" # optional
|
|
85
91
|
```
|
|
86
92
|
|
|
87
|
-
> **Security note:** `salt` and
|
|
93
|
+
> **Security note:** `salt`, `apiToken`, and all GitHub App private keys are read only on the backend and are never serialised into any API response.
|
|
88
94
|
|
|
89
95
|
---
|
|
90
96
|
|
|
91
97
|
### 4. Database
|
|
92
98
|
|
|
93
|
-
The plugin uses the standard Backstage database service.
|
|
99
|
+
The plugin uses the standard Backstage database service. Both tables are created automatically on startup — no manual migration is needed.
|
|
100
|
+
|
|
101
|
+
#### `devxp_developer_mappings`
|
|
102
|
+
|
|
103
|
+
Stores the hashed identity pairs populated via CSV upload or GitHub sync.
|
|
94
104
|
|
|
95
105
|
```
|
|
96
106
|
devxp_developer_mappings
|
|
97
|
-
├── id INTEGER
|
|
98
|
-
├── masked_name TEXT
|
|
99
|
-
├── real_name TEXT
|
|
100
|
-
└── created_at DATETIME
|
|
107
|
+
├── id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
108
|
+
├── masked_name TEXT UNIQUE NOT NULL
|
|
109
|
+
├── real_name TEXT NOT NULL
|
|
110
|
+
└── created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### `devxp_github_sync_configs`
|
|
114
|
+
|
|
115
|
+
Stores GitHub App credentials and sync state per organization.
|
|
116
|
+
|
|
101
117
|
```
|
|
118
|
+
devxp_github_sync_configs
|
|
119
|
+
├── id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
120
|
+
├── org_name TEXT NOT NULL
|
|
121
|
+
├── github_hostname TEXT NOT NULL DEFAULT 'github.com'
|
|
122
|
+
├── app_client_id TEXT NOT NULL
|
|
123
|
+
├── app_private_key TEXT NOT NULL ← stored server-side only, never returned by the API
|
|
124
|
+
├── active BOOLEAN NOT NULL DEFAULT true
|
|
125
|
+
├── last_synced_at DATETIME NULLABLE
|
|
126
|
+
└── created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If upgrading from a version that did not have `devxp_github_sync_configs`, the table is created fresh. If upgrading from an earlier version of the sync table that lacked the `github_hostname` column, the column is added automatically via an `ALTER TABLE` on startup.
|
|
102
130
|
|
|
103
131
|
Works with both **SQLite** (development) and **PostgreSQL** (production).
|
|
104
132
|
|
|
@@ -108,42 +136,97 @@ Works with both **SQLite** (development) and **PostgreSQL** (production).
|
|
|
108
136
|
|
|
109
137
|
All endpoints are mounted at `/api/devxp/`.
|
|
110
138
|
|
|
139
|
+
### Developer mappings
|
|
140
|
+
|
|
111
141
|
| Method | Path | Description |
|
|
112
142
|
|---|---|---|
|
|
113
143
|
| `GET` | `/health` | Health check — returns `{ status: 'ok' }` |
|
|
114
|
-
| `GET` | `/config` | Non-sensitive configuration status (booleans — never exposes secret values) |
|
|
144
|
+
| `GET` | `/config` | Non-sensitive configuration status (booleans only — never exposes secret values) |
|
|
115
145
|
| `GET` | `/mappings` | List all stored `masked_name ↔ real_name` pairs |
|
|
116
146
|
| `POST` | `/mappings/upload` | Upload CSV text; hashes each line and upserts mappings |
|
|
117
147
|
| `POST` | `/mappings/delete` | Delete a single mapping by `maskedName` |
|
|
118
148
|
| `POST` | `/unmask` | Look up a real name from a masked name |
|
|
119
149
|
| `POST` | `/hash` | Compute the masked hash for a given real name |
|
|
120
150
|
|
|
121
|
-
###
|
|
151
|
+
### GitHub sync configurations
|
|
152
|
+
|
|
153
|
+
| Method | Path | Description |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `GET` | `/github-sync` | List all sync configurations (private key excluded from response) |
|
|
156
|
+
| `POST` | `/github-sync` | Register a new GitHub App sync configuration |
|
|
157
|
+
| `POST` | `/github-sync/:id/toggle` | Activate or deactivate a configuration |
|
|
158
|
+
| `DELETE` | `/github-sync/:id` | Delete a configuration |
|
|
159
|
+
| `POST` | `/github-sync/:id/sync` | Manually trigger a member sync for a specific configuration |
|
|
160
|
+
| `POST` | `/github-sync/auto` | Trigger auto-sync for all active configurations (throttled to 24 h server-side; fire-and-forget) |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### Request / response examples
|
|
165
|
+
|
|
166
|
+
#### Upload CSV
|
|
122
167
|
|
|
123
168
|
```bash
|
|
124
169
|
curl -X POST http://localhost:7007/api/devxp/mappings/upload \
|
|
125
170
|
-H 'Content-Type: application/json' \
|
|
126
171
|
-d '{"csvContent": "Alice Smith\nBob Jones\nCarol White"}'
|
|
172
|
+
# → { "message": "Successfully processed 3 developer name mappings", "count": 3 }
|
|
127
173
|
```
|
|
128
174
|
|
|
129
|
-
|
|
175
|
+
#### Unmask a developer name
|
|
130
176
|
|
|
131
177
|
```bash
|
|
132
|
-
curl -X POST http://localhost:7007/api/devxp/
|
|
178
|
+
curl -X POST http://localhost:7007/api/devxp/unmask \
|
|
133
179
|
-H 'Content-Type: application/json' \
|
|
134
180
|
-d '{"maskedName": "a3f2c1d4e5b67890"}'
|
|
135
181
|
# → { "maskedName": "a3f2c1d4e5b67890", "realName": "Alice Smith" }
|
|
136
182
|
```
|
|
137
183
|
|
|
138
|
-
|
|
184
|
+
#### Register a GitHub sync configuration
|
|
139
185
|
|
|
140
186
|
```bash
|
|
141
|
-
curl -X POST http://localhost:7007/api/devxp/
|
|
187
|
+
curl -X POST http://localhost:7007/api/devxp/github-sync \
|
|
142
188
|
-H 'Content-Type: application/json' \
|
|
143
|
-
-d '{
|
|
144
|
-
|
|
189
|
+
-d '{
|
|
190
|
+
"orgName": "acme-corp",
|
|
191
|
+
"githubHostname": "github.com",
|
|
192
|
+
"appClientId": "Iv1.a1b2c3d4e5f67890",
|
|
193
|
+
"appPrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
|
194
|
+
}'
|
|
195
|
+
# → { "id": 1, "message": "GitHub sync configuration registered for org \"acme-corp\" on \"github.com\"" }
|
|
145
196
|
```
|
|
146
197
|
|
|
198
|
+
For **GitHub Enterprise Server**, pass the GHES hostname instead:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
"githubHostname": "github.acme.com"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The backend resolves the API base URL automatically:
|
|
205
|
+
- `github.com` → `https://api.github.com`
|
|
206
|
+
- `<hostname>` → `https://<hostname>/api/v3`
|
|
207
|
+
|
|
208
|
+
#### Manually trigger a sync
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
curl -X POST http://localhost:7007/api/devxp/github-sync/1/sync
|
|
212
|
+
# → { "message": "Synced 42 members from \"acme-corp\"", "count": 42, "orgName": "acme-corp" }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## GitHub App setup
|
|
218
|
+
|
|
219
|
+
The sync feature requires a GitHub App installed in the target organization with **Read-only** access to **Organization members**.
|
|
220
|
+
|
|
221
|
+
1. Create a GitHub App (see the in-plugin setup guide, or follow [GitHub's documentation](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)).
|
|
222
|
+
2. Under *Organization permissions*, set **Members → Read-only**.
|
|
223
|
+
3. Note the **Client ID** from the General tab.
|
|
224
|
+
4. Generate and download a **Private key** (`.pem` file).
|
|
225
|
+
5. [Install the app](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app) in your organization.
|
|
226
|
+
6. Register the credentials via the plugin UI or the `POST /api/devxp/github-sync` endpoint.
|
|
227
|
+
|
|
228
|
+
The backend authenticates as the GitHub App using an RS256-signed JWT (built with Node.js built-in `crypto` — no extra dependencies), locates the app's installation in the target organization, obtains an installation access token, then paginates through `/orgs/{org}/members`.
|
|
229
|
+
|
|
147
230
|
---
|
|
148
231
|
|
|
149
232
|
## Hashing algorithm
|
|
@@ -159,7 +242,7 @@ function hashUsername(salt: string, username: string): string {
|
|
|
159
242
|
}
|
|
160
243
|
```
|
|
161
244
|
|
|
162
|
-
This matches the algorithm used in [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer).
|
|
245
|
+
This matches the algorithm used in [dev-xp-analyzer](https://github.com/karimov-labs/dev-xp-analyzer). Ensure the same `salt` value is configured in both systems.
|
|
163
246
|
|
|
164
247
|
---
|
|
165
248
|
|
package/dist/router.cjs.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
var express = require('express');
|
|
5
5
|
var MappingStore = require('./service/MappingStore.cjs.js');
|
|
6
|
+
var GithubSyncStore = require('./service/GithubSyncStore.cjs.js');
|
|
7
|
+
var GithubSyncService = require('./service/GithubSyncService.cjs.js');
|
|
6
8
|
|
|
7
9
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
10
|
|
|
@@ -22,6 +24,9 @@ async function createRouter(options) {
|
|
|
22
24
|
const knex = await database.getClient();
|
|
23
25
|
const store = new MappingStore.MappingStore(knex);
|
|
24
26
|
await store.initialize();
|
|
27
|
+
const githubSyncStore = new GithubSyncStore.GithubSyncStore(knex);
|
|
28
|
+
await githubSyncStore.initialize();
|
|
29
|
+
const githubSyncService = new GithubSyncService.GithubSyncService(githubSyncStore, store, salt, logger);
|
|
25
30
|
logger.info("DevXP backend plugin initialized");
|
|
26
31
|
const router = express.Router();
|
|
27
32
|
router.use(express__default.default.json({ limit: "10mb" }));
|
|
@@ -107,6 +112,73 @@ async function createRouter(options) {
|
|
|
107
112
|
const maskedName = hashUsername(salt, realName);
|
|
108
113
|
res.json({ realName, maskedName });
|
|
109
114
|
});
|
|
115
|
+
router.get("/github-sync", async (_req, res) => {
|
|
116
|
+
const configs = await githubSyncStore.getAll();
|
|
117
|
+
res.json({ configs });
|
|
118
|
+
});
|
|
119
|
+
router.post("/github-sync", async (req, res) => {
|
|
120
|
+
const { orgName, githubHostname, appClientId, appPrivateKey } = req.body;
|
|
121
|
+
if (!orgName || typeof orgName !== "string") {
|
|
122
|
+
res.status(400).json({ error: "orgName is required" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!appClientId || typeof appClientId !== "string") {
|
|
126
|
+
res.status(400).json({ error: "appClientId is required" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!appPrivateKey || typeof appPrivateKey !== "string") {
|
|
130
|
+
res.status(400).json({ error: "appPrivateKey is required" });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const hostname = typeof githubHostname === "string" && githubHostname.trim() ? githubHostname.trim() : "github.com";
|
|
134
|
+
const id = await githubSyncStore.create(orgName.trim(), hostname, appClientId.trim(), appPrivateKey.trim());
|
|
135
|
+
logger.info(`DevXP: Registered GitHub sync config for org "${orgName}" on "${hostname}" (id=${id})`);
|
|
136
|
+
res.json({ id, message: `GitHub sync configuration registered for org "${orgName}" on "${hostname}"` });
|
|
137
|
+
});
|
|
138
|
+
router.post("/github-sync/:id/toggle", async (req, res) => {
|
|
139
|
+
const id = parseInt(req.params.id, 10);
|
|
140
|
+
const { active } = req.body;
|
|
141
|
+
if (typeof active !== "boolean") {
|
|
142
|
+
res.status(400).json({ error: "active (boolean) is required" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const updated = await githubSyncStore.setActive(id, active);
|
|
146
|
+
if (!updated) {
|
|
147
|
+
res.status(404).json({ error: "GitHub sync configuration not found" });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
res.json({ message: `GitHub sync configuration ${active ? "activated" : "deactivated"}` });
|
|
151
|
+
});
|
|
152
|
+
router.delete("/github-sync/:id", async (req, res) => {
|
|
153
|
+
const id = parseInt(req.params.id, 10);
|
|
154
|
+
const deleted = await githubSyncStore.delete(id);
|
|
155
|
+
if (!deleted) {
|
|
156
|
+
res.status(404).json({ error: "GitHub sync configuration not found" });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
logger.info(`DevXP: Deleted GitHub sync config id=${id}`);
|
|
160
|
+
res.json({ message: "GitHub sync configuration deleted" });
|
|
161
|
+
});
|
|
162
|
+
router.post("/github-sync/:id/sync", async (req, res) => {
|
|
163
|
+
const id = parseInt(req.params.id, 10);
|
|
164
|
+
try {
|
|
165
|
+
const result = await githubSyncService.syncConfig(id);
|
|
166
|
+
res.json({
|
|
167
|
+
message: `Synced ${result.count} members from "${result.orgName}"`,
|
|
168
|
+
count: result.count,
|
|
169
|
+
orgName: result.orgName
|
|
170
|
+
});
|
|
171
|
+
} catch (e) {
|
|
172
|
+
logger.error(`DevXP GitHub manual sync failed for config ${id}: ${e.message}`);
|
|
173
|
+
res.status(500).json({ error: e.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
router.post("/github-sync/auto", async (_req, res) => {
|
|
177
|
+
githubSyncService.autoSync().catch((e) => {
|
|
178
|
+
logger.warn(`DevXP GitHub auto-sync error: ${e.message}`);
|
|
179
|
+
});
|
|
180
|
+
res.json({ message: "Auto-sync triggered" });
|
|
181
|
+
});
|
|
110
182
|
return router;
|
|
111
183
|
}
|
|
112
184
|
|
package/dist/router.cjs.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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';\nimport { GithubSyncStore } from './service/GithubSyncStore';\nimport { GithubSyncService } from './service/GithubSyncService';\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 const githubSyncStore = new GithubSyncStore(knex);\n await githubSyncStore.initialize();\n\n const githubSyncService = new GithubSyncService(githubSyncStore, store, salt, logger);\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 // ─── GitHub Sync Configurations ─────────────────────────────────────────────\n\n // List all GitHub sync configurations (private key excluded)\n router.get('/github-sync', async (_req, res) => {\n const configs = await githubSyncStore.getAll();\n res.json({ configs });\n });\n\n // Register a new GitHub sync configuration\n router.post('/github-sync', async (req, res) => {\n const { orgName, githubHostname, appClientId, appPrivateKey } = req.body;\n if (!orgName || typeof orgName !== 'string') {\n res.status(400).json({ error: 'orgName is required' });\n return;\n }\n if (!appClientId || typeof appClientId !== 'string') {\n res.status(400).json({ error: 'appClientId is required' });\n return;\n }\n if (!appPrivateKey || typeof appPrivateKey !== 'string') {\n res.status(400).json({ error: 'appPrivateKey is required' });\n return;\n }\n\n const hostname = (typeof githubHostname === 'string' && githubHostname.trim())\n ? githubHostname.trim()\n : 'github.com';\n\n const id = await githubSyncStore.create(orgName.trim(), hostname, appClientId.trim(), appPrivateKey.trim());\n logger.info(`DevXP: Registered GitHub sync config for org \"${orgName}\" on \"${hostname}\" (id=${id})`);\n res.json({ id, message: `GitHub sync configuration registered for org \"${orgName}\" on \"${hostname}\"` });\n });\n\n // Toggle active/inactive for a GitHub sync configuration\n router.post('/github-sync/:id/toggle', async (req, res) => {\n const id = parseInt(req.params.id, 10);\n const { active } = req.body;\n if (typeof active !== 'boolean') {\n res.status(400).json({ error: 'active (boolean) is required' });\n return;\n }\n\n const updated = await githubSyncStore.setActive(id, active);\n if (!updated) {\n res.status(404).json({ error: 'GitHub sync configuration not found' });\n return;\n }\n\n res.json({ message: `GitHub sync configuration ${active ? 'activated' : 'deactivated'}` });\n });\n\n // Delete a GitHub sync configuration\n router.delete('/github-sync/:id', async (req, res) => {\n const id = parseInt(req.params.id, 10);\n const deleted = await githubSyncStore.delete(id);\n if (!deleted) {\n res.status(404).json({ error: 'GitHub sync configuration not found' });\n return;\n }\n logger.info(`DevXP: Deleted GitHub sync config id=${id}`);\n res.json({ message: 'GitHub sync configuration deleted' });\n });\n\n // Manually trigger sync for a specific configuration\n router.post('/github-sync/:id/sync', async (req, res) => {\n const id = parseInt(req.params.id, 10);\n try {\n const result = await githubSyncService.syncConfig(id);\n res.json({\n message: `Synced ${result.count} members from \"${result.orgName}\"`,\n count: result.count,\n orgName: result.orgName,\n });\n } catch (e: any) {\n logger.error(`DevXP GitHub manual sync failed for config ${id}: ${e.message}`);\n res.status(500).json({ error: e.message });\n }\n });\n\n // Trigger automatic sync (called by frontend on page load, throttled to 24h)\n router.post('/github-sync/auto', async (_req, res) => {\n // Fire and forget — respond immediately, let sync run in background\n githubSyncService.autoSync().catch((e: any) => {\n logger.warn(`DevXP GitHub auto-sync error: ${e.message}`);\n });\n res.json({ message: 'Auto-sync triggered' });\n });\n\n return router;\n}\n"],"names":["createHash","MappingStore","GithubSyncStore","GithubSyncService","Router","express"],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function base64UrlEncode(data) {
|
|
6
|
+
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
7
|
+
return buf.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
8
|
+
}
|
|
9
|
+
function createGithubAppJwt(clientId, privateKey) {
|
|
10
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
11
|
+
const header = base64UrlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
12
|
+
const payload = base64UrlEncode(
|
|
13
|
+
JSON.stringify({ iat: now - 60, exp: now + 600, iss: clientId })
|
|
14
|
+
);
|
|
15
|
+
const signingInput = `${header}.${payload}`;
|
|
16
|
+
const sign = crypto.createSign("RSA-SHA256");
|
|
17
|
+
sign.update(signingInput);
|
|
18
|
+
const signature = base64UrlEncode(sign.sign(privateKey));
|
|
19
|
+
return `${signingInput}.${signature}`;
|
|
20
|
+
}
|
|
21
|
+
function hashUsername(salt, username) {
|
|
22
|
+
return crypto.createHash("sha256").update(`${salt}${username}`).digest("hex").substring(0, 16);
|
|
23
|
+
}
|
|
24
|
+
const USER_AGENT = "devxp-backstage-plugin/1.0";
|
|
25
|
+
function resolveApiBase(hostname) {
|
|
26
|
+
const h = hostname.trim().toLowerCase();
|
|
27
|
+
return h === "github.com" ? "https://api.github.com" : `https://${h}/api/v3`;
|
|
28
|
+
}
|
|
29
|
+
const AUTO_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
30
|
+
class GithubSyncService {
|
|
31
|
+
constructor(githubSyncStore, mappingStore, salt, logger) {
|
|
32
|
+
this.githubSyncStore = githubSyncStore;
|
|
33
|
+
this.mappingStore = mappingStore;
|
|
34
|
+
this.salt = salt;
|
|
35
|
+
this.logger = logger;
|
|
36
|
+
}
|
|
37
|
+
lastAutoSyncTime = null;
|
|
38
|
+
/**
|
|
39
|
+
* Sync all active configurations. Called automatically on frontend page load,
|
|
40
|
+
* but throttled to at most once per 24 hours per process.
|
|
41
|
+
*/
|
|
42
|
+
async autoSync() {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (this.lastAutoSyncTime !== null && now - this.lastAutoSyncTime < AUTO_SYNC_INTERVAL_MS) {
|
|
45
|
+
this.logger.debug("DevXP GitHub auto-sync skipped (already ran within 24h)");
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
this.lastAutoSyncTime = now;
|
|
49
|
+
const activeConfigs = await this.githubSyncStore.getActiveConfigs();
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const config of activeConfigs) {
|
|
52
|
+
try {
|
|
53
|
+
const result = await this.syncConfig(config.id);
|
|
54
|
+
results.push(result);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
this.logger.warn(
|
|
57
|
+
`DevXP GitHub auto-sync failed for org "${config.org_name}": ${e.message}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Manually sync a specific configuration by ID.
|
|
65
|
+
*/
|
|
66
|
+
async syncConfig(configId) {
|
|
67
|
+
const config = await this.githubSyncStore.getById(configId);
|
|
68
|
+
if (!config) throw new Error(`GitHub sync config ${configId} not found`);
|
|
69
|
+
if (!this.salt) {
|
|
70
|
+
throw new Error("Salt is not configured. Set devxp.salt in app-config.yaml");
|
|
71
|
+
}
|
|
72
|
+
const members = await this.fetchOrgMembers(
|
|
73
|
+
config.org_name,
|
|
74
|
+
config.github_hostname ?? "github.com",
|
|
75
|
+
config.app_client_id,
|
|
76
|
+
config.app_private_key
|
|
77
|
+
);
|
|
78
|
+
const mappings = members.map((login) => ({
|
|
79
|
+
maskedName: hashUsername(this.salt, login),
|
|
80
|
+
realName: login
|
|
81
|
+
}));
|
|
82
|
+
await this.mappingStore.upsertBatch(mappings);
|
|
83
|
+
await this.githubSyncStore.updateLastSynced(configId);
|
|
84
|
+
this.logger.info(
|
|
85
|
+
`DevXP GitHub sync: synced ${members.length} members from org "${config.org_name}"`
|
|
86
|
+
);
|
|
87
|
+
return { configId, orgName: config.org_name, count: members.length };
|
|
88
|
+
}
|
|
89
|
+
async fetchOrgMembers(orgName, hostname, clientId, privateKey) {
|
|
90
|
+
const apiBase = resolveApiBase(hostname);
|
|
91
|
+
const jwt = createGithubAppJwt(clientId, privateKey);
|
|
92
|
+
const headers = {
|
|
93
|
+
Authorization: `Bearer ${jwt}`,
|
|
94
|
+
Accept: "application/vnd.github.v3+json",
|
|
95
|
+
"User-Agent": USER_AGENT
|
|
96
|
+
};
|
|
97
|
+
const installationsRes = await fetch(`${apiBase}/app/installations`, { headers });
|
|
98
|
+
if (!installationsRes.ok) {
|
|
99
|
+
const body = await installationsRes.text();
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Failed to list GitHub App installations (${installationsRes.status}): ${body}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const installations = await installationsRes.json();
|
|
105
|
+
const installation = installations.find(
|
|
106
|
+
(i) => i.account?.login?.toLowerCase() === orgName.toLowerCase()
|
|
107
|
+
);
|
|
108
|
+
if (!installation) {
|
|
109
|
+
const installUrl = hostname === "github.com" ? `https://github.com/organizations/${orgName}/settings/installations` : `https://${hostname}/organizations/${orgName}/settings/installations`;
|
|
110
|
+
throw new Error(
|
|
111
|
+
`GitHub App is not installed in organization "${orgName}". Please install the app at ${installUrl}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const tokenRes = await fetch(
|
|
115
|
+
`${apiBase}/app/installations/${installation.id}/access_tokens`,
|
|
116
|
+
{ method: "POST", headers }
|
|
117
|
+
);
|
|
118
|
+
if (!tokenRes.ok) {
|
|
119
|
+
const body = await tokenRes.text();
|
|
120
|
+
throw new Error(`Failed to create installation token (${tokenRes.status}): ${body}`);
|
|
121
|
+
}
|
|
122
|
+
const { token } = await tokenRes.json();
|
|
123
|
+
const members = [];
|
|
124
|
+
let page = 1;
|
|
125
|
+
while (true) {
|
|
126
|
+
const membersRes = await fetch(
|
|
127
|
+
`${apiBase}/orgs/${encodeURIComponent(orgName)}/members?per_page=100&page=${page}`,
|
|
128
|
+
{
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `token ${token}`,
|
|
131
|
+
Accept: "application/vnd.github.v3+json",
|
|
132
|
+
"User-Agent": USER_AGENT
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
if (!membersRes.ok) {
|
|
137
|
+
const body = await membersRes.text();
|
|
138
|
+
throw new Error(`Failed to list org members (${membersRes.status}): ${body}`);
|
|
139
|
+
}
|
|
140
|
+
const batch = await membersRes.json();
|
|
141
|
+
if (batch.length === 0) break;
|
|
142
|
+
members.push(...batch.map((m) => m.login));
|
|
143
|
+
if (batch.length < 100) break;
|
|
144
|
+
page++;
|
|
145
|
+
}
|
|
146
|
+
return members;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
exports.GithubSyncService = GithubSyncService;
|
|
151
|
+
//# sourceMappingURL=GithubSyncService.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubSyncService.cjs.js","sources":["../../src/service/GithubSyncService.ts"],"sourcesContent":["import { createHash, createSign } from 'crypto';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { GithubSyncStore } from './GithubSyncStore';\nimport { MappingStore } from './MappingStore';\n\nfunction base64UrlEncode(data: string | Buffer): string {\n const buf = typeof data === 'string' ? Buffer.from(data) : data;\n return buf.toString('base64').replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\nfunction createGithubAppJwt(clientId: string, privateKey: string): string {\n const now = Math.floor(Date.now() / 1000);\n const header = base64UrlEncode(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const payload = base64UrlEncode(\n JSON.stringify({ iat: now - 60, exp: now + 600, iss: clientId }),\n );\n const signingInput = `${header}.${payload}`;\n const sign = createSign('RSA-SHA256');\n sign.update(signingInput);\n const signature = base64UrlEncode(sign.sign(privateKey));\n return `${signingInput}.${signature}`;\n}\n\nfunction hashUsername(salt: string, username: string): string {\n return createHash('sha256')\n .update(`${salt}${username}`)\n .digest('hex')\n .substring(0, 16);\n}\n\nconst USER_AGENT = 'devxp-backstage-plugin/1.0';\n\n/**\n * Returns the REST API base URL for a given GitHub hostname.\n * - github.com → https://api.github.com\n * - GHES host → https://{hostname}/api/v3\n */\nfunction resolveApiBase(hostname: string): string {\n const h = hostname.trim().toLowerCase();\n return h === 'github.com' ? 'https://api.github.com' : `https://${h}/api/v3`;\n}\n// Auto-sync at most once every 24 hours per process lifetime\nconst AUTO_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\nexport interface SyncResult {\n configId: number;\n orgName: string;\n count: number;\n}\n\nexport class GithubSyncService {\n private lastAutoSyncTime: number | null = null;\n\n constructor(\n private readonly githubSyncStore: GithubSyncStore,\n private readonly mappingStore: MappingStore,\n private readonly salt: string,\n private readonly logger: LoggerService,\n ) {}\n\n /**\n * Sync all active configurations. Called automatically on frontend page load,\n * but throttled to at most once per 24 hours per process.\n */\n async autoSync(): Promise<SyncResult[]> {\n const now = Date.now();\n if (\n this.lastAutoSyncTime !== null &&\n now - this.lastAutoSyncTime < AUTO_SYNC_INTERVAL_MS\n ) {\n this.logger.debug('DevXP GitHub auto-sync skipped (already ran within 24h)');\n return [];\n }\n this.lastAutoSyncTime = now;\n\n const activeConfigs = await this.githubSyncStore.getActiveConfigs();\n const results: SyncResult[] = [];\n\n for (const config of activeConfigs) {\n try {\n const result = await this.syncConfig(config.id);\n results.push(result);\n } catch (e: any) {\n this.logger.warn(\n `DevXP GitHub auto-sync failed for org \"${config.org_name}\": ${e.message}`,\n );\n }\n }\n\n return results;\n }\n\n /**\n * Manually sync a specific configuration by ID.\n */\n async syncConfig(configId: number): Promise<SyncResult> {\n const config = await this.githubSyncStore.getById(configId);\n if (!config) throw new Error(`GitHub sync config ${configId} not found`);\n\n if (!this.salt) {\n throw new Error('Salt is not configured. Set devxp.salt in app-config.yaml');\n }\n\n const members = await this.fetchOrgMembers(\n config.org_name,\n config.github_hostname ?? 'github.com',\n config.app_client_id,\n config.app_private_key,\n );\n\n const mappings = members.map(login => ({\n maskedName: hashUsername(this.salt, login),\n realName: login,\n }));\n\n await this.mappingStore.upsertBatch(mappings);\n await this.githubSyncStore.updateLastSynced(configId);\n\n this.logger.info(\n `DevXP GitHub sync: synced ${members.length} members from org \"${config.org_name}\"`,\n );\n\n return { configId, orgName: config.org_name, count: members.length };\n }\n\n private async fetchOrgMembers(\n orgName: string,\n hostname: string,\n clientId: string,\n privateKey: string,\n ): Promise<string[]> {\n const apiBase = resolveApiBase(hostname);\n const jwt = createGithubAppJwt(clientId, privateKey);\n const headers = {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github.v3+json',\n 'User-Agent': USER_AGENT,\n };\n\n // Find the installation for this org\n const installationsRes = await fetch(`${apiBase}/app/installations`, { headers });\n if (!installationsRes.ok) {\n const body = await installationsRes.text();\n throw new Error(\n `Failed to list GitHub App installations (${installationsRes.status}): ${body}`,\n );\n }\n\n const installations: any[] = await installationsRes.json();\n const installation = installations.find(\n i => i.account?.login?.toLowerCase() === orgName.toLowerCase(),\n );\n if (!installation) {\n const installUrl = hostname === 'github.com'\n ? `https://github.com/organizations/${orgName}/settings/installations`\n : `https://${hostname}/organizations/${orgName}/settings/installations`;\n throw new Error(\n `GitHub App is not installed in organization \"${orgName}\". ` +\n `Please install the app at ${installUrl}`,\n );\n }\n\n // Create an installation access token\n const tokenRes = await fetch(\n `${apiBase}/app/installations/${installation.id}/access_tokens`,\n { method: 'POST', headers },\n );\n if (!tokenRes.ok) {\n const body = await tokenRes.text();\n throw new Error(`Failed to create installation token (${tokenRes.status}): ${body}`);\n }\n const { token } = await tokenRes.json();\n\n // List all org members (paginated)\n const members: string[] = [];\n let page = 1;\n while (true) {\n const membersRes = await fetch(\n `${apiBase}/orgs/${encodeURIComponent(orgName)}/members?per_page=100&page=${page}`,\n {\n headers: {\n Authorization: `token ${token}`,\n Accept: 'application/vnd.github.v3+json',\n 'User-Agent': USER_AGENT,\n },\n },\n );\n if (!membersRes.ok) {\n const body = await membersRes.text();\n throw new Error(`Failed to list org members (${membersRes.status}): ${body}`);\n }\n const batch: any[] = await membersRes.json();\n if (batch.length === 0) break;\n members.push(...batch.map((m: any) => m.login as string));\n if (batch.length < 100) break;\n page++;\n }\n\n return members;\n }\n}\n"],"names":["createSign","createHash"],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class GithubSyncStore {
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
async initialize() {
|
|
8
|
+
if (!await this.db.schema.hasTable("devxp_github_sync_configs")) {
|
|
9
|
+
await this.db.schema.createTable("devxp_github_sync_configs", (table) => {
|
|
10
|
+
table.increments("id").primary();
|
|
11
|
+
table.string("org_name", 255).notNullable();
|
|
12
|
+
table.string("github_hostname", 255).notNullable().defaultTo("github.com");
|
|
13
|
+
table.string("app_client_id", 255).notNullable();
|
|
14
|
+
table.text("app_private_key").notNullable();
|
|
15
|
+
table.boolean("active").notNullable().defaultTo(true);
|
|
16
|
+
table.timestamp("last_synced_at").nullable();
|
|
17
|
+
table.timestamp("created_at").defaultTo(this.db.fn.now());
|
|
18
|
+
});
|
|
19
|
+
} else if (!await this.db.schema.hasColumn("devxp_github_sync_configs", "github_hostname")) {
|
|
20
|
+
await this.db.schema.alterTable("devxp_github_sync_configs", (table) => {
|
|
21
|
+
table.string("github_hostname", 255).notNullable().defaultTo("github.com");
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async getAll() {
|
|
26
|
+
return this.db("devxp_github_sync_configs").select("id", "org_name", "github_hostname", "app_client_id", "active", "last_synced_at", "created_at").orderBy("created_at", "desc");
|
|
27
|
+
}
|
|
28
|
+
async getById(id) {
|
|
29
|
+
return this.db("devxp_github_sync_configs").where("id", id).first();
|
|
30
|
+
}
|
|
31
|
+
async getActiveConfigs() {
|
|
32
|
+
return this.db("devxp_github_sync_configs").where("active", true).select("*");
|
|
33
|
+
}
|
|
34
|
+
async create(orgName, githubHostname, appClientId, appPrivateKey) {
|
|
35
|
+
await this.db("devxp_github_sync_configs").insert({
|
|
36
|
+
org_name: orgName,
|
|
37
|
+
github_hostname: githubHostname,
|
|
38
|
+
app_client_id: appClientId,
|
|
39
|
+
app_private_key: appPrivateKey,
|
|
40
|
+
active: true
|
|
41
|
+
});
|
|
42
|
+
const row = await this.db("devxp_github_sync_configs").where({ org_name: orgName, app_client_id: appClientId }).orderBy("id", "desc").first();
|
|
43
|
+
return row.id;
|
|
44
|
+
}
|
|
45
|
+
async setActive(id, active) {
|
|
46
|
+
const updated = await this.db("devxp_github_sync_configs").where("id", id).update({ active });
|
|
47
|
+
return updated > 0;
|
|
48
|
+
}
|
|
49
|
+
async updateLastSynced(id) {
|
|
50
|
+
await this.db("devxp_github_sync_configs").where("id", id).update({ last_synced_at: this.db.fn.now() });
|
|
51
|
+
}
|
|
52
|
+
async delete(id) {
|
|
53
|
+
const deleted = await this.db("devxp_github_sync_configs").where("id", id).delete();
|
|
54
|
+
return deleted > 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.GithubSyncStore = GithubSyncStore;
|
|
59
|
+
//# sourceMappingURL=GithubSyncStore.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubSyncStore.cjs.js","sources":["../../src/service/GithubSyncStore.ts"],"sourcesContent":["import { Knex } from 'knex';\n\nexport interface GithubSyncConfig {\n id: number;\n org_name: string;\n github_hostname: string;\n app_client_id: string;\n app_private_key: string;\n active: boolean;\n last_synced_at: string | null;\n created_at: string;\n}\n\nexport interface GithubSyncConfigPublic {\n id: number;\n org_name: string;\n github_hostname: string;\n app_client_id: string;\n active: boolean;\n last_synced_at: string | null;\n created_at: string;\n}\n\nexport class GithubSyncStore {\n constructor(private readonly db: Knex) {}\n\n async initialize(): Promise<void> {\n if (!(await this.db.schema.hasTable('devxp_github_sync_configs'))) {\n await this.db.schema.createTable('devxp_github_sync_configs', table => {\n table.increments('id').primary();\n table.string('org_name', 255).notNullable();\n table.string('github_hostname', 255).notNullable().defaultTo('github.com');\n table.string('app_client_id', 255).notNullable();\n table.text('app_private_key').notNullable();\n table.boolean('active').notNullable().defaultTo(true);\n table.timestamp('last_synced_at').nullable();\n table.timestamp('created_at').defaultTo(this.db.fn.now());\n });\n } else if (!(await this.db.schema.hasColumn('devxp_github_sync_configs', 'github_hostname'))) {\n // Migrate existing table: add github_hostname column\n await this.db.schema.alterTable('devxp_github_sync_configs', table => {\n table.string('github_hostname', 255).notNullable().defaultTo('github.com');\n });\n }\n }\n\n async getAll(): Promise<GithubSyncConfigPublic[]> {\n return this.db<GithubSyncConfig>('devxp_github_sync_configs')\n .select('id', 'org_name', 'github_hostname', 'app_client_id', 'active', 'last_synced_at', 'created_at')\n .orderBy('created_at', 'desc');\n }\n\n async getById(id: number): Promise<GithubSyncConfig | undefined> {\n return this.db<GithubSyncConfig>('devxp_github_sync_configs')\n .where('id', id)\n .first();\n }\n\n async getActiveConfigs(): Promise<GithubSyncConfig[]> {\n return this.db<GithubSyncConfig>('devxp_github_sync_configs')\n .where('active', true)\n .select('*');\n }\n\n async create(orgName: string, githubHostname: string, appClientId: string, appPrivateKey: string): Promise<number> {\n await this.db('devxp_github_sync_configs').insert({\n org_name: orgName,\n github_hostname: githubHostname,\n app_client_id: appClientId,\n app_private_key: appPrivateKey,\n active: true,\n });\n const row = await this.db<GithubSyncConfig>('devxp_github_sync_configs')\n .where({ org_name: orgName, app_client_id: appClientId })\n .orderBy('id', 'desc')\n .first();\n return row!.id;\n }\n\n async setActive(id: number, active: boolean): Promise<boolean> {\n const updated = await this.db('devxp_github_sync_configs')\n .where('id', id)\n .update({ active });\n return updated > 0;\n }\n\n async updateLastSynced(id: number): Promise<void> {\n await this.db('devxp_github_sync_configs')\n .where('id', id)\n .update({ last_synced_at: this.db.fn.now() });\n }\n\n async delete(id: number): Promise<boolean> {\n const deleted = await this.db('devxp_github_sync_configs')\n .where('id', id)\n .delete();\n return deleted > 0;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karimov-labs/backstage-plugin-devxp-backend",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Backstage backend plugin for developer intelligence — SHA-256 identity hashing, developer name mappings, and CSV ingestion.",
|
|
5
|
-
"main": "
|
|
6
|
-
"types": "
|
|
5
|
+
"main": "dist/index.cjs.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"author": "karimov-labs",
|
|
9
9
|
"homepage": "https://devxp.net",
|
|
@@ -20,9 +20,7 @@
|
|
|
20
20
|
"pluginId": "devxp"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
|
-
"access": "public"
|
|
24
|
-
"main": "dist/index.cjs.js",
|
|
25
|
-
"types": "dist/index.d.ts"
|
|
23
|
+
"access": "public"
|
|
26
24
|
},
|
|
27
25
|
"scripts": {
|
|
28
26
|
"start": "backstage-cli package start",
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { devxpPlugin as default } from './plugin';
|