@nospt/plugin-dev-ai-hub-backend 0.3.0 → 1.0.0-rc1
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 +121 -0
- package/dist/database/TelemetryStore.cjs.js +12 -2
- package/dist/database/TelemetryStore.cjs.js.map +1 -1
- package/dist/router.cjs.js +1 -2
- package/dist/router.cjs.js.map +1 -1
- package/package.json +11 -5
- package/dist/service/toResourceSummary.cjs.js +0 -31
- package/dist/service/toResourceSummary.cjs.js.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @nospt/plugin-dev-ai-hub-backend
|
|
2
|
+
|
|
3
|
+
The backend half of **Dev AI Hub**. It reads `AiResource` entities from the Backstage catalog on demand, resolves each resource's body from its `backstage.io/source-location`, and records lightweight install/view telemetry.
|
|
4
|
+
|
|
5
|
+
> **Requires Backstage 1.51 or later.** The `AiResource` kind comes from Backstage's own alpha catalog module, which is not available before that.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
The catalog is the sole source of truth. This plugin keeps **no second copy** of any resource — it ships no ingestion path, no EntityProvider, no Git discovery and no scheduled sync. It is a consumer, never a producer.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
AiResource entity ──► flattened to ResourceSummary ──► frontend
|
|
13
|
+
(the catalog) (this plugin)
|
|
14
|
+
|
|
15
|
+
its source-location ──► read via UrlReader ──► the body
|
|
16
|
+
(a URL into Git) (your integrations)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Two consequences worth knowing before you install it:
|
|
20
|
+
|
|
21
|
+
- **Nothing is cached.** Edit an entity or its body in Git, and the next request reflects it as soon as the catalog has refreshed.
|
|
22
|
+
- **Reads resolve as the calling user.** A resource someone cannot see in the catalog is not readable through this plugin either — visibility is Backstage's to decide, not this plugin's.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn --cwd packages/backend add @nospt/plugin-dev-ai-hub-backend
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In `packages/backend/src/index.ts`, register the `AiResource` kind and then this plugin:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
backend.add(import('@backstage/plugin-catalog-backend-module-ai-model'));
|
|
34
|
+
backend.add(import('@nospt/plugin-dev-ai-hub-backend'));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`AiResource` must also be allow-listed in `app-config.yaml`, or the catalog will reject the entities before this plugin ever sees them:
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
catalog:
|
|
41
|
+
rules:
|
|
42
|
+
- allow: [Component, API, Resource, System, Domain, Location, AiResource]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Routes
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
GET /api/dev-ai-hub/resources List AiResource entities as flat ResourceSummary items
|
|
49
|
+
GET /api/dev-ai-hub/entity/:ref/raw Resolved body (markdown or JSON, by resource shape)
|
|
50
|
+
GET /api/dev-ai-hub/entity/:ref/raw/:filename A specific file from a directory-shaped body
|
|
51
|
+
GET /api/dev-ai-hub/entity/:ref/raw?download=true Download the artifact (zipped if multi-file)
|
|
52
|
+
POST /api/dev-ai-hub/telemetry Record an install/copy/download/view event
|
|
53
|
+
GET /api/dev-ai-hub/telemetry/:ref Per-action counts for a resource
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Every route requires standard Backstage authentication — there are no unauthenticated endpoints.
|
|
57
|
+
|
|
58
|
+
`GET /resources` returns the flat contract, never a raw catalog `Entity`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"items": [
|
|
63
|
+
{
|
|
64
|
+
"entityRef": "airesource:default/azure-devops-cli",
|
|
65
|
+
"name": "azure-devops-cli",
|
|
66
|
+
"title": "Azure DevOps CLI",
|
|
67
|
+
"description": "Manage Azure DevOps resources via CLI…",
|
|
68
|
+
"type": "skill",
|
|
69
|
+
"lifecycle": "production",
|
|
70
|
+
"owner": "group:ai-platform-team",
|
|
71
|
+
"sourceLocation": "url:https://github.com/your-org/ai-assets/blob/main/skills/azure-devops-cli.md",
|
|
72
|
+
"frameworks": ["github-copilot", "claude-code"],
|
|
73
|
+
"version": "1.0.0",
|
|
74
|
+
"tags": ["azure-devops", "ci-cd"],
|
|
75
|
+
"kind": "AiResource",
|
|
76
|
+
"annotations": { "…": "…" }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Body resolution
|
|
83
|
+
|
|
84
|
+
A `source-location` pointing at a **file** is served as that file. One pointing at a **directory** (a trailing `/`) is served as a tree: the entry file is picked for viewing, and `?download=true` returns the whole directory zipped.
|
|
85
|
+
|
|
86
|
+
Resolution goes through Backstage's `UrlReaderService`, so GitHub, GitLab, Bitbucket and Azure all work from your existing `integrations` config with no code here. A resource with no `source-location` is browsable but not installable, by design.
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
devAiHub:
|
|
92
|
+
telemetry:
|
|
93
|
+
# Optional. Salts the per-user hash used to dedup `view` events.
|
|
94
|
+
# Omit it and the plugin generates one on first start and persists it in
|
|
95
|
+
# its own database. Set it to keep the salt outside that database, or to
|
|
96
|
+
# control rotation — a real secret, never committed, and stable once set.
|
|
97
|
+
salt: ${DEV_AI_HUB_TELEMETRY_SALT}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Database
|
|
101
|
+
|
|
102
|
+
The plugin owns one table for telemetry events, created by its own migrations and applied automatically on start. It runs on both SQLite and PostgreSQL and needs no manual setup.
|
|
103
|
+
|
|
104
|
+
Actor identity is stored as a **one-way hash**. Counts are deduplicated per person, but "who installed this" is deliberately unanswerable — not merely unbuilt.
|
|
105
|
+
|
|
106
|
+
## Troubleshooting
|
|
107
|
+
|
|
108
|
+
| Symptom | Usually means |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `/resources` returns an empty list | No `AiResource` entities in the catalog, or the kind is missing from `catalog.rules` |
|
|
111
|
+
| A resource appears but its body 404s | The `source-location` points somewhere the `UrlReader` cannot reach — check `integrations` credentials and that the path still exists |
|
|
112
|
+
| 401 / 403 on every route | The request carries no Backstage identity; all routes are authenticated |
|
|
113
|
+
| Entities are rejected at catalog ingestion | `plugin-catalog-backend-module-ai-model` is not registered, so the `AiResource` kind is unknown |
|
|
114
|
+
|
|
115
|
+
## Full setup
|
|
116
|
+
|
|
117
|
+
Entity authoring, the frontend wiring and the decisions behind all of the above are covered once in the [repository README](https://github.com/nosportugal/backstage-plugin-dev-ai-hub#readme), which is the single place kept current.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
Apache-2.0
|
|
@@ -30,8 +30,10 @@ class TelemetryStore {
|
|
|
30
30
|
* The salt used to hash actor identity (ADR-0007). A configured value wins
|
|
31
31
|
* and is never persisted, so a deployment can keep the salt outside the
|
|
32
32
|
* database it protects. Otherwise the salt is generated once and stored,
|
|
33
|
-
* which is what makes it survive restarts
|
|
34
|
-
*
|
|
33
|
+
* which is what makes it survive restarts. No current query groups by
|
|
34
|
+
* `actor_hash` — counts are raw — but stability is what keeps hashes
|
|
35
|
+
* comparable across restarts, and so keeps distinct-viewer counting possible
|
|
36
|
+
* later without a backfill. It is deliberately not required from config: a
|
|
35
37
|
* missing value used to fail the plugin's init and take the whole backend
|
|
36
38
|
* down with it.
|
|
37
39
|
*
|
|
@@ -68,6 +70,14 @@ class TelemetryStore {
|
|
|
68
70
|
occurred_at: now.toISOString()
|
|
69
71
|
});
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Popularity for one resource (ADR-0007): every action counted raw, as a
|
|
75
|
+
* lifetime total. `view` included — it is recorded only when a user opens a
|
|
76
|
+
* resource, so each row is already one deliberate look and there is nothing
|
|
77
|
+
* to dedup away. The read-time per-(hash, day) dedup the ADR originally
|
|
78
|
+
* called for was dropped once the render-time trigger it existed to cancel
|
|
79
|
+
* was removed; see the 2026-08-03 amendment.
|
|
80
|
+
*/
|
|
71
81
|
async getCounts(entityRef) {
|
|
72
82
|
const rows = await this.db("telemetry_events").where("entity_ref", entityRef).groupBy("action").select("action").count(
|
|
73
83
|
"id as count"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TelemetryStore.cjs.js","sources":["../../src/database/TelemetryStore.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport type { Knex } from 'knex';\nimport {\n resolvePackagePath,\n type DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport type {\n TelemetryAction,\n TelemetryCounts,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/** Settings key holding the salt used to hash telemetry actor identity. */\nconst SALT_KEY = 'actor_salt';\n\nconst ZERO_COUNTS: TelemetryCounts = {\n install: 0,\n copy: 0,\n download: 0,\n view: 0,\n};\n\nexport interface TelemetryEventRecord {\n entityRef: string;\n action: TelemetryAction;\n actorHash: string | null;\n tool?: string;\n}\n\n/**\n * Telemetry event log (ADR-0007): store-all writes, raw
|
|
1
|
+
{"version":3,"file":"TelemetryStore.cjs.js","sources":["../../src/database/TelemetryStore.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport type { Knex } from 'knex';\nimport {\n resolvePackagePath,\n type DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport type {\n TelemetryAction,\n TelemetryCounts,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/** Settings key holding the salt used to hash telemetry actor identity. */\nconst SALT_KEY = 'actor_salt';\n\nconst ZERO_COUNTS: TelemetryCounts = {\n install: 0,\n copy: 0,\n download: 0,\n view: 0,\n};\n\nexport interface TelemetryEventRecord {\n entityRef: string;\n action: TelemetryAction;\n actorHash: string | null;\n tool?: string;\n}\n\n/**\n * Telemetry event log (ADR-0007): store-all writes, raw lifetime totals on\n * read. Every action counts each occurrence, `view` included.\n */\nexport class TelemetryStore {\n private constructor(private readonly db: Knex) {}\n\n static async create(options: {\n database: DatabaseService;\n }): Promise<TelemetryStore> {\n const db = await options.database.getClient();\n await db.migrate.latest({\n directory: resolvePackagePath(\n '@nospt/plugin-dev-ai-hub-backend',\n 'migrations',\n ),\n loadExtensions: ['.js'],\n });\n return new TelemetryStore(db);\n }\n\n /**\n * The salt used to hash actor identity (ADR-0007). A configured value wins\n * and is never persisted, so a deployment can keep the salt outside the\n * database it protects. Otherwise the salt is generated once and stored,\n * which is what makes it survive restarts. No current query groups by\n * `actor_hash` — counts are raw — but stability is what keeps hashes\n * comparable across restarts, and so keeps distinct-viewer counting possible\n * later without a backfill. It is deliberately not required from config: a\n * missing value used to fail the plugin's init and take the whole backend\n * down with it.\n *\n * Concurrent replicas starting together race on the insert; the loser reads\n * back the winner's value, so every replica ends up on the same salt.\n */\n async resolveSalt(configured?: string): Promise<string> {\n if (configured) {\n return configured;\n }\n const existing = await this.readSalt();\n if (existing) {\n return existing;\n }\n await this.db('telemetry_settings')\n .insert({ key: SALT_KEY, value: randomBytes(32).toString('hex') })\n .onConflict('key')\n .ignore();\n const stored = await this.readSalt();\n if (!stored) {\n throw new Error('Failed to persist the telemetry actor salt');\n }\n return stored;\n }\n\n private async readSalt(): Promise<string | undefined> {\n const row = await this.db('telemetry_settings')\n .where('key', SALT_KEY)\n .first<{ value: string } | undefined>('value');\n return row?.value;\n }\n\n async record(event: TelemetryEventRecord): Promise<void> {\n const now = new Date();\n await this.db('telemetry_events').insert({\n entity_ref: event.entityRef,\n action: event.action,\n actor_hash: event.actorHash,\n tool: event.tool ?? null,\n day: now.toISOString().slice(0, 10),\n occurred_at: now.toISOString(),\n });\n }\n\n /**\n * Popularity for one resource (ADR-0007): every action counted raw, as a\n * lifetime total. `view` included — it is recorded only when a user opens a\n * resource, so each row is already one deliberate look and there is nothing\n * to dedup away. The read-time per-(hash, day) dedup the ADR originally\n * called for was dropped once the render-time trigger it existed to cancel\n * was removed; see the 2026-08-03 amendment.\n */\n async getCounts(entityRef: string): Promise<TelemetryCounts> {\n const rows = await this.db('telemetry_events')\n .where('entity_ref', entityRef)\n .groupBy('action')\n .select('action')\n .count<Array<{ action: TelemetryAction; count: string | number }>>(\n 'id as count',\n );\n\n const counts = { ...ZERO_COUNTS };\n for (const row of rows) {\n counts[row.action] = Number(row.count);\n }\n return counts;\n }\n}\n"],"names":["resolvePackagePath","randomBytes"],"mappings":"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"}
|
package/dist/router.cjs.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
var express = require('express');
|
|
4
4
|
var archiver = require('archiver');
|
|
5
5
|
var pluginDevAiHubCommon = require('@nospt/plugin-dev-ai-hub-common');
|
|
6
|
-
var toResourceSummary = require('./service/toResourceSummary.cjs.js');
|
|
7
6
|
var telemetryHash = require('./service/telemetryHash.cjs.js');
|
|
8
7
|
var bodyResolver = require('./service/bodyResolver.cjs.js');
|
|
9
8
|
|
|
@@ -23,7 +22,7 @@ function createRouter(options) {
|
|
|
23
22
|
{ filter: { kind: "AiResource" } },
|
|
24
23
|
{ credentials }
|
|
25
24
|
);
|
|
26
|
-
const items = entities.map(
|
|
25
|
+
const items = entities.map(pluginDevAiHubCommon.toResourceSummary).filter((s) => s !== void 0);
|
|
27
26
|
res.json({ items });
|
|
28
27
|
} catch (error) {
|
|
29
28
|
if (error.name === "AuthenticationError") {
|
package/dist/router.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import express from 'express';\nimport archiver from 'archiver';\nimport type {\n HttpAuthService,\n LoggerService,\n UrlReaderService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport type { TelemetryStore } from './database/TelemetryStore';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport { TelemetryEventInputSchema } from '@nospt/plugin-dev-ai-hub-common';\nimport { toResourceSummary } from './service/toResourceSummary';\nimport { hashActor } from './service/telemetryHash';\nimport {\n SOURCE_LOCATION_ANNOTATION,\n contentTypeFor,\n parseUrlTarget,\n resolveBody,\n} from './service/bodyResolver';\n\ninterface RouterOptions {\n logger: LoggerService;\n telemetryStore: TelemetryStore;\n telemetrySalt: string;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n reader: UrlReaderService;\n}\n\nexport function createRouter(options: RouterOptions): express.Router {\n const { telemetryStore, telemetrySalt, catalog, httpAuth, reader } = options;\n const router = express.Router();\n\n router.use(express.json());\n\n // ── Resources (v2 — catalog-backed, ADR-0001) ─────────────────────────────\n\n router.get('/resources', async (req, res) => {\n try {\n const credentials = await httpAuth.credentials(req);\n const { items: entities } = await catalog.getEntities(\n { filter: { kind: 'AiResource' } },\n { credentials },\n );\n const items = entities\n .map(toResourceSummary)\n .filter((s): s is ResourceSummary => s !== undefined);\n res.json({ items });\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(`Failed to list resources: ${error}`);\n res.status(500).json({ error: 'Failed to list resources' });\n }\n });\n\n // ── Body resolver (v2 — ADR-0002/0006/0009) ───────────────────────────────\n\n const safeFilename = (name: string) => name.replace(/[^\\w.-]+/g, '_');\n\n const sendBodyFile = (\n res: express.Response,\n name: string,\n content: Buffer,\n download: boolean,\n ) => {\n res.setHeader('Content-Type', contentTypeFor(name));\n if (download) {\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${safeFilename(name.split('/').pop() ?? name)}\"`,\n );\n }\n res.send(content);\n };\n\n const handleBodyRequest = async (\n req: express.Request,\n res: express.Response,\n filename?: string,\n ) => {\n try {\n // Re-fetch as the caller: catalog visibility is the only gate (ADR-0006).\n const credentials = await httpAuth.credentials(req);\n const entity = await catalog.getEntityByRef(req.params.ref, {\n credentials,\n });\n if (!entity) {\n res.status(404).json({ error: 'Resource not found' });\n return;\n }\n\n const sourceLocation =\n entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION];\n const target = sourceLocation && parseUrlTarget(sourceLocation);\n if (!target) {\n res.status(404).json({ error: 'Resource has no content location' });\n return;\n }\n\n let body;\n try {\n body = await resolveBody(reader, target);\n } catch (error) {\n if ((error as Error).name === 'NotFoundError') {\n res.status(404).json({ error: 'Resource content not found' });\n return;\n }\n options.logger.error(\n `Body fetch failed for ${req.params.ref}: ${error}`,\n );\n res\n .status(502)\n .json({ error: 'Failed to fetch resource content from source' });\n return;\n }\n\n const download = req.query.download === 'true';\n\n if (filename !== undefined) {\n const file =\n body.kind === 'tree'\n ? body.files.find(f => f.path === filename)\n : undefined;\n if (!file) {\n res.status(404).json({ error: 'File not found in resource body' });\n return;\n }\n sendBodyFile(res, filename, await file.content(), download);\n return;\n }\n\n if (body.kind === 'file') {\n sendBodyFile(res, body.name, body.content, download);\n return;\n }\n\n if (!download) {\n if (!body.entryPath) {\n res.status(404).json({ error: 'Resource body has no entry file' });\n return;\n }\n const entry = body.files.find(f => f.path === body.entryPath)!;\n sendBodyFile(res, body.entryPath, await entry.content(), false);\n return;\n }\n\n // Multi-file artifact: one zip (ADR-0009 — interim, not the golden road).\n if (body.files.length === 1) {\n sendBodyFile(\n res,\n body.files[0].path,\n await body.files[0].content(),\n true,\n );\n return;\n }\n res.setHeader('Content-Type', 'application/zip');\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${safeFilename(entity.metadata.name)}.zip\"`,\n );\n const archive = archiver('zip');\n archive.on('error', error => {\n options.logger.error(\n `Zip assembly failed for ${req.params.ref}: ${error}`,\n );\n res.destroy(error);\n });\n archive.pipe(res);\n for (const file of body.files) {\n archive.append(await file.content(), { name: file.path });\n }\n await archive.finalize();\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(\n `Body resolution failed for ${req.params.ref}: ${error}`,\n );\n res.status(500).json({ error: 'Failed to resolve resource body' });\n }\n };\n\n router.get('/entity/:ref/raw', (req, res) => handleBodyRequest(req, res));\n router.get('/entity/:ref/raw/:filename', (req, res) =>\n handleBodyRequest(req, res, req.params.filename),\n );\n\n // ── Telemetry (v2 — ADR-0007) ──────────────────────────────────────────────\n\n router.post('/telemetry', async (req, res) => {\n try {\n const parsed = TelemetryEventInputSchema.safeParse(req.body);\n if (!parsed.success) {\n res.status(400).json({ error: parsed.error.message });\n return;\n }\n\n const credentials = await httpAuth.credentials(req);\n const entity = await catalog.getEntityByRef(parsed.data.ref, {\n credentials,\n });\n if (!entity) {\n res.status(404).json({ error: 'Resource not found' });\n return;\n }\n\n await telemetryStore.record({\n entityRef: parsed.data.ref,\n action: parsed.data.action,\n actorHash: hashActor(credentials, telemetrySalt),\n tool: parsed.data.tool,\n });\n res.sendStatus(204);\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(`POST /telemetry failed: ${error}`);\n res.status(500).json({ error: 'Failed to record telemetry event' });\n }\n });\n\n router.get('/telemetry/:ref', async (req, res) => {\n try {\n await httpAuth.credentials(req);\n const counts = await telemetryStore.getCounts(req.params.ref);\n res.json(counts);\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(`GET /telemetry/${req.params.ref} failed: ${error}`);\n res.status(500).json({ error: 'Failed to fetch telemetry counts' });\n }\n });\n\n return router;\n}\n"],"names":["express","toResourceSummary","contentTypeFor","SOURCE_LOCATION_ANNOTATION","parseUrlTarget","resolveBody","archiver","TelemetryEventInputSchema","hashActor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import express from 'express';\nimport archiver from 'archiver';\nimport type {\n HttpAuthService,\n LoggerService,\n UrlReaderService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport type { TelemetryStore } from './database/TelemetryStore';\nimport type { ResourceSummary } from '@nospt/plugin-dev-ai-hub-common';\nimport {\n TelemetryEventInputSchema,\n toResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\nimport { hashActor } from './service/telemetryHash';\nimport {\n SOURCE_LOCATION_ANNOTATION,\n contentTypeFor,\n parseUrlTarget,\n resolveBody,\n} from './service/bodyResolver';\n\ninterface RouterOptions {\n logger: LoggerService;\n telemetryStore: TelemetryStore;\n telemetrySalt: string;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n reader: UrlReaderService;\n}\n\nexport function createRouter(options: RouterOptions): express.Router {\n const { telemetryStore, telemetrySalt, catalog, httpAuth, reader } = options;\n const router = express.Router();\n\n router.use(express.json());\n\n // ── Resources (v2 — catalog-backed, ADR-0001) ─────────────────────────────\n\n router.get('/resources', async (req, res) => {\n try {\n const credentials = await httpAuth.credentials(req);\n const { items: entities } = await catalog.getEntities(\n { filter: { kind: 'AiResource' } },\n { credentials },\n );\n const items = entities\n .map(toResourceSummary)\n .filter((s): s is ResourceSummary => s !== undefined);\n res.json({ items });\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(`Failed to list resources: ${error}`);\n res.status(500).json({ error: 'Failed to list resources' });\n }\n });\n\n // ── Body resolver (v2 — ADR-0002/0006/0009) ───────────────────────────────\n\n const safeFilename = (name: string) => name.replace(/[^\\w.-]+/g, '_');\n\n const sendBodyFile = (\n res: express.Response,\n name: string,\n content: Buffer,\n download: boolean,\n ) => {\n res.setHeader('Content-Type', contentTypeFor(name));\n if (download) {\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${safeFilename(name.split('/').pop() ?? name)}\"`,\n );\n }\n res.send(content);\n };\n\n const handleBodyRequest = async (\n req: express.Request,\n res: express.Response,\n filename?: string,\n ) => {\n try {\n // Re-fetch as the caller: catalog visibility is the only gate (ADR-0006).\n const credentials = await httpAuth.credentials(req);\n const entity = await catalog.getEntityByRef(req.params.ref, {\n credentials,\n });\n if (!entity) {\n res.status(404).json({ error: 'Resource not found' });\n return;\n }\n\n const sourceLocation =\n entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION];\n const target = sourceLocation && parseUrlTarget(sourceLocation);\n if (!target) {\n res.status(404).json({ error: 'Resource has no content location' });\n return;\n }\n\n let body;\n try {\n body = await resolveBody(reader, target);\n } catch (error) {\n if ((error as Error).name === 'NotFoundError') {\n res.status(404).json({ error: 'Resource content not found' });\n return;\n }\n options.logger.error(\n `Body fetch failed for ${req.params.ref}: ${error}`,\n );\n res\n .status(502)\n .json({ error: 'Failed to fetch resource content from source' });\n return;\n }\n\n const download = req.query.download === 'true';\n\n if (filename !== undefined) {\n const file =\n body.kind === 'tree'\n ? body.files.find(f => f.path === filename)\n : undefined;\n if (!file) {\n res.status(404).json({ error: 'File not found in resource body' });\n return;\n }\n sendBodyFile(res, filename, await file.content(), download);\n return;\n }\n\n if (body.kind === 'file') {\n sendBodyFile(res, body.name, body.content, download);\n return;\n }\n\n if (!download) {\n if (!body.entryPath) {\n res.status(404).json({ error: 'Resource body has no entry file' });\n return;\n }\n const entry = body.files.find(f => f.path === body.entryPath)!;\n sendBodyFile(res, body.entryPath, await entry.content(), false);\n return;\n }\n\n // Multi-file artifact: one zip (ADR-0009 — interim, not the golden road).\n if (body.files.length === 1) {\n sendBodyFile(\n res,\n body.files[0].path,\n await body.files[0].content(),\n true,\n );\n return;\n }\n res.setHeader('Content-Type', 'application/zip');\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${safeFilename(entity.metadata.name)}.zip\"`,\n );\n const archive = archiver('zip');\n archive.on('error', error => {\n options.logger.error(\n `Zip assembly failed for ${req.params.ref}: ${error}`,\n );\n res.destroy(error);\n });\n archive.pipe(res);\n for (const file of body.files) {\n archive.append(await file.content(), { name: file.path });\n }\n await archive.finalize();\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(\n `Body resolution failed for ${req.params.ref}: ${error}`,\n );\n res.status(500).json({ error: 'Failed to resolve resource body' });\n }\n };\n\n router.get('/entity/:ref/raw', (req, res) => handleBodyRequest(req, res));\n router.get('/entity/:ref/raw/:filename', (req, res) =>\n handleBodyRequest(req, res, req.params.filename),\n );\n\n // ── Telemetry (v2 — ADR-0007) ──────────────────────────────────────────────\n\n router.post('/telemetry', async (req, res) => {\n try {\n const parsed = TelemetryEventInputSchema.safeParse(req.body);\n if (!parsed.success) {\n res.status(400).json({ error: parsed.error.message });\n return;\n }\n\n const credentials = await httpAuth.credentials(req);\n const entity = await catalog.getEntityByRef(parsed.data.ref, {\n credentials,\n });\n if (!entity) {\n res.status(404).json({ error: 'Resource not found' });\n return;\n }\n\n await telemetryStore.record({\n entityRef: parsed.data.ref,\n action: parsed.data.action,\n actorHash: hashActor(credentials, telemetrySalt),\n tool: parsed.data.tool,\n });\n res.sendStatus(204);\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(`POST /telemetry failed: ${error}`);\n res.status(500).json({ error: 'Failed to record telemetry event' });\n }\n });\n\n router.get('/telemetry/:ref', async (req, res) => {\n try {\n await httpAuth.credentials(req);\n const counts = await telemetryStore.getCounts(req.params.ref);\n res.json(counts);\n } catch (error) {\n if ((error as Error).name === 'AuthenticationError') {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n options.logger.error(`GET /telemetry/${req.params.ref} failed: ${error}`);\n res.status(500).json({ error: 'Failed to fetch telemetry counts' });\n }\n });\n\n return router;\n}\n"],"names":["express","toResourceSummary","contentTypeFor","SOURCE_LOCATION_ANNOTATION","parseUrlTarget","resolveBody","archiver","TelemetryEventInputSchema","hashActor"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nospt/plugin-dev-ai-hub-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc1",
|
|
4
4
|
"description": "Backend plugin for Dev AI Hub",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.cjs.js",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
33
|
"migrations",
|
|
34
|
-
"config.d.ts"
|
|
34
|
+
"config.d.ts",
|
|
35
|
+
"README.md"
|
|
35
36
|
],
|
|
36
37
|
"configSchema": "config.d.ts",
|
|
37
38
|
"backstage": {
|
|
@@ -40,8 +41,7 @@
|
|
|
40
41
|
"pluginPackages": [
|
|
41
42
|
"@nospt/plugin-dev-ai-hub",
|
|
42
43
|
"@nospt/plugin-dev-ai-hub-backend",
|
|
43
|
-
"@nospt/plugin-dev-ai-hub-common"
|
|
44
|
-
"@nospt/plugin-dev-ai-hub-node"
|
|
44
|
+
"@nospt/plugin-dev-ai-hub-common"
|
|
45
45
|
],
|
|
46
46
|
"features": {
|
|
47
47
|
".": "@backstage/BackendFeature"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@backstage/backend-plugin-api": "^1.9.1",
|
|
62
62
|
"@backstage/catalog-model": "^1.9.0",
|
|
63
63
|
"@backstage/plugin-catalog-node": "^2.2.2",
|
|
64
|
-
"@nospt/plugin-dev-ai-hub-common": "^0.
|
|
64
|
+
"@nospt/plugin-dev-ai-hub-common": "^1.0.0-rc1",
|
|
65
65
|
"archiver": "^6.0.0",
|
|
66
66
|
"express": "^5.2.1",
|
|
67
67
|
"js-yaml": "^4.1.0",
|
|
@@ -85,5 +85,11 @@
|
|
|
85
85
|
"jest-environment-jsdom": "^29.0.0",
|
|
86
86
|
"knex": "^3.1.0",
|
|
87
87
|
"supertest": "^7.0.0"
|
|
88
|
+
},
|
|
89
|
+
"jest": {
|
|
90
|
+
"coveragePathIgnorePatterns": [
|
|
91
|
+
"/node_modules/",
|
|
92
|
+
"plugin\\.ts$"
|
|
93
|
+
]
|
|
88
94
|
}
|
|
89
95
|
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
-
var pluginDevAiHubCommon = require('@nospt/plugin-dev-ai-hub-common');
|
|
5
|
-
|
|
6
|
-
function toResourceSummary(entity) {
|
|
7
|
-
const type = entity.spec?.type;
|
|
8
|
-
if (!pluginDevAiHubCommon.isResourceType(type)) {
|
|
9
|
-
return void 0;
|
|
10
|
-
}
|
|
11
|
-
const annotations = entity.metadata.annotations ?? {};
|
|
12
|
-
return {
|
|
13
|
-
entityRef: catalogModel.stringifyEntityRef(entity),
|
|
14
|
-
name: entity.metadata.name,
|
|
15
|
-
title: entity.metadata.title,
|
|
16
|
-
description: entity.metadata.description,
|
|
17
|
-
tags: entity.metadata.tags ?? [],
|
|
18
|
-
type,
|
|
19
|
-
lifecycle: typeof entity.spec?.lifecycle === "string" ? entity.spec.lifecycle : "",
|
|
20
|
-
owner: typeof entity.spec?.owner === "string" ? entity.spec.owner : void 0,
|
|
21
|
-
sourceLocation: annotations[catalogModel.ANNOTATION_SOURCE_LOCATION],
|
|
22
|
-
frameworks: pluginDevAiHubCommon.getFrameworks(entity),
|
|
23
|
-
version: annotations[pluginDevAiHubCommon.ANNOTATION_VERSION],
|
|
24
|
-
kind: entity.kind,
|
|
25
|
-
helpText: annotations[pluginDevAiHubCommon.ANNOTATION_HELP],
|
|
26
|
-
annotations
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
exports.toResourceSummary = toResourceSummary;
|
|
31
|
-
//# sourceMappingURL=toResourceSummary.cjs.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"toResourceSummary.cjs.js","sources":["../../src/service/toResourceSummary.ts"],"sourcesContent":["import {\n ANNOTATION_SOURCE_LOCATION,\n stringifyEntityRef,\n type Entity,\n} from '@backstage/catalog-model';\nimport {\n ANNOTATION_HELP,\n ANNOTATION_VERSION,\n getFrameworks,\n isResourceType,\n type ResourceSummary,\n} from '@nospt/plugin-dev-ai-hub-common';\n\n/**\n * Map a catalog AiResource entity to the flat contract served to the\n * frontend. Returns undefined for entities with an unsupported `spec.type`,\n * which are silently dropped (ADR-0003).\n */\nexport function toResourceSummary(entity: Entity): ResourceSummary | undefined {\n const type = entity.spec?.type;\n if (!isResourceType(type)) {\n return undefined;\n }\n\n const annotations = entity.metadata.annotations ?? {};\n\n return {\n entityRef: stringifyEntityRef(entity),\n name: entity.metadata.name,\n title: entity.metadata.title,\n description: entity.metadata.description,\n tags: entity.metadata.tags ?? [],\n type,\n lifecycle:\n typeof entity.spec?.lifecycle === 'string' ? entity.spec.lifecycle : '',\n owner:\n typeof entity.spec?.owner === 'string' ? entity.spec.owner : undefined,\n sourceLocation: annotations[ANNOTATION_SOURCE_LOCATION],\n frameworks: getFrameworks(entity),\n version: annotations[ANNOTATION_VERSION],\n kind: entity.kind,\n helpText: annotations[ANNOTATION_HELP],\n annotations,\n };\n}\n"],"names":["isResourceType","stringifyEntityRef","ANNOTATION_SOURCE_LOCATION","getFrameworks","ANNOTATION_VERSION","ANNOTATION_HELP"],"mappings":";;;;;AAkBO,SAAS,kBAAkB,MAAA,EAA6C;AAC7E,EAAA,MAAM,IAAA,GAAO,OAAO,IAAA,EAAM,IAAA;AAC1B,EAAA,IAAI,CAACA,mCAAA,CAAe,IAAI,CAAA,EAAG;AACzB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,CAAS,WAAA,IAAe,EAAC;AAEpD,EAAA,OAAO;AAAA,IACL,SAAA,EAAWC,gCAAmB,MAAM,CAAA;AAAA,IACpC,IAAA,EAAM,OAAO,QAAA,CAAS,IAAA;AAAA,IACtB,KAAA,EAAO,OAAO,QAAA,CAAS,KAAA;AAAA,IACvB,WAAA,EAAa,OAAO,QAAA,CAAS,WAAA;AAAA,IAC7B,IAAA,EAAM,MAAA,CAAO,QAAA,CAAS,IAAA,IAAQ,EAAC;AAAA,IAC/B,IAAA;AAAA,IACA,SAAA,EACE,OAAO,MAAA,CAAO,IAAA,EAAM,cAAc,QAAA,GAAW,MAAA,CAAO,KAAK,SAAA,GAAY,EAAA;AAAA,IACvE,KAAA,EACE,OAAO,MAAA,CAAO,IAAA,EAAM,UAAU,QAAA,GAAW,MAAA,CAAO,KAAK,KAAA,GAAQ,MAAA;AAAA,IAC/D,cAAA,EAAgB,YAAYC,uCAA0B,CAAA;AAAA,IACtD,UAAA,EAAYC,mCAAc,MAAM,CAAA;AAAA,IAChC,OAAA,EAAS,YAAYC,uCAAkB,CAAA;AAAA,IACvC,MAAM,MAAA,CAAO,IAAA;AAAA,IACb,QAAA,EAAU,YAAYC,oCAAe,CAAA;AAAA,IACrC;AAAA,GACF;AACF;;;;"}
|