npm package: @karimov-labs/backstage-plugin-devxp-backend
Description: Backstage backend plugin for developer intelligence — SHA-256 identity hashing, developer name mappings, and CSV ingestion.
Author: karimov-labs
Latest version: 1.3.1

# @karimov-labs/backstage-plugin-devxp-backend

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).

This plugin is the backend counterpart to [`@karimov-labs/backstage-plugin-devxp`](https://www.npmjs.com/package/@karimov-labs/backstage-plugin-devxp).

Built for use with the [DevXP](https://devxp.net) developer analytics platform.

---

## Features

- Persists `masked_name ↔ real_name` pairs in the Backstage database using Knex
- 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
- REST API for hashing, unmasking, bulk CSV upload, and listing/deleting mappings
- **GitHub Organization Auto-Sync** — store GitHub App credentials per organization and sync all members on demand or automatically on frontend page load
  - Supports **github.com** and **GitHub Enterprise Server** (any custom hostname)
  - Uses GitHub App JWT authentication (RS256) — no OAuth token required
  - Paginates through the full member list automatically
  - Per-configuration active/inactive toggle
  - Auto-sync throttled to at most once per 24 hours per backend process
- Reads salt and API credentials from `app-config.yaml` (never exposes secrets to the browser)
- Runs unauthenticated (`allow: 'unauthenticated'`) so the frontend can reach it without an extra auth token

---

## Requirements

| Dependency | Version |
|---|---|
| Backstage | >= 1.30 |
| Node.js | >= 22 (uses native `fetch` and `crypto`) |

---

## Installation

### 1. Install the package

```bash
# yarn (Backstage default)
yarn workspace backend add @karimov-labs/backstage-plugin-devxp-backend

# npm
npm install @karimov-labs/backstage-plugin-devxp-backend
```

---

### 2. Register the plugin

Edit `packages/backend/src/index.ts`:

```ts
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// ... other plugins

backend.add(import('@karimov-labs/backstage-plugin-devxp-backend'));

backend.start();
```

---

### 3. Configure app-config.yaml

```yaml
devxp:
  # Salt used for SHA-256 hashing — must match the salt used in dev-xp-analyzer
  salt: ${DEVXP_SALT}

  # Whether developer names are masked in the analytics tool
  masked: true

  # dev-xp-analyzer API credentials (optional)
  apiToken: ${DEVXP_API_TOKEN}
  apiEndpoint: ${DEVXP_API_ENDPOINT}
  projectId: ${DEVXP_PROJECT_ID}
```

Set the corresponding environment variables before starting Backstage:

```bash
export DEVXP_SALT="your-secret-salt"
export DEVXP_API_TOKEN="your-api-token"       # optional
export DEVXP_API_ENDPOINT="https://..."        # optional
export DEVXP_PROJECT_ID="your-project-id"     # optional
```

> **Security note:** `salt`, `apiToken`, and all GitHub App private keys are read only on the backend and are never serialised into any API response.

---

### 4. Database

The plugin uses the standard Backstage database service. Both tables are created automatically on startup — no manual migration is needed.

#### `devxp_developer_mappings`

Stores the hashed identity pairs populated via CSV upload or GitHub sync.

```
devxp_developer_mappings
├── id          INTEGER   PRIMARY KEY AUTOINCREMENT
├── masked_name TEXT      UNIQUE NOT NULL
├── real_name   TEXT      NOT NULL
└── created_at  DATETIME  DEFAULT CURRENT_TIMESTAMP
```

#### `devxp_github_sync_configs`

Stores GitHub App credentials and sync state per organization.

```
devxp_github_sync_configs
├── id              INTEGER   PRIMARY KEY AUTOINCREMENT
├── org_name        TEXT      NOT NULL
├── github_hostname TEXT      NOT NULL  DEFAULT 'github.com'
├── app_client_id   TEXT      NOT NULL
├── app_private_key TEXT      NOT NULL   ← stored server-side only, never returned by the API
├── active          BOOLEAN   NOT NULL  DEFAULT true
├── last_synced_at  DATETIME  NULLABLE
└── created_at      DATETIME  DEFAULT CURRENT_TIMESTAMP
```

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.

Works with both **SQLite** (development) and **PostgreSQL** (production).

---

## REST API

All endpoints are mounted at `/api/devxp/`.

### Developer mappings

| Method | Path | Description |
|---|---|---|
| `GET` | `/health` | Health check — returns `{ status: 'ok' }` |
| `GET` | `/config` | Non-sensitive configuration status (booleans only — never exposes secret values) |
| `GET` | `/mappings` | List all stored `masked_name ↔ real_name` pairs |
| `POST` | `/mappings/upload` | Upload CSV text; hashes each line and upserts mappings |
| `POST` | `/mappings/delete` | Delete a single mapping by `maskedName` |
| `POST` | `/unmask` | Look up a real name from a masked name |
| `POST` | `/hash` | Compute the masked hash for a given real name |

### GitHub sync configurations

| Method | Path | Description |
|---|---|---|
| `GET` | `/github-sync` | List all sync configurations (private key excluded from response) |
| `POST` | `/github-sync` | Register a new GitHub App sync configuration |
| `POST` | `/github-sync/:id/toggle` | Activate or deactivate a configuration |
| `DELETE` | `/github-sync/:id` | Delete a configuration |
| `POST` | `/github-sync/:id/sync` | Manually trigger a member sync for a specific configuration |
| `POST` | `/github-sync/auto` | Trigger auto-sync for all active configurations (throttled to 24 h server-side; fire-and-forget) |

---

### Request / response examples

#### Upload CSV

```bash
curl -X POST http://localhost:7007/api/devxp/mappings/upload \
  -H 'Content-Type: application/json' \
  -d '{"csvContent": "Alice Smith\nBob Jones\nCarol White"}'
# → { "message": "Successfully processed 3 developer name mappings", "count": 3 }
```

#### Unmask a developer name

```bash
curl -X POST http://localhost:7007/api/devxp/unmask \
  -H 'Content-Type: application/json' \
  -d '{"maskedName": "a3f2c1d4e5b67890"}'
# → { "maskedName": "a3f2c1d4e5b67890", "realName": "Alice Smith" }
```

#### Register a GitHub sync configuration

```bash
curl -X POST http://localhost:7007/api/devxp/github-sync \
  -H 'Content-Type: application/json' \
  -d '{
    "orgName": "acme-corp",
    "githubHostname": "github.com",
    "appClientId": "Iv1.a1b2c3d4e5f67890",
    "appPrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
  }'
# → { "id": 1, "message": "GitHub sync configuration registered for org \"acme-corp\" on \"github.com\"" }
```

For **GitHub Enterprise Server**, pass the GHES hostname instead:

```bash
"githubHostname": "github.acme.com"
```

The backend resolves the API base URL automatically:
- `github.com` → `https://api.github.com`
- `<hostname>` → `https://<hostname>/api/v3`

#### Manually trigger a sync

```bash
curl -X POST http://localhost:7007/api/devxp/github-sync/1/sync
# → { "message": "Synced 42 members from \"acme-corp\"", "count": 42, "orgName": "acme-corp" }
```

---

## GitHub App setup

The sync feature requires a GitHub App installed in the target organization with **Read-only** access to **Organization members**.

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)).
2. Under *Organization permissions*, set **Members → Read-only**.
3. Note the **Client ID** from the General tab.
4. Generate and download a **Private key** (`.pem` file).
5. [Install the app](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app) in your organization.
6. Register the credentials via the plugin UI or the `POST /api/devxp/github-sync` endpoint.

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`.

---

## Hashing algorithm

```ts
import { createHash } from 'crypto';

function hashUsername(salt: string, username: string): string {
  return createHash('sha256')
    .update(salt + username)
    .digest('hex')
    .substring(0, 16);
}
```

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.

---

## License

Apache-2.0