@nospt/plugin-dev-ai-hub-backend 0.3.1 → 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 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
@@ -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(toResourceSummary.toResourceSummary).filter((s) => s !== void 0);
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") {
@@ -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":";;;;;;;;;;;;;;AA6BO,SAAS,aAAa,OAAA,EAAwC;AACnE,EAAA,MAAM,EAAE,cAAA,EAAgB,aAAA,EAAe,OAAA,EAAS,QAAA,EAAU,QAAO,GAAI,OAAA;AACrE,EAAA,MAAM,MAAA,GAASA,yBAAQ,MAAA,EAAO;AAE9B,EAAA,MAAA,CAAO,GAAA,CAAIA,wBAAA,CAAQ,IAAA,EAAM,CAAA;AAIzB,EAAA,MAAA,CAAO,GAAA,CAAI,YAAA,EAAc,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC3C,IAAA,IAAI;AACF,MAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,MAAA,MAAM,EAAE,KAAA,EAAO,QAAA,EAAS,GAAI,MAAM,OAAA,CAAQ,WAAA;AAAA,QACxC,EAAE,MAAA,EAAQ,EAAE,IAAA,EAAM,cAAa,EAAE;AAAA,QACjC,EAAE,WAAA;AAAY,OAChB;AACA,MAAA,MAAM,KAAA,GAAQ,SACX,GAAA,CAAIC,mCAAiB,EACrB,MAAA,CAAO,CAAC,CAAA,KAA4B,CAAA,KAAM,KAAA,CAAS,CAAA;AACtD,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA;AAAA,IACpB,SAAS,KAAA,EAAO;AACd,MAAA,IAAK,KAAA,CAAgB,SAAS,qBAAA,EAAuB;AACnD,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,2BAA2B,CAAA;AACzD,QAAA;AAAA,MACF;AACA,MAAA,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,0BAAA,EAA6B,KAAK,CAAA,CAAE,CAAA;AACzD,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,4BAA4B,CAAA;AAAA,IAC5D;AAAA,EACF,CAAC,CAAA;AAID,EAAA,MAAM,eAAe,CAAC,IAAA,KAAiB,IAAA,CAAK,OAAA,CAAQ,aAAa,GAAG,CAAA;AAEpE,EAAA,MAAM,YAAA,GAAe,CACnB,GAAA,EACA,IAAA,EACA,SACA,QAAA,KACG;AACH,IAAA,GAAA,CAAI,SAAA,CAAU,cAAA,EAAgBC,2BAAA,CAAe,IAAI,CAAC,CAAA;AAClD,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,GAAA,CAAI,SAAA;AAAA,QACF,qBAAA;AAAA,QACA,CAAA,sBAAA,EAAyB,aAAa,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,EAAI,IAAK,IAAI,CAAC,CAAA,CAAA;AAAA,OACtE;AAAA,IACF;AACA,IAAA,GAAA,CAAI,KAAK,OAAO,CAAA;AAAA,EAClB,CAAA;AAEA,EAAA,MAAM,iBAAA,GAAoB,OACxB,GAAA,EACA,GAAA,EACA,QAAA,KACG;AACH,IAAA,IAAI;AAEF,MAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,MAAA,MAAM,SAAS,MAAM,OAAA,CAAQ,cAAA,CAAe,GAAA,CAAI,OAAO,GAAA,EAAK;AAAA,QAC1D;AAAA,OACD,CAAA;AACD,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,sBAAsB,CAAA;AACpD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,cAAA,GACJ,MAAA,CAAO,QAAA,CAAS,WAAA,GAAcC,uCAA0B,CAAA;AAC1D,MAAA,MAAM,MAAA,GAAS,cAAA,IAAkBC,2BAAA,CAAe,cAAc,CAAA;AAC9D,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,oCAAoC,CAAA;AAClE,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,IAAA;AACJ,MAAA,IAAI;AACF,QAAA,IAAA,GAAO,MAAMC,wBAAA,CAAY,MAAA,EAAQ,MAAM,CAAA;AAAA,MACzC,SAAS,KAAA,EAAO;AACd,QAAA,IAAK,KAAA,CAAgB,SAAS,eAAA,EAAiB;AAC7C,UAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,8BAA8B,CAAA;AAC5D,UAAA;AAAA,QACF;AACA,QAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,UACb,CAAA,sBAAA,EAAyB,GAAA,CAAI,MAAA,CAAO,GAAG,KAAK,KAAK,CAAA;AAAA,SACnD;AACA,QAAA,GAAA,CACG,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,gDAAgD,CAAA;AACjE,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,QAAA,GAAW,GAAA,CAAI,KAAA,CAAM,QAAA,KAAa,MAAA;AAExC,MAAA,IAAI,aAAa,KAAA,CAAA,EAAW;AAC1B,QAAA,MAAM,IAAA,GACJ,IAAA,CAAK,IAAA,KAAS,MAAA,GACV,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,KAAS,QAAQ,CAAA,GACxC,KAAA,CAAA;AACN,QAAA,IAAI,CAAC,IAAA,EAAM;AACT,UAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,mCAAmC,CAAA;AACjE,UAAA;AAAA,QACF;AACA,QAAA,YAAA,CAAa,KAAK,QAAA,EAAU,MAAM,IAAA,CAAK,OAAA,IAAW,QAAQ,CAAA;AAC1D,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,IAAA,CAAK,SAAS,MAAA,EAAQ;AACxB,QAAA,YAAA,CAAa,GAAA,EAAK,IAAA,CAAK,IAAA,EAAM,IAAA,CAAK,SAAS,QAAQ,CAAA;AACnD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACnB,UAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,mCAAmC,CAAA;AACjE,UAAA;AAAA,QACF;AACA,QAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,CAAM,IAAA,CAAK,OAAK,CAAA,CAAE,IAAA,KAAS,KAAK,SAAS,CAAA;AAC5D,QAAA,YAAA,CAAa,KAAK,IAAA,CAAK,SAAA,EAAW,MAAM,KAAA,CAAM,OAAA,IAAW,KAAK,CAAA;AAC9D,QAAA;AAAA,MACF;AAGA,MAAA,IAAI,IAAA,CAAK,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC3B,QAAA,YAAA;AAAA,UACE,GAAA;AAAA,UACA,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA;AAAA,UACd,MAAM,IAAA,CAAK,KAAA,CAAM,CAAC,EAAE,OAAA,EAAQ;AAAA,UAC5B;AAAA,SACF;AACA,QAAA;AAAA,MACF;AACA,MAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,iBAAiB,CAAA;AAC/C,MAAA,GAAA,CAAI,SAAA;AAAA,QACF,qBAAA;AAAA,QACA,CAAA,sBAAA,EAAyB,YAAA,CAAa,MAAA,CAAO,QAAA,CAAS,IAAI,CAAC,CAAA,KAAA;AAAA,OAC7D;AACA,MAAA,MAAM,OAAA,GAAUC,0BAAS,KAAK,CAAA;AAC9B,MAAA,OAAA,CAAQ,EAAA,CAAG,SAAS,CAAA,KAAA,KAAS;AAC3B,QAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,UACb,CAAA,wBAAA,EAA2B,GAAA,CAAI,MAAA,CAAO,GAAG,KAAK,KAAK,CAAA;AAAA,SACrD;AACA,QAAA,GAAA,CAAI,QAAQ,KAAK,CAAA;AAAA,MACnB,CAAC,CAAA;AACD,MAAA,OAAA,CAAQ,KAAK,GAAG,CAAA;AAChB,MAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,KAAA,EAAO;AAC7B,QAAA,OAAA,CAAQ,MAAA,CAAO,MAAM,IAAA,CAAK,OAAA,IAAW,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,MAC1D;AACA,MAAA,MAAM,QAAQ,QAAA,EAAS;AAAA,IACzB,SAAS,KAAA,EAAO;AACd,MAAA,IAAK,KAAA,CAAgB,SAAS,qBAAA,EAAuB;AACnD,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,2BAA2B,CAAA;AACzD,QAAA;AAAA,MACF;AACA,MAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,QACb,CAAA,2BAAA,EAA8B,GAAA,CAAI,MAAA,CAAO,GAAG,KAAK,KAAK,CAAA;AAAA,OACxD;AACA,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,mCAAmC,CAAA;AAAA,IACnE;AAAA,EACF,CAAA;AAEA,EAAA,MAAA,CAAO,GAAA,CAAI,oBAAoB,CAAC,GAAA,EAAK,QAAQ,iBAAA,CAAkB,GAAA,EAAK,GAAG,CAAC,CAAA;AACxE,EAAA,MAAA,CAAO,GAAA;AAAA,IAAI,4BAAA;AAAA,IAA8B,CAAC,KAAK,GAAA,KAC7C,iBAAA,CAAkB,KAAK,GAAA,EAAK,GAAA,CAAI,OAAO,QAAQ;AAAA,GACjD;AAIA,EAAA,MAAA,CAAO,IAAA,CAAK,YAAA,EAAc,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC5C,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAASC,8CAAA,CAA0B,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAC3D,MAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,OAAA,EAAS,CAAA;AACpD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,MAAA,MAAM,SAAS,MAAM,OAAA,CAAQ,cAAA,CAAe,MAAA,CAAO,KAAK,GAAA,EAAK;AAAA,QAC3D;AAAA,OACD,CAAA;AACD,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,sBAAsB,CAAA;AACpD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,eAAe,MAAA,CAAO;AAAA,QAC1B,SAAA,EAAW,OAAO,IAAA,CAAK,GAAA;AAAA,QACvB,MAAA,EAAQ,OAAO,IAAA,CAAK,MAAA;AAAA,QACpB,SAAA,EAAWC,uBAAA,CAAU,WAAA,EAAa,aAAa,CAAA;AAAA,QAC/C,IAAA,EAAM,OAAO,IAAA,CAAK;AAAA,OACnB,CAAA;AACD,MAAA,GAAA,CAAI,WAAW,GAAG,CAAA;AAAA,IACpB,SAAS,KAAA,EAAO;AACd,MAAA,IAAK,KAAA,CAAgB,SAAS,qBAAA,EAAuB;AACnD,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,2BAA2B,CAAA;AACzD,QAAA;AAAA,MACF;AACA,MAAA,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,wBAAA,EAA2B,KAAK,CAAA,CAAE,CAAA;AACvD,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,oCAAoC,CAAA;AAAA,IACpE;AAAA,EACF,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,iBAAA,EAAmB,OAAO,GAAA,EAAK,GAAA,KAAQ;AAChD,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,CAAS,YAAY,GAAG,CAAA;AAC9B,MAAA,MAAM,SAAS,MAAM,cAAA,CAAe,SAAA,CAAU,GAAA,CAAI,OAAO,GAAG,CAAA;AAC5D,MAAA,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,IACjB,SAAS,KAAA,EAAO;AACd,MAAA,IAAK,KAAA,CAAgB,SAAS,qBAAA,EAAuB;AACnD,QAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,2BAA2B,CAAA;AACzD,QAAA;AAAA,MACF;AACA,MAAA,OAAA,CAAQ,MAAA,CAAO,MAAM,CAAA,eAAA,EAAkB,GAAA,CAAI,OAAO,GAAG,CAAA,SAAA,EAAY,KAAK,CAAA,CAAE,CAAA;AACxE,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,oCAAoC,CAAA;AAAA,IACpE;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
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.1",
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.3.1",
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;;;;"}