@nospt/plugin-dev-ai-hub-backend 0.2.4 → 0.3.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.
Files changed (46) hide show
  1. package/config.d.ts +22 -0
  2. package/dist/database/TelemetryStore.cjs.js +84 -0
  3. package/dist/database/TelemetryStore.cjs.js.map +1 -0
  4. package/dist/plugin.cjs.js +24 -45
  5. package/dist/plugin.cjs.js.map +1 -1
  6. package/dist/router.cjs.js +147 -202
  7. package/dist/router.cjs.js.map +1 -1
  8. package/dist/service/bodyResolver.cjs.js +56 -0
  9. package/dist/service/bodyResolver.cjs.js.map +1 -0
  10. package/dist/service/telemetryHash.cjs.js +14 -0
  11. package/dist/service/telemetryHash.cjs.js.map +1 -0
  12. package/dist/service/toResourceSummary.cjs.js +31 -0
  13. package/dist/service/toResourceSummary.cjs.js.map +1 -0
  14. package/migrations/001_init.js +13 -42
  15. package/migrations/002_install_count.js +7 -12
  16. package/migrations/003_install_paths.js +7 -14
  17. package/migrations/004_label.js +7 -12
  18. package/migrations/005_remove_unused_fields.js +6 -16
  19. package/migrations/006_resources_content.js +10 -13
  20. package/migrations/007_telemetry.js +28 -0
  21. package/migrations/008_drop_legacy_tables.js +24 -0
  22. package/migrations/009_telemetry_settings.js +25 -0
  23. package/package.json +18 -8
  24. package/.eslintrc.js +0 -1
  25. package/dev/index.ts +0 -8
  26. package/dist/database/AiAssetStore.cjs.js +0 -225
  27. package/dist/database/AiAssetStore.cjs.js.map +0 -1
  28. package/dist/service/AiAssetSyncService.cjs.js +0 -235
  29. package/dist/service/AiAssetSyncService.cjs.js.map +0 -1
  30. package/dist/service/AssetParser.cjs.js +0 -74
  31. package/dist/service/AssetParser.cjs.js.map +0 -1
  32. package/dist/service/McpServerService.cjs.js +0 -366
  33. package/dist/service/McpServerService.cjs.js.map +0 -1
  34. package/package.json-prepack +0 -71
  35. package/src/database/AiAssetStore.test.ts +0 -294
  36. package/src/database/AiAssetStore.ts +0 -256
  37. package/src/index.ts +0 -2
  38. package/src/plugin.ts +0 -84
  39. package/src/router.smoke.test.ts +0 -262
  40. package/src/router.ts +0 -338
  41. package/src/service/AiAssetSyncService.ts +0 -298
  42. package/src/service/AssetParser.test.ts +0 -329
  43. package/src/service/AssetParser.ts +0 -103
  44. package/src/service/McpServerService.ts +0 -464
  45. package/src/types.ts +0 -31
  46. package/tsconfig.json +0 -8
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import { randomUUID } from 'crypto';\nimport express from 'express';\nimport archiver from 'archiver';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport type { LoggerService } from '@backstage/backend-plugin-api';\nimport type { AiAssetStore } from './database/AiAssetStore';\nimport type { AiAssetSyncService } from './service/AiAssetSyncService';\nimport { createMcpServer } from './service/McpServerService';\nimport type { ProviderConfig, AssetListFilter } from './types';\nimport type { AssetType } from '@nospt/plugin-dev-ai-hub-common';\n\ninterface RouterOptions {\n logger: LoggerService;\n store: AiAssetStore;\n syncService: AiAssetSyncService;\n providers: ProviderConfig[];\n}\n\nexport function createRouter(options: RouterOptions): express.Router {\n const { store, syncService, providers } = options;\n const router = express.Router();\n\n /** Active MCP sessions: sessionId → transport */\n const mcpSessions = new Map<string, StreamableHTTPServerTransport>();\n\n router.use(express.json());\n\n // ── Assets ────────────────────────────────────────────────────────────────\n\n router.get('/assets', async (req, res) => {\n try {\n const filter: AssetListFilter = {\n type: req.query.type as AssetType | undefined,\n tool: req.query.tool as string | undefined,\n providerId: req.query.provider as string | undefined,\n search: req.query.search as string | undefined,\n tags: req.query.tags\n ? (req.query.tags as string).split(',').filter(Boolean)\n : undefined,\n page: req.query.page ? Number(req.query.page) : 1,\n pageSize: req.query.pageSize ? Number(req.query.pageSize) : 20,\n };\n\n const { items, totalCount } = await store.listAssets(filter);\n\n res.json({\n items,\n totalCount,\n page: filter.page,\n pageSize: filter.pageSize,\n });\n } catch (err) {\n options.logger.error('GET /assets failed', err as Error);\n res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n router.get('/assets/:id', async (req, res) => {\n try {\n const asset = await store.getAsset(req.params.id);\n if (!asset) return res.status(404).json({ error: 'Asset not found' });\n return res.json(asset);\n } catch (err) {\n options.logger.error('GET /assets/:id failed', err as Error);\n return res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n /** Returns the pure markdown content of the asset */\n router.get('/assets/:id/raw', async (req, res) => {\n try {\n const asset = await store.getAsset(req.params.id);\n if (!asset) return res.status(404).json({ error: 'Asset not found' });\n res.setHeader('Content-Type', 'text/markdown; charset=utf-8');\n return res.send(asset.content);\n } catch (err) {\n options.logger.error('GET /assets/:id/raw failed', err as Error);\n return res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n /**\n * For skills: returns a zip with the markdown + metadata about bundled resources.\n * For other types: returns the markdown as a text file.\n */\n router.get('/assets/:id/download', async (req, res) => {\n try {\n const asset = await store.getAsset(req.params.id);\n if (!asset) return res.status(404).json({ error: 'Asset not found' });\n\n const filename = `${asset.name.replace(/\\s+/g, '-').toLowerCase()}`;\n\n if (asset.type === 'skill') {\n res.setHeader('Content-Type', 'application/zip');\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${filename}.zip\"`,\n );\n const archive = archiver('zip');\n archive.pipe(res);\n archive.append(asset.content, { name: 'SKILL.md' });\n if (asset.resourcesContent) {\n for (const [resourcePath, resourceContent] of Object.entries(asset.resourcesContent)) {\n archive.append(resourceContent, { name: resourcePath });\n }\n }\n await archive.finalize();\n } else {\n res.setHeader('Content-Type', 'text/markdown; charset=utf-8');\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${filename}.md\"`,\n );\n return res.send(asset.content);\n }\n\n return undefined;\n } catch (err) {\n options.logger.error('GET /assets/:id/download failed', err as Error);\n return res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n router.post('/assets/:id/track-install', async (req, res) => {\n try {\n const asset = await store.getAsset(req.params.id);\n if (!asset) return res.status(404).json({ error: 'Asset not found' });\n await store.incrementInstallCount(req.params.id);\n return res.status(204).send();\n } catch (err) {\n options.logger.error('POST /assets/:id/track-install failed', err as Error);\n return res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n // ── Providers ─────────────────────────────────────────────────────────────\n\n router.get('/providers', async (_req, res) => {\n try {\n const statuses = await store.getAllSyncStatuses();\n const statusMap = new Map(statuses.map(s => [s.providerId, s]));\n\n const result = providers.map(p => {\n const status = statusMap.get(p.id);\n return {\n id: p.id,\n type: p.type,\n target: p.target,\n branch: p.branch,\n lastSync: status?.lastSync,\n lastCommit: status?.lastCommit,\n status: status?.status ?? 'idle',\n error: status?.error,\n assetCount: status?.assetCount ?? 0,\n };\n });\n\n res.json(result);\n } catch (err) {\n options.logger.error('GET /providers failed', err as Error);\n res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n router.get('/providers/:id/status', async (req, res) => {\n try {\n const provider = providers.find(p => p.id === req.params.id);\n if (!provider) return res.status(404).json({ error: 'Provider not found' });\n\n const status = await store.getSyncStatus(req.params.id);\n return res.json(\n status ?? {\n providerId: req.params.id,\n status: 'idle',\n assetCount: 0,\n },\n );\n } catch (err) {\n options.logger.error('GET /providers/:id/status failed', err as Error);\n return res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n router.post('/providers/:id/sync', async (req, res) => {\n try {\n const provider = providers.find(p => p.id === req.params.id);\n if (!provider) return res.status(404).json({ error: 'Provider not found' });\n\n // Fire and forget — respond immediately, sync runs in background\n setImmediate(() => syncService.syncProvider(provider));\n\n return res.json({ message: 'Sync triggered', providerId: provider.id });\n } catch (err) {\n options.logger.error('POST /providers/:id/sync failed', err as Error);\n return res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n // ── Stats ─────────────────────────────────────────────────────────────────\n\n router.get('/stats', async (_req, res) => {\n try {\n const stats = await store.getStats();\n res.json(stats);\n } catch (err) {\n options.logger.error('GET /stats failed', err as Error);\n res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n // ── MCP — StreamableHTTP transport ────────────────────────────────────────\n //\n // Clients connect to /mcp?tool=<ai-tool-name> using the MCP HTTP protocol.\n // The `tool` query parameter filters assets to those compatible with that\n // AI tool. Each session is stateful: the tool filter is captured at\n // initialization time and reused for the session lifetime.\n //\n // Client configuration example (.mcp.json / mcp_settings.json):\n //\n // {\n // \"mcpServers\": {\n // \"dev-ai-hub\": {\n // \"type\": \"http\",\n // \"url\": \"http://<backstage-host>:7007/api/dev-ai-hub/mcp?tool=claude-code\"\n // }\n // }\n // }\n //\n // Set AI_HUB_TOOL in your environment and substitute it into the URL, or\n // hard-code the tool name per workspace.\n\n /**\n * POST /mcp — Initialize a new MCP session or handle an existing one.\n * On first request (no mcp-session-id header), a new session is created\n * and its ID is returned in the response header.\n */\n router.post('/mcp', async (req, res) => {\n try {\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (sessionId) {\n const transport = mcpSessions.get(sessionId);\n if (!transport) {\n res.status(404).json({ error: 'MCP session not found' });\n return;\n }\n await transport.handleRequest(req, res, req.body);\n return;\n }\n\n // New session — extract filters from query params or headers\n const toolFilter =\n (req.query.tool as string | undefined) ??\n (req.headers['x-ai-hub-tool'] as string | undefined) ??\n '';\n const providerFilter =\n (req.query.provider as string | undefined) ??\n (req.headers['x-ai-hub-provider'] as string | undefined) ??\n '';\n // proactive defaults to false; pass ?proactive=true to enable check_for_assets + suggest_assets\n const proactiveEnabled =\n ((req.query.proactive as string | undefined) ??\n (req.headers['x-ai-hub-proactive'] as string | undefined) ??\n 'false') === 'true';\n\n const newSessionId = randomUUID();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => newSessionId,\n onsessioninitialized: id => {\n mcpSessions.set(id, transport);\n options.logger.debug(\n `dev-ai-hub MCP: session ${id} opened (tool=\"${toolFilter || 'all'}\", provider=\"${providerFilter || 'all'}\", proactive=${proactiveEnabled})`,\n );\n },\n });\n\n transport.onclose = () => {\n mcpSessions.delete(newSessionId);\n options.logger.debug(`dev-ai-hub MCP: session ${newSessionId} closed`);\n };\n\n const server = createMcpServer(store, toolFilter, providerFilter, providers, proactiveEnabled);\n await server.connect(transport);\n\n await transport.handleRequest(req, res, req.body);\n } catch (err) {\n options.logger.error('POST /mcp failed', err as Error);\n if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n /**\n * GET /mcp — SSE stream for server-to-client notifications on an existing session.\n */\n router.get('/mcp', async (req, res) => {\n try {\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n if (!sessionId) {\n res.status(400).json({ error: 'mcp-session-id header required' });\n return;\n }\n const transport = mcpSessions.get(sessionId);\n if (!transport) {\n res.status(404).json({ error: 'MCP session not found' });\n return;\n }\n await transport.handleRequest(req, res);\n } catch (err) {\n options.logger.error('GET /mcp failed', err as Error);\n if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n /**\n * DELETE /mcp — Terminate an existing MCP session.\n */\n router.delete('/mcp', async (req, res) => {\n try {\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n if (!sessionId) {\n res.status(400).json({ error: 'mcp-session-id header required' });\n return;\n }\n const transport = mcpSessions.get(sessionId);\n if (!transport) {\n res.status(404).json({ error: 'MCP session not found' });\n return;\n }\n await transport.handleRequest(req, res);\n mcpSessions.delete(sessionId);\n } catch (err) {\n options.logger.error('DELETE /mcp failed', err as Error);\n if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });\n }\n });\n\n return router;\n}\n"],"names":["express","archiver","transport","randomUUID","StreamableHTTPServerTransport","createMcpServer"],"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 { 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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"}
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const SOURCE_LOCATION_ANNOTATION = "backstage.io/source-location";
4
+ function parseUrlTarget(sourceLocation) {
5
+ return sourceLocation.startsWith("url:") ? sourceLocation.slice("url:".length) : void 0;
6
+ }
7
+ function dirNameOfTarget(target) {
8
+ const segments = new URL(target).pathname.split("/").filter(Boolean);
9
+ return segments[segments.length - 1] ?? "";
10
+ }
11
+ function pickEntryFile(paths, dirName) {
12
+ const markdown = paths.filter((p) => p.toLowerCase().endsWith(".md"));
13
+ if (markdown.length === 1) return markdown[0];
14
+ return markdown.find((p) => p === "SKILL.md") ?? markdown.find((p) => p === `${dirName}.md`);
15
+ }
16
+ function contentTypeFor(path) {
17
+ const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
18
+ switch (ext) {
19
+ case "md":
20
+ return "text/markdown; charset=utf-8";
21
+ case "json":
22
+ return "application/json; charset=utf-8";
23
+ case "yaml":
24
+ case "yml":
25
+ return "text/yaml; charset=utf-8";
26
+ default:
27
+ return "text/plain; charset=utf-8";
28
+ }
29
+ }
30
+ async function resolveBody(reader, target) {
31
+ if (!target.endsWith("/")) {
32
+ const response = await reader.readUrl(target);
33
+ const name = new URL(target).pathname.split("/").filter(Boolean).pop() ?? "body";
34
+ return { kind: "file", name, content: await response.buffer() };
35
+ }
36
+ const tree = await reader.readTree(target);
37
+ const files = await tree.files();
38
+ const dirName = dirNameOfTarget(target);
39
+ return {
40
+ kind: "tree",
41
+ dirName,
42
+ entryPath: pickEntryFile(
43
+ files.map((f) => f.path),
44
+ dirName
45
+ ),
46
+ files
47
+ };
48
+ }
49
+
50
+ exports.SOURCE_LOCATION_ANNOTATION = SOURCE_LOCATION_ANNOTATION;
51
+ exports.contentTypeFor = contentTypeFor;
52
+ exports.dirNameOfTarget = dirNameOfTarget;
53
+ exports.parseUrlTarget = parseUrlTarget;
54
+ exports.pickEntryFile = pickEntryFile;
55
+ exports.resolveBody = resolveBody;
56
+ //# sourceMappingURL=bodyResolver.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bodyResolver.cjs.js","sources":["../../src/service/bodyResolver.ts"],"sourcesContent":["import type { UrlReaderService } from '@backstage/backend-plugin-api';\n\n/**\n * The body resolver (ADR-0002): given an entity's `backstage.io/source-location`\n * target, fetch the body via UrlReader — a single targeted read, never Git\n * enumeration. A target ending in `/` is a directory-shaped (resource-bearing)\n * body read as one subtree; anything else is a single-file body.\n */\n\nexport const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location';\n\nexport interface BodyFile {\n path: string;\n content(): Promise<Buffer>;\n}\n\nexport type ResolvedBody =\n | { kind: 'file'; name: string; content: Buffer }\n | {\n kind: 'tree';\n dirName: string;\n /** Server-resolved entry file path, when one exists (CONTEXT.md: entry file). */\n entryPath?: string;\n files: BodyFile[];\n };\n\n/** Strip the `url:` location-ref prefix; undefined when it is another type. */\nexport function parseUrlTarget(sourceLocation: string): string | undefined {\n return sourceLocation.startsWith('url:')\n ? sourceLocation.slice('url:'.length)\n : undefined;\n}\n\n/** Last non-empty path segment of a directory target URL. */\nexport function dirNameOfTarget(target: string): string {\n const segments = new URL(target).pathname.split('/').filter(Boolean);\n return segments[segments.length - 1] ?? '';\n}\n\n/**\n * Pick the entry file of a directory body: the only `.md` in the tree, else\n * `SKILL.md`, else the `.md` named after the directory (CONTEXT.md).\n */\nexport function pickEntryFile(\n paths: string[],\n dirName: string,\n): string | undefined {\n const markdown = paths.filter(p => p.toLowerCase().endsWith('.md'));\n if (markdown.length === 1) return markdown[0];\n return (\n markdown.find(p => p === 'SKILL.md') ??\n markdown.find(p => p === `${dirName}.md`)\n );\n}\n\nexport function contentTypeFor(path: string): string {\n const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();\n switch (ext) {\n case 'md':\n return 'text/markdown; charset=utf-8';\n case 'json':\n return 'application/json; charset=utf-8';\n case 'yaml':\n case 'yml':\n return 'text/yaml; charset=utf-8';\n default:\n return 'text/plain; charset=utf-8';\n }\n}\n\nexport async function resolveBody(\n reader: UrlReaderService,\n target: string,\n): Promise<ResolvedBody> {\n if (!target.endsWith('/')) {\n const response = await reader.readUrl(target);\n const name =\n new URL(target).pathname.split('/').filter(Boolean).pop() ?? 'body';\n return { kind: 'file', name, content: await response.buffer() };\n }\n\n const tree = await reader.readTree(target);\n const files: BodyFile[] = await tree.files();\n const dirName = dirNameOfTarget(target);\n return {\n kind: 'tree',\n dirName,\n entryPath: pickEntryFile(\n files.map(f => f.path),\n dirName,\n ),\n files,\n };\n}\n"],"names":[],"mappings":";;AASO,MAAM,0BAAA,GAA6B;AAkBnC,SAAS,eAAe,cAAA,EAA4C;AACzE,EAAA,OAAO,cAAA,CAAe,WAAW,MAAM,CAAA,GACnC,eAAe,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA,GAClC,MAAA;AACN;AAGO,SAAS,gBAAgB,MAAA,EAAwB;AACtD,EAAA,MAAM,QAAA,GAAW,IAAI,GAAA,CAAI,MAAM,CAAA,CAAE,SAAS,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AACnE,EAAA,OAAO,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,CAAC,CAAA,IAAK,EAAA;AAC1C;AAMO,SAAS,aAAA,CACd,OACA,OAAA,EACoB;AACpB,EAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,CAAA,CAAA,KAAK,EAAE,WAAA,EAAY,CAAE,QAAA,CAAS,KAAK,CAAC,CAAA;AAClE,EAAA,IAAI,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG,OAAO,SAAS,CAAC,CAAA;AAC5C,EAAA,OACE,QAAA,CAAS,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,KAAM,UAAU,CAAA,IACnC,QAAA,CAAS,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,KAAM,CAAA,EAAG,OAAO,CAAA,GAAA,CAAK,CAAA;AAE5C;AAEO,SAAS,eAAe,IAAA,EAAsB;AACnD,EAAA,MAAM,GAAA,GAAM,KAAK,KAAA,CAAM,IAAA,CAAK,YAAY,GAAG,CAAA,GAAI,CAAC,CAAA,CAAE,WAAA,EAAY;AAC9D,EAAA,QAAQ,GAAA;AAAK,IACX,KAAK,IAAA;AACH,MAAA,OAAO,8BAAA;AAAA,IACT,KAAK,MAAA;AACH,MAAA,OAAO,iCAAA;AAAA,IACT,KAAK,MAAA;AAAA,IACL,KAAK,KAAA;AACH,MAAA,OAAO,0BAAA;AAAA,IACT;AACE,MAAA,OAAO,2BAAA;AAAA;AAEb;AAEA,eAAsB,WAAA,CACpB,QACA,MAAA,EACuB;AACvB,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,GAAG,CAAA,EAAG;AACzB,IAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA;AAC5C,IAAA,MAAM,IAAA,GACJ,IAAI,GAAA,CAAI,MAAM,CAAA,CAAE,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,KAAI,IAAK,MAAA;AAC/D,IAAA,OAAO,EAAE,MAAM,MAAA,EAAQ,IAAA,EAAM,SAAS,MAAM,QAAA,CAAS,QAAO,EAAE;AAAA,EAChE;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA;AACzC,EAAA,MAAM,KAAA,GAAoB,MAAM,IAAA,CAAK,KAAA,EAAM;AAC3C,EAAA,MAAM,OAAA,GAAU,gBAAgB,MAAM,CAAA;AACtC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAA;AAAA,IACN,OAAA;AAAA,IACA,SAAA,EAAW,aAAA;AAAA,MACT,KAAA,CAAM,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAI,CAAA;AAAA,MACrB;AAAA,KACF;AAAA,IACA;AAAA,GACF;AACF;;;;;;;;;"}
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var node_crypto = require('node:crypto');
4
+
5
+ function hashActor(credentials, salt) {
6
+ const principal = credentials.principal;
7
+ if (principal.type !== "user") {
8
+ return null;
9
+ }
10
+ return node_crypto.createHmac("sha256", salt).update(principal.userEntityRef).digest("hex");
11
+ }
12
+
13
+ exports.hashActor = hashActor;
14
+ //# sourceMappingURL=telemetryHash.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetryHash.cjs.js","sources":["../../src/service/telemetryHash.ts"],"sourcesContent":["import { createHmac } from 'node:crypto';\nimport type {\n BackstageCredentials,\n BackstageUserPrincipal,\n} from '@backstage/backend-plugin-api';\n\n/**\n * Salted one-way hash of the calling user's entity ref (ADR-0007). Only\n * `'user'` principals get a hash — service principals record `null`\n * (identity-less writes are still stored and counted raw, never rejected).\n */\nexport function hashActor(\n credentials: BackstageCredentials,\n salt: string,\n): string | null {\n const principal = credentials.principal as { type: string };\n if (principal.type !== 'user') {\n return null;\n }\n return createHmac('sha256', salt)\n .update((principal as BackstageUserPrincipal).userEntityRef)\n .digest('hex');\n}\n"],"names":["createHmac"],"mappings":";;;;AAWO,SAAS,SAAA,CACd,aACA,IAAA,EACe;AACf,EAAA,MAAM,YAAY,WAAA,CAAY,SAAA;AAC9B,EAAA,IAAI,SAAA,CAAU,SAAS,MAAA,EAAQ;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAOA,sBAAA,CAAW,UAAU,IAAI,CAAA,CAC7B,OAAQ,SAAA,CAAqC,aAAa,CAAA,CAC1D,MAAA,CAAO,KAAK,CAAA;AACjB;;;;"}
@@ -0,0 +1,31 @@
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
@@ -0,0 +1 @@
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;;;;"}
@@ -1,47 +1,18 @@
1
1
  /**
2
- * @param {import('knex').Knex} knex
2
+ * Tombstone. This migration created the legacy `ai_assets` and
3
+ * `ai_asset_sync_status` tables; 008 drops them and the code behind them is
4
+ * gone (ADR-0001). The file stays, deliberately empty, because knex validates
5
+ * that every migration recorded in `knex_migrations` is still present on disk
6
+ * and throws "The migration directory is corrupt" when one is not. Deleting
7
+ * these six files broke startup for every database that had already run them.
8
+ *
9
+ * Do not delete a migration file. Supersede it with a new one, as 008 does.
10
+ *
11
+ * @param {import('knex').Knex} _knex
3
12
  */
4
- exports.up = async function up(knex) {
5
- await knex.schema.createTable('ai_assets', table => {
6
- table.string('id').primary();
7
- table.string('provider_id').notNullable().index();
8
- table.string('name').notNullable().index();
9
- table.text('description').notNullable();
10
- table.string('type').notNullable().index();
11
- table.text('tools').notNullable(); // JSON array
12
- table.text('tags'); // JSON array
13
- table.string('author');
14
- table.text('icon');
15
- table.string('version');
16
- table.text('apply_to');
17
- table.string('model');
18
- table.text('content').notNullable();
19
- table.text('yaml_raw').notNullable();
20
- table.text('metadata'); // JSON object
21
- table.string('yaml_path').notNullable();
22
- table.string('md_path').notNullable();
23
- table.text('repo_url').notNullable();
24
- table.string('branch').notNullable().defaultTo('main');
25
- table.string('commit_sha');
26
- table.timestamp('synced_at').notNullable().defaultTo(knex.fn.now());
27
- table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
28
- table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
29
- });
30
-
31
- await knex.schema.createTable('ai_asset_sync_status', table => {
32
- table.string('provider_id').primary();
33
- table.timestamp('last_sync');
34
- table.string('last_commit');
35
- table.string('status').notNullable().defaultTo('idle');
36
- table.text('error');
37
- table.integer('asset_count').defaultTo(0);
38
- });
39
- };
13
+ exports.up = async function up(_knex) {};
40
14
 
41
15
  /**
42
- * @param {import('knex').Knex} knex
16
+ * @param {import('knex').Knex} _knex
43
17
  */
44
- exports.down = async function down(knex) {
45
- await knex.schema.dropTableIfExists('ai_asset_sync_status');
46
- await knex.schema.dropTableIfExists('ai_assets');
47
- };
18
+ exports.down = async function down(_knex) {};
@@ -1,17 +1,12 @@
1
1
  /**
2
- * @param {import('knex').Knex} knex
2
+ * Tombstone — see 001_init.js. Added an install counter to the legacy
3
+ * `ai_assets` table, since dropped by 008; telemetry (007) replaced it.
4
+ *
5
+ * @param {import('knex').Knex} _knex
3
6
  */
4
- exports.up = async function up(knex) {
5
- await knex.schema.alterTable('ai_assets', table => {
6
- table.integer('install_count').notNullable().defaultTo(0);
7
- });
8
- };
7
+ exports.up = async function up(_knex) {};
9
8
 
10
9
  /**
11
- * @param {import('knex').Knex} knex
10
+ * @param {import('knex').Knex} _knex
12
11
  */
13
- exports.down = async function down(knex) {
14
- await knex.schema.alterTable('ai_assets', table => {
15
- table.dropColumn('install_count');
16
- });
17
- };
12
+ exports.down = async function down(_knex) {};
@@ -1,19 +1,12 @@
1
1
  /**
2
- * @param {import('knex').Knex} knex
2
+ * Tombstone — see 001_init.js. Added per-tool install paths to the legacy
3
+ * `ai_assets` table, since dropped by 008.
4
+ *
5
+ * @param {import('knex').Knex} _knex
3
6
  */
4
- exports.up = async function up(knex) {
5
- await knex.schema.alterTable('ai_assets', table => {
6
- table.text('install_path').nullable();
7
- table.text('install_paths').nullable(); // JSON map: { tool: path }
8
- });
9
- };
7
+ exports.up = async function up(_knex) {};
10
8
 
11
9
  /**
12
- * @param {import('knex').Knex} knex
10
+ * @param {import('knex').Knex} _knex
13
11
  */
14
- exports.down = async function down(knex) {
15
- await knex.schema.alterTable('ai_assets', table => {
16
- table.dropColumn('install_path');
17
- table.dropColumn('install_paths');
18
- });
19
- };
12
+ exports.down = async function down(_knex) {};
@@ -1,17 +1,12 @@
1
1
  /**
2
- * @param {import('knex').Knex} knex
2
+ * Tombstone — see 001_init.js. Added a label column to the legacy `ai_assets`
3
+ * table, since dropped by 008.
4
+ *
5
+ * @param {import('knex').Knex} _knex
3
6
  */
4
- exports.up = async function up(knex) {
5
- await knex.schema.table('ai_assets', table => {
6
- table.string('label').nullable();
7
- });
8
- };
7
+ exports.up = async function up(_knex) {};
9
8
 
10
9
  /**
11
- * @param {import('knex').Knex} knex
10
+ * @param {import('knex').Knex} _knex
12
11
  */
13
- exports.down = async function down(knex) {
14
- await knex.schema.table('ai_assets', table => {
15
- table.dropColumn('label');
16
- });
17
- };
12
+ exports.down = async function down(_knex) {};
@@ -1,22 +1,12 @@
1
1
  /**
2
- * Remove apply_to and model columns — these fields belong in the .md content,
3
- * not in the envelope. Only `resources` (used for skill zip downloads) stays.
2
+ * Tombstone — see 001_init.js. Dropped unused columns from the legacy
3
+ * `ai_assets` table, since dropped entirely by 008.
4
4
  *
5
- * @param {import('knex').Knex} knex
5
+ * @param {import('knex').Knex} _knex
6
6
  */
7
- exports.up = async function up(knex) {
8
- await knex.schema.table('ai_assets', table => {
9
- table.dropColumn('apply_to');
10
- table.dropColumn('model');
11
- });
12
- };
7
+ exports.up = async function up(_knex) {};
13
8
 
14
9
  /**
15
- * @param {import('knex').Knex} knex
10
+ * @param {import('knex').Knex} _knex
16
11
  */
17
- exports.down = async function down(knex) {
18
- await knex.schema.table('ai_assets', table => {
19
- table.text('apply_to');
20
- table.string('model');
21
- });
22
- };
12
+ exports.down = async function down(_knex) {};
@@ -1,16 +1,13 @@
1
1
  /**
2
- * Add resources_content column to ai_assets.
3
- * Stores the content of bundled resource files for skills as JSON: { path: content }.
2
+ * Tombstone — see 001_init.js. Added resource-content storage to the legacy
3
+ * `ai_assets` table, since dropped by 008. Bodies now resolve on demand from
4
+ * each entity's source-location and are never stored (ADR-0001/0002).
5
+ *
6
+ * @param {import('knex').Knex} _knex
4
7
  */
8
+ exports.up = async function up(_knex) {};
5
9
 
6
- exports.up = async function up(knex) {
7
- await knex.schema.table('ai_assets', table => {
8
- table.text('resources_content').nullable();
9
- });
10
- };
11
-
12
- exports.down = async function down(knex) {
13
- await knex.schema.table('ai_assets', table => {
14
- table.dropColumn('resources_content');
15
- });
16
- };
10
+ /**
11
+ * @param {import('knex').Knex} _knex
12
+ */
13
+ exports.down = async function down(_knex) {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Telemetry event log (ADR-0007): one row per install/copy/download/view
3
+ * event, store-all — no write-time dedup or unique constraint. `day` is the
4
+ * UTC calendar day computed by the application at write time, not derived
5
+ * from `occurred_at` at read time, so dedup queries stay identical across
6
+ * sqlite (dev) and Postgres (prod) without DB-specific date functions.
7
+ *
8
+ * @param {import('knex').Knex} knex
9
+ */
10
+ exports.up = async function up(knex) {
11
+ await knex.schema.createTable('telemetry_events', table => {
12
+ table.increments('id').primary();
13
+ table.string('entity_ref').notNullable();
14
+ table.string('action').notNullable();
15
+ table.string('actor_hash').nullable();
16
+ table.string('tool').nullable();
17
+ table.string('day').notNullable();
18
+ table.timestamp('occurred_at').notNullable().defaultTo(knex.fn.now());
19
+ table.index(['entity_ref', 'action']);
20
+ });
21
+ };
22
+
23
+ /**
24
+ * @param {import('knex').Knex} knex
25
+ */
26
+ exports.down = async function down(knex) {
27
+ await knex.schema.dropTableIfExists('telemetry_events');
28
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tear down the legacy asset-store silo (issue #34). `ai_assets` and
3
+ * `ai_asset_sync_status` were created by 001_init.js (since-deleted); every
4
+ * capability they backed is now served from the catalog (ADR-0001). Nothing
5
+ * else depends on these tables, including `telemetry_events` (007).
6
+ *
7
+ * This is irreversible: recreating the exact legacy schema in `down()`
8
+ * would resurrect dead code paths that no longer exist.
9
+ *
10
+ * @param {import('knex').Knex} knex
11
+ */
12
+ exports.up = async function up(knex) {
13
+ await knex.schema.dropTableIfExists('ai_asset_sync_status');
14
+ await knex.schema.dropTableIfExists('ai_assets');
15
+ };
16
+
17
+ /**
18
+ * @param {import('knex').Knex} knex
19
+ */
20
+ exports.down = async function down() {
21
+ throw new Error(
22
+ '008_drop_legacy_tables is irreversible — the legacy asset-store schema is gone from the codebase.',
23
+ );
24
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Plugin-owned settings, holding the telemetry actor salt (ADR-0007, issue
3
+ * #56). The salt must be *stable* — a per-process value breaks per-day dedup
4
+ * across restarts — but it does not have to come from config: generating it
5
+ * once and persisting it here gives stability without making a missing config
6
+ * value fail the backend's startup.
7
+ *
8
+ * A key/value shape rather than a single-column table so a second plugin-level
9
+ * setting does not need a migration of its own.
10
+ *
11
+ * @param {import('knex').Knex} knex
12
+ */
13
+ exports.up = async function up(knex) {
14
+ await knex.schema.createTable('telemetry_settings', table => {
15
+ table.string('key').primary();
16
+ table.string('value').notNullable();
17
+ });
18
+ };
19
+
20
+ /**
21
+ * @param {import('knex').Knex} knex
22
+ */
23
+ exports.down = async function down(knex) {
24
+ await knex.schema.dropTableIfExists('telemetry_settings');
25
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nospt/plugin-dev-ai-hub-backend",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Backend plugin for Dev AI Hub",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.cjs.js",
@@ -28,6 +28,12 @@
28
28
  ]
29
29
  }
30
30
  },
31
+ "files": [
32
+ "dist",
33
+ "migrations",
34
+ "config.d.ts"
35
+ ],
36
+ "configSchema": "config.d.ts",
31
37
  "backstage": {
32
38
  "role": "backend-plugin",
33
39
  "pluginId": "dev-ai-hub",
@@ -52,19 +58,23 @@
52
58
  "tsc": "tsc --noEmit"
53
59
  },
54
60
  "dependencies": {
55
- "@backstage/backend-plugin-api": "^1.7.0",
56
- "@modelcontextprotocol/sdk": "^1.5.0",
57
- "@nospt/plugin-dev-ai-hub-common": "^0.2.4",
58
- "@nospt/plugin-dev-ai-hub-node": "^0.2.4",
61
+ "@backstage/backend-plugin-api": "^1.9.1",
62
+ "@backstage/catalog-model": "^1.9.0",
63
+ "@backstage/plugin-catalog-node": "^2.2.2",
64
+ "@nospt/plugin-dev-ai-hub-common": "^0.3.0",
59
65
  "archiver": "^6.0.0",
60
66
  "express": "^5.2.1",
61
67
  "js-yaml": "^4.1.0",
62
68
  "zod": "^3.22.0"
63
69
  },
64
70
  "devDependencies": {
65
- "@backstage/backend-defaults": "^0.17.0",
66
- "@backstage/backend-test-utils": "^1.11.0",
67
- "@backstage/cli": "^0.36.1",
71
+ "@backstage/backend-defaults": "^0.17.1",
72
+ "@backstage/backend-test-utils": "^1.11.3",
73
+ "@backstage/cli": "^0.36.2",
74
+ "@backstage/plugin-auth-backend": "0.29.0",
75
+ "@backstage/plugin-auth-backend-module-guest-provider": "0.2.10",
76
+ "@backstage/plugin-catalog-backend": "^3.8.0",
77
+ "@backstage/plugin-catalog-backend-module-ai-model": "^0.1.1",
68
78
  "@types/archiver": "^6.0.0",
69
79
  "@types/express": "^4.17.21",
70
80
  "@types/jest": "^29.0.0",
package/.eslintrc.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
package/dev/index.ts DELETED
@@ -1,8 +0,0 @@
1
- import { createBackend } from '@backstage/backend-defaults';
2
-
3
- const backend = createBackend();
4
-
5
- // The plugin registers its own routes, database migrations, and sync scheduler
6
- backend.add(import('../src'));
7
-
8
- backend.start();