@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.
- package/config.d.ts +22 -0
- package/dist/database/TelemetryStore.cjs.js +84 -0
- package/dist/database/TelemetryStore.cjs.js.map +1 -0
- package/dist/plugin.cjs.js +24 -45
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/router.cjs.js +147 -202
- package/dist/router.cjs.js.map +1 -1
- package/dist/service/bodyResolver.cjs.js +56 -0
- package/dist/service/bodyResolver.cjs.js.map +1 -0
- package/dist/service/telemetryHash.cjs.js +14 -0
- package/dist/service/telemetryHash.cjs.js.map +1 -0
- package/dist/service/toResourceSummary.cjs.js +31 -0
- package/dist/service/toResourceSummary.cjs.js.map +1 -0
- package/migrations/001_init.js +13 -42
- package/migrations/002_install_count.js +7 -12
- package/migrations/003_install_paths.js +7 -14
- package/migrations/004_label.js +7 -12
- package/migrations/005_remove_unused_fields.js +6 -16
- package/migrations/006_resources_content.js +10 -13
- package/migrations/007_telemetry.js +28 -0
- package/migrations/008_drop_legacy_tables.js +24 -0
- package/migrations/009_telemetry_settings.js +25 -0
- package/package.json +18 -8
- package/.eslintrc.js +0 -1
- package/dev/index.ts +0 -8
- package/dist/database/AiAssetStore.cjs.js +0 -225
- package/dist/database/AiAssetStore.cjs.js.map +0 -1
- package/dist/service/AiAssetSyncService.cjs.js +0 -235
- package/dist/service/AiAssetSyncService.cjs.js.map +0 -1
- package/dist/service/AssetParser.cjs.js +0 -74
- package/dist/service/AssetParser.cjs.js.map +0 -1
- package/dist/service/McpServerService.cjs.js +0 -366
- package/dist/service/McpServerService.cjs.js.map +0 -1
- package/package.json-prepack +0 -71
- package/src/database/AiAssetStore.test.ts +0 -294
- package/src/database/AiAssetStore.ts +0 -256
- package/src/index.ts +0 -2
- package/src/plugin.ts +0 -84
- package/src/router.smoke.test.ts +0 -262
- package/src/router.ts +0 -338
- package/src/service/AiAssetSyncService.ts +0 -298
- package/src/service/AssetParser.test.ts +0 -329
- package/src/service/AssetParser.ts +0 -103
- package/src/service/McpServerService.ts +0 -464
- package/src/types.ts +0 -31
- package/tsconfig.json +0 -8
package/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
devAiHub?: {
|
|
3
|
+
/**
|
|
4
|
+
* Install telemetry (ADR-0007). The write path is always active — there
|
|
5
|
+
* is no feature flag — so a salt is always in play; the only question is
|
|
6
|
+
* where it comes from.
|
|
7
|
+
*/
|
|
8
|
+
telemetry?: {
|
|
9
|
+
/**
|
|
10
|
+
* Stable salt used to hash the caller identity recorded on telemetry
|
|
11
|
+
* events. Optional: when unset, the plugin generates a salt on first
|
|
12
|
+
* start and persists it in its own database, which keeps hashes stable
|
|
13
|
+
* across restarts. Set it explicitly to keep the salt outside the
|
|
14
|
+
* database it protects — and then it must not change across
|
|
15
|
+
* restarts/deploys, or historical per-day dedup keys become
|
|
16
|
+
* unrecoverable.
|
|
17
|
+
* @visibility secret
|
|
18
|
+
*/
|
|
19
|
+
salt?: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
5
|
+
|
|
6
|
+
const SALT_KEY = "actor_salt";
|
|
7
|
+
const ZERO_COUNTS = {
|
|
8
|
+
install: 0,
|
|
9
|
+
copy: 0,
|
|
10
|
+
download: 0,
|
|
11
|
+
view: 0
|
|
12
|
+
};
|
|
13
|
+
class TelemetryStore {
|
|
14
|
+
constructor(db) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
}
|
|
17
|
+
db;
|
|
18
|
+
static async create(options) {
|
|
19
|
+
const db = await options.database.getClient();
|
|
20
|
+
await db.migrate.latest({
|
|
21
|
+
directory: backendPluginApi.resolvePackagePath(
|
|
22
|
+
"@nospt/plugin-dev-ai-hub-backend",
|
|
23
|
+
"migrations"
|
|
24
|
+
),
|
|
25
|
+
loadExtensions: [".js"]
|
|
26
|
+
});
|
|
27
|
+
return new TelemetryStore(db);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The salt used to hash actor identity (ADR-0007). A configured value wins
|
|
31
|
+
* and is never persisted, so a deployment can keep the salt outside the
|
|
32
|
+
* database it protects. Otherwise the salt is generated once and stored,
|
|
33
|
+
* which is what makes it survive restarts — the property per-day dedup
|
|
34
|
+
* actually depends on. It is deliberately not required from config: a
|
|
35
|
+
* missing value used to fail the plugin's init and take the whole backend
|
|
36
|
+
* down with it.
|
|
37
|
+
*
|
|
38
|
+
* Concurrent replicas starting together race on the insert; the loser reads
|
|
39
|
+
* back the winner's value, so every replica ends up on the same salt.
|
|
40
|
+
*/
|
|
41
|
+
async resolveSalt(configured) {
|
|
42
|
+
if (configured) {
|
|
43
|
+
return configured;
|
|
44
|
+
}
|
|
45
|
+
const existing = await this.readSalt();
|
|
46
|
+
if (existing) {
|
|
47
|
+
return existing;
|
|
48
|
+
}
|
|
49
|
+
await this.db("telemetry_settings").insert({ key: SALT_KEY, value: node_crypto.randomBytes(32).toString("hex") }).onConflict("key").ignore();
|
|
50
|
+
const stored = await this.readSalt();
|
|
51
|
+
if (!stored) {
|
|
52
|
+
throw new Error("Failed to persist the telemetry actor salt");
|
|
53
|
+
}
|
|
54
|
+
return stored;
|
|
55
|
+
}
|
|
56
|
+
async readSalt() {
|
|
57
|
+
const row = await this.db("telemetry_settings").where("key", SALT_KEY).first("value");
|
|
58
|
+
return row?.value;
|
|
59
|
+
}
|
|
60
|
+
async record(event) {
|
|
61
|
+
const now = /* @__PURE__ */ new Date();
|
|
62
|
+
await this.db("telemetry_events").insert({
|
|
63
|
+
entity_ref: event.entityRef,
|
|
64
|
+
action: event.action,
|
|
65
|
+
actor_hash: event.actorHash,
|
|
66
|
+
tool: event.tool ?? null,
|
|
67
|
+
day: now.toISOString().slice(0, 10),
|
|
68
|
+
occurred_at: now.toISOString()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async getCounts(entityRef) {
|
|
72
|
+
const rows = await this.db("telemetry_events").where("entity_ref", entityRef).groupBy("action").select("action").count(
|
|
73
|
+
"id as count"
|
|
74
|
+
);
|
|
75
|
+
const counts = { ...ZERO_COUNTS };
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
counts[row.action] = Number(row.count);
|
|
78
|
+
}
|
|
79
|
+
return counts;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
exports.TelemetryStore = TelemetryStore;
|
|
84
|
+
//# sourceMappingURL=TelemetryStore.cjs.js.map
|
|
@@ -0,0 +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 counts on read for\n * this slice — per-(hash, day)-distinct dedup for `view` is a follow-up.\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 — the property per-day dedup\n * actually depends on. 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 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":";;;;;AAYA,MAAM,QAAA,GAAW,YAAA;AAEjB,MAAM,WAAA,GAA+B;AAAA,EACnC,OAAA,EAAS,CAAA;AAAA,EACT,IAAA,EAAM,CAAA;AAAA,EACN,QAAA,EAAU,CAAA;AAAA,EACV,IAAA,EAAM;AACR,CAAA;AAaO,MAAM,cAAA,CAAe;AAAA,EAClB,YAA6B,EAAA,EAAU;AAAV,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAAA,EAAW;AAAA,EAAX,EAAA;AAAA,EAErC,aAAa,OAAO,OAAA,EAEQ;AAC1B,IAAA,MAAM,EAAA,GAAK,MAAM,OAAA,CAAQ,QAAA,CAAS,SAAA,EAAU;AAC5C,IAAA,MAAM,EAAA,CAAG,QAAQ,MAAA,CAAO;AAAA,MACtB,SAAA,EAAWA,mCAAA;AAAA,QACT,kCAAA;AAAA,QACA;AAAA,OACF;AAAA,MACA,cAAA,EAAgB,CAAC,KAAK;AAAA,KACvB,CAAA;AACD,IAAA,OAAO,IAAI,eAAe,EAAE,CAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,YAAY,UAAA,EAAsC;AACtD,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,OAAO,UAAA;AAAA,IACT;AACA,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,QAAA,EAAS;AACrC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAO,QAAA;AAAA,IACT;AACA,IAAA,MAAM,IAAA,CAAK,GAAG,oBAAoB,CAAA,CAC/B,OAAO,EAAE,GAAA,EAAK,UAAU,KAAA,EAAOC,uBAAA,CAAY,EAAE,CAAA,CAAE,QAAA,CAAS,KAAK,CAAA,EAAG,EAChE,UAAA,CAAW,KAAK,EAChB,MAAA,EAAO;AACV,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,QAAA,EAAS;AACnC,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,IAC9D;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,MAAc,QAAA,GAAwC;AACpD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,EAAA,CAAG,oBAAoB,CAAA,CAC3C,KAAA,CAAM,KAAA,EAAO,QAAQ,CAAA,CACrB,KAAA,CAAqC,OAAO,CAAA;AAC/C,IAAA,OAAO,GAAA,EAAK,KAAA;AAAA,EACd;AAAA,EAEA,MAAM,OAAO,KAAA,EAA4C;AACvD,IAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,kBAAkB,CAAA,CAAE,MAAA,CAAO;AAAA,MACvC,YAAY,KAAA,CAAM,SAAA;AAAA,MAClB,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,YAAY,KAAA,CAAM,SAAA;AAAA,MAClB,IAAA,EAAM,MAAM,IAAA,IAAQ,IAAA;AAAA,MACpB,KAAK,GAAA,CAAI,WAAA,EAAY,CAAE,KAAA,CAAM,GAAG,EAAE,CAAA;AAAA,MAClC,WAAA,EAAa,IAAI,WAAA;AAAY,KAC9B,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,UAAU,SAAA,EAA6C;AAC3D,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,EAAA,CAAG,kBAAkB,CAAA,CAC1C,KAAA,CAAM,YAAA,EAAc,SAAS,EAC7B,OAAA,CAAQ,QAAQ,CAAA,CAChB,MAAA,CAAO,QAAQ,CAAA,CACf,KAAA;AAAA,MACC;AAAA,KACF;AAEF,IAAA,MAAM,MAAA,GAAS,EAAE,GAAG,WAAA,EAAY;AAChC,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA,GAAI,MAAA,CAAO,IAAI,KAAK,CAAA;AAAA,IACvC;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AACF;;;;"}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -1,66 +1,45 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var AiAssetSyncService = require('./service/AiAssetSyncService.cjs.js');
|
|
4
|
+
var pluginCatalogNode = require('@backstage/plugin-catalog-node');
|
|
5
|
+
var TelemetryStore = require('./database/TelemetryStore.cjs.js');
|
|
7
6
|
var router = require('./router.cjs.js');
|
|
8
7
|
|
|
9
8
|
const devAiHubPlugin = backendPluginApi.createBackendPlugin({
|
|
10
9
|
pluginId: "dev-ai-hub",
|
|
11
10
|
register(env) {
|
|
12
|
-
const externalProviders = [];
|
|
13
|
-
env.registerExtensionPoint(pluginDevAiHubNode.devAiHubProviderExtensionPoint, {
|
|
14
|
-
addProvider(provider) {
|
|
15
|
-
externalProviders.push(provider);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
11
|
env.registerInit({
|
|
19
12
|
deps: {
|
|
20
13
|
config: backendPluginApi.coreServices.rootConfig,
|
|
21
14
|
logger: backendPluginApi.coreServices.logger,
|
|
22
15
|
database: backendPluginApi.coreServices.database,
|
|
23
|
-
scheduler: backendPluginApi.coreServices.scheduler,
|
|
24
16
|
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
25
|
-
urlReader: backendPluginApi.coreServices.urlReader
|
|
17
|
+
urlReader: backendPluginApi.coreServices.urlReader,
|
|
18
|
+
httpAuth: backendPluginApi.coreServices.httpAuth,
|
|
19
|
+
catalog: pluginCatalogNode.catalogServiceRef
|
|
26
20
|
},
|
|
27
|
-
async init({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
hours: p.getOptionalNumber("schedule.timeout.hours")
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
filters: p.has("filters") ? {
|
|
45
|
-
tools: p.getOptionalStringArray("filters.tools"),
|
|
46
|
-
types: p.getOptionalStringArray("filters.types")
|
|
47
|
-
} : void 0
|
|
48
|
-
}));
|
|
49
|
-
const syncService = new AiAssetSyncService.AiAssetSyncService({
|
|
21
|
+
async init({
|
|
22
|
+
config,
|
|
23
|
+
logger,
|
|
24
|
+
database,
|
|
25
|
+
httpRouter,
|
|
26
|
+
urlReader,
|
|
27
|
+
httpAuth,
|
|
28
|
+
catalog
|
|
29
|
+
}) {
|
|
30
|
+
const telemetryStore = await TelemetryStore.TelemetryStore.create({ database });
|
|
31
|
+
const telemetrySalt = await telemetryStore.resolveSalt(
|
|
32
|
+
config.getOptionalString("devAiHub.telemetry.salt")
|
|
33
|
+
);
|
|
34
|
+
const router$1 = router.createRouter({
|
|
50
35
|
logger,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
telemetryStore,
|
|
37
|
+
telemetrySalt,
|
|
38
|
+
catalog,
|
|
39
|
+
httpAuth,
|
|
40
|
+
reader: urlReader
|
|
56
41
|
});
|
|
57
|
-
await syncService.start();
|
|
58
|
-
const router$1 = router.createRouter({ logger, store, syncService, providers });
|
|
59
42
|
httpRouter.use(router$1);
|
|
60
|
-
httpRouter.addAuthPolicy({ path: "/assets", allow: "unauthenticated" });
|
|
61
|
-
httpRouter.addAuthPolicy({ path: "/providers", allow: "unauthenticated" });
|
|
62
|
-
httpRouter.addAuthPolicy({ path: "/stats", allow: "unauthenticated" });
|
|
63
|
-
httpRouter.addAuthPolicy({ path: "/mcp", allow: "unauthenticated" });
|
|
64
43
|
}
|
|
65
44
|
});
|
|
66
45
|
}
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport {
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { TelemetryStore } from './database/TelemetryStore';\nimport { createRouter } from './router';\n\nexport const devAiHubPlugin = createBackendPlugin({\n pluginId: 'dev-ai-hub',\n register(env) {\n env.registerInit({\n deps: {\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n urlReader: coreServices.urlReader,\n httpAuth: coreServices.httpAuth,\n catalog: catalogServiceRef,\n },\n async init({\n config,\n logger,\n database,\n httpRouter,\n urlReader,\n httpAuth,\n catalog,\n }) {\n const telemetryStore = await TelemetryStore.create({ database });\n // Optional on purpose (#56): unset means \"generate and persist one\",\n // not \"fail\". Requiring it here took the whole backend down on any\n // deployment that had not configured this plugin.\n const telemetrySalt = await telemetryStore.resolveSalt(\n config.getOptionalString('devAiHub.telemetry.salt'),\n );\n\n const router = createRouter({\n logger,\n telemetryStore,\n telemetrySalt,\n catalog,\n httpAuth,\n reader: urlReader,\n });\n\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","catalogServiceRef","TelemetryStore","router","createRouter"],"mappings":";;;;;;;AAQO,MAAM,iBAAiBA,oCAAA,CAAoB;AAAA,EAChD,QAAA,EAAU,YAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,UAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,QACzB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,OAAA,EAASC;AAAA,OACX;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,SAAA;AAAA,QACA,QAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAM,iBAAiB,MAAMC,6BAAA,CAAe,MAAA,CAAO,EAAE,UAAU,CAAA;AAI/D,QAAA,MAAM,aAAA,GAAgB,MAAM,cAAA,CAAe,WAAA;AAAA,UACzC,MAAA,CAAO,kBAAkB,yBAAyB;AAAA,SACpD;AAEA,QAAA,MAAMC,WAASC,mBAAA,CAAa;AAAA,UAC1B,MAAA;AAAA,UACA,cAAA;AAAA,UACA,aAAA;AAAA,UACA,OAAA;AAAA,UACA,QAAA;AAAA,UACA,MAAA,EAAQ;AAAA,SACT,CAAA;AAED,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AAAA,MACvB;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var crypto = require('crypto');
|
|
4
3
|
var express = require('express');
|
|
5
4
|
var archiver = require('archiver');
|
|
6
|
-
var
|
|
7
|
-
var
|
|
5
|
+
var pluginDevAiHubCommon = require('@nospt/plugin-dev-ai-hub-common');
|
|
6
|
+
var toResourceSummary = require('./service/toResourceSummary.cjs.js');
|
|
7
|
+
var telemetryHash = require('./service/telemetryHash.cjs.js');
|
|
8
|
+
var bodyResolver = require('./service/bodyResolver.cjs.js');
|
|
8
9
|
|
|
9
10
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
11
|
|
|
@@ -12,232 +13,176 @@ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
|
12
13
|
var archiver__default = /*#__PURE__*/_interopDefaultCompat(archiver);
|
|
13
14
|
|
|
14
15
|
function createRouter(options) {
|
|
15
|
-
const {
|
|
16
|
+
const { telemetryStore, telemetrySalt, catalog, httpAuth, reader } = options;
|
|
16
17
|
const router = express__default.default.Router();
|
|
17
|
-
const mcpSessions = /* @__PURE__ */ new Map();
|
|
18
18
|
router.use(express__default.default.json());
|
|
19
|
-
router.get("/
|
|
19
|
+
router.get("/resources", async (req, res) => {
|
|
20
20
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
items,
|
|
33
|
-
totalCount,
|
|
34
|
-
page: filter.page,
|
|
35
|
-
pageSize: filter.pageSize
|
|
36
|
-
});
|
|
37
|
-
} catch (err) {
|
|
38
|
-
options.logger.error("GET /assets failed", err);
|
|
39
|
-
res.status(500).json({ error: "Internal server error" });
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
router.get("/assets/:id", async (req, res) => {
|
|
43
|
-
try {
|
|
44
|
-
const asset = await store.getAsset(req.params.id);
|
|
45
|
-
if (!asset) return res.status(404).json({ error: "Asset not found" });
|
|
46
|
-
return res.json(asset);
|
|
47
|
-
} catch (err) {
|
|
48
|
-
options.logger.error("GET /assets/:id failed", err);
|
|
49
|
-
return res.status(500).json({ error: "Internal server error" });
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
router.get("/assets/:id/raw", async (req, res) => {
|
|
53
|
-
try {
|
|
54
|
-
const asset = await store.getAsset(req.params.id);
|
|
55
|
-
if (!asset) return res.status(404).json({ error: "Asset not found" });
|
|
56
|
-
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
57
|
-
return res.send(asset.content);
|
|
58
|
-
} catch (err) {
|
|
59
|
-
options.logger.error("GET /assets/:id/raw failed", err);
|
|
60
|
-
return res.status(500).json({ error: "Internal server error" });
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
router.get("/assets/:id/download", async (req, res) => {
|
|
64
|
-
try {
|
|
65
|
-
const asset = await store.getAsset(req.params.id);
|
|
66
|
-
if (!asset) return res.status(404).json({ error: "Asset not found" });
|
|
67
|
-
const filename = `${asset.name.replace(/\s+/g, "-").toLowerCase()}`;
|
|
68
|
-
if (asset.type === "skill") {
|
|
69
|
-
res.setHeader("Content-Type", "application/zip");
|
|
70
|
-
res.setHeader(
|
|
71
|
-
"Content-Disposition",
|
|
72
|
-
`attachment; filename="${filename}.zip"`
|
|
73
|
-
);
|
|
74
|
-
const archive = archiver__default.default("zip");
|
|
75
|
-
archive.pipe(res);
|
|
76
|
-
archive.append(asset.content, { name: "SKILL.md" });
|
|
77
|
-
if (asset.resourcesContent) {
|
|
78
|
-
for (const [resourcePath, resourceContent] of Object.entries(asset.resourcesContent)) {
|
|
79
|
-
archive.append(resourceContent, { name: resourcePath });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
await archive.finalize();
|
|
83
|
-
} else {
|
|
84
|
-
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
85
|
-
res.setHeader(
|
|
86
|
-
"Content-Disposition",
|
|
87
|
-
`attachment; filename="${filename}.md"`
|
|
88
|
-
);
|
|
89
|
-
return res.send(asset.content);
|
|
21
|
+
const credentials = await httpAuth.credentials(req);
|
|
22
|
+
const { items: entities } = await catalog.getEntities(
|
|
23
|
+
{ filter: { kind: "AiResource" } },
|
|
24
|
+
{ credentials }
|
|
25
|
+
);
|
|
26
|
+
const items = entities.map(toResourceSummary.toResourceSummary).filter((s) => s !== void 0);
|
|
27
|
+
res.json({ items });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.name === "AuthenticationError") {
|
|
30
|
+
res.status(401).json({ error: "Authentication required" });
|
|
31
|
+
return;
|
|
90
32
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
options.logger.error("GET /assets/:id/download failed", err);
|
|
94
|
-
return res.status(500).json({ error: "Internal server error" });
|
|
33
|
+
options.logger.error(`Failed to list resources: ${error}`);
|
|
34
|
+
res.status(500).json({ error: "Failed to list resources" });
|
|
95
35
|
}
|
|
96
36
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return res.status(500).json({ error: "Internal server error" });
|
|
37
|
+
const safeFilename = (name) => name.replace(/[^\w.-]+/g, "_");
|
|
38
|
+
const sendBodyFile = (res, name, content, download) => {
|
|
39
|
+
res.setHeader("Content-Type", bodyResolver.contentTypeFor(name));
|
|
40
|
+
if (download) {
|
|
41
|
+
res.setHeader(
|
|
42
|
+
"Content-Disposition",
|
|
43
|
+
`attachment; filename="${safeFilename(name.split("/").pop() ?? name)}"`
|
|
44
|
+
);
|
|
106
45
|
}
|
|
107
|
-
|
|
108
|
-
|
|
46
|
+
res.send(content);
|
|
47
|
+
};
|
|
48
|
+
const handleBodyRequest = async (req, res, filename) => {
|
|
109
49
|
try {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const status = statusMap.get(p.id);
|
|
114
|
-
return {
|
|
115
|
-
id: p.id,
|
|
116
|
-
type: p.type,
|
|
117
|
-
target: p.target,
|
|
118
|
-
branch: p.branch,
|
|
119
|
-
lastSync: status?.lastSync,
|
|
120
|
-
lastCommit: status?.lastCommit,
|
|
121
|
-
status: status?.status ?? "idle",
|
|
122
|
-
error: status?.error,
|
|
123
|
-
assetCount: status?.assetCount ?? 0
|
|
124
|
-
};
|
|
50
|
+
const credentials = await httpAuth.credentials(req);
|
|
51
|
+
const entity = await catalog.getEntityByRef(req.params.ref, {
|
|
52
|
+
credentials
|
|
125
53
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
54
|
+
if (!entity) {
|
|
55
|
+
res.status(404).json({ error: "Resource not found" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const sourceLocation = entity.metadata.annotations?.[bodyResolver.SOURCE_LOCATION_ANNOTATION];
|
|
59
|
+
const target = sourceLocation && bodyResolver.parseUrlTarget(sourceLocation);
|
|
60
|
+
if (!target) {
|
|
61
|
+
res.status(404).json({ error: "Resource has no content location" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let body;
|
|
65
|
+
try {
|
|
66
|
+
body = await bodyResolver.resolveBody(reader, target);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.name === "NotFoundError") {
|
|
69
|
+
res.status(404).json({ error: "Resource content not found" });
|
|
70
|
+
return;
|
|
142
71
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return res.json({ message: "Sync triggered", providerId: provider.id });
|
|
155
|
-
} catch (err) {
|
|
156
|
-
options.logger.error("POST /providers/:id/sync failed", err);
|
|
157
|
-
return res.status(500).json({ error: "Internal server error" });
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
router.get("/stats", async (_req, res) => {
|
|
161
|
-
try {
|
|
162
|
-
const stats = await store.getStats();
|
|
163
|
-
res.json(stats);
|
|
164
|
-
} catch (err) {
|
|
165
|
-
options.logger.error("GET /stats failed", err);
|
|
166
|
-
res.status(500).json({ error: "Internal server error" });
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
router.post("/mcp", async (req, res) => {
|
|
170
|
-
try {
|
|
171
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
172
|
-
if (sessionId) {
|
|
173
|
-
const transport2 = mcpSessions.get(sessionId);
|
|
174
|
-
if (!transport2) {
|
|
175
|
-
res.status(404).json({ error: "MCP session not found" });
|
|
72
|
+
options.logger.error(
|
|
73
|
+
`Body fetch failed for ${req.params.ref}: ${error}`
|
|
74
|
+
);
|
|
75
|
+
res.status(502).json({ error: "Failed to fetch resource content from source" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const download = req.query.download === "true";
|
|
79
|
+
if (filename !== void 0) {
|
|
80
|
+
const file = body.kind === "tree" ? body.files.find((f) => f.path === filename) : void 0;
|
|
81
|
+
if (!file) {
|
|
82
|
+
res.status(404).json({ error: "File not found in resource body" });
|
|
176
83
|
return;
|
|
177
84
|
}
|
|
178
|
-
|
|
85
|
+
sendBodyFile(res, filename, await file.content(), download);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (body.kind === "file") {
|
|
89
|
+
sendBodyFile(res, body.name, body.content, download);
|
|
179
90
|
return;
|
|
180
91
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const transport = new streamableHttp_js.StreamableHTTPServerTransport({
|
|
186
|
-
sessionIdGenerator: () => newSessionId,
|
|
187
|
-
onsessioninitialized: (id) => {
|
|
188
|
-
mcpSessions.set(id, transport);
|
|
189
|
-
options.logger.debug(
|
|
190
|
-
`dev-ai-hub MCP: session ${id} opened (tool="${toolFilter || "all"}", provider="${providerFilter || "all"}", proactive=${proactiveEnabled})`
|
|
191
|
-
);
|
|
92
|
+
if (!download) {
|
|
93
|
+
if (!body.entryPath) {
|
|
94
|
+
res.status(404).json({ error: "Resource body has no entry file" });
|
|
95
|
+
return;
|
|
192
96
|
}
|
|
97
|
+
const entry = body.files.find((f) => f.path === body.entryPath);
|
|
98
|
+
sendBodyFile(res, body.entryPath, await entry.content(), false);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (body.files.length === 1) {
|
|
102
|
+
sendBodyFile(
|
|
103
|
+
res,
|
|
104
|
+
body.files[0].path,
|
|
105
|
+
await body.files[0].content(),
|
|
106
|
+
true
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
res.setHeader("Content-Type", "application/zip");
|
|
111
|
+
res.setHeader(
|
|
112
|
+
"Content-Disposition",
|
|
113
|
+
`attachment; filename="${safeFilename(entity.metadata.name)}.zip"`
|
|
114
|
+
);
|
|
115
|
+
const archive = archiver__default.default("zip");
|
|
116
|
+
archive.on("error", (error) => {
|
|
117
|
+
options.logger.error(
|
|
118
|
+
`Zip assembly failed for ${req.params.ref}: ${error}`
|
|
119
|
+
);
|
|
120
|
+
res.destroy(error);
|
|
193
121
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
122
|
+
archive.pipe(res);
|
|
123
|
+
for (const file of body.files) {
|
|
124
|
+
archive.append(await file.content(), { name: file.path });
|
|
125
|
+
}
|
|
126
|
+
await archive.finalize();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.name === "AuthenticationError") {
|
|
129
|
+
res.status(401).json({ error: "Authentication required" });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
options.logger.error(
|
|
133
|
+
`Body resolution failed for ${req.params.ref}: ${error}`
|
|
134
|
+
);
|
|
135
|
+
res.status(500).json({ error: "Failed to resolve resource body" });
|
|
204
136
|
}
|
|
205
|
-
}
|
|
206
|
-
router.get("/
|
|
137
|
+
};
|
|
138
|
+
router.get("/entity/:ref/raw", (req, res) => handleBodyRequest(req, res));
|
|
139
|
+
router.get(
|
|
140
|
+
"/entity/:ref/raw/:filename",
|
|
141
|
+
(req, res) => handleBodyRequest(req, res, req.params.filename)
|
|
142
|
+
);
|
|
143
|
+
router.post("/telemetry", async (req, res) => {
|
|
207
144
|
try {
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
210
|
-
res.status(400).json({ error:
|
|
145
|
+
const parsed = pluginDevAiHubCommon.TelemetryEventInputSchema.safeParse(req.body);
|
|
146
|
+
if (!parsed.success) {
|
|
147
|
+
res.status(400).json({ error: parsed.error.message });
|
|
211
148
|
return;
|
|
212
149
|
}
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
150
|
+
const credentials = await httpAuth.credentials(req);
|
|
151
|
+
const entity = await catalog.getEntityByRef(parsed.data.ref, {
|
|
152
|
+
credentials
|
|
153
|
+
});
|
|
154
|
+
if (!entity) {
|
|
155
|
+
res.status(404).json({ error: "Resource not found" });
|
|
216
156
|
return;
|
|
217
157
|
}
|
|
218
|
-
await
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
158
|
+
await telemetryStore.record({
|
|
159
|
+
entityRef: parsed.data.ref,
|
|
160
|
+
action: parsed.data.action,
|
|
161
|
+
actorHash: telemetryHash.hashActor(credentials, telemetrySalt),
|
|
162
|
+
tool: parsed.data.tool
|
|
163
|
+
});
|
|
164
|
+
res.sendStatus(204);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error.name === "AuthenticationError") {
|
|
167
|
+
res.status(401).json({ error: "Authentication required" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
options.logger.error(`POST /telemetry failed: ${error}`);
|
|
171
|
+
res.status(500).json({ error: "Failed to record telemetry event" });
|
|
222
172
|
}
|
|
223
173
|
});
|
|
224
|
-
router.
|
|
174
|
+
router.get("/telemetry/:ref", async (req, res) => {
|
|
225
175
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!transport) {
|
|
233
|
-
res.status(404).json({ error: "MCP session not found" });
|
|
176
|
+
await httpAuth.credentials(req);
|
|
177
|
+
const counts = await telemetryStore.getCounts(req.params.ref);
|
|
178
|
+
res.json(counts);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.name === "AuthenticationError") {
|
|
181
|
+
res.status(401).json({ error: "Authentication required" });
|
|
234
182
|
return;
|
|
235
183
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
} catch (err) {
|
|
239
|
-
options.logger.error("DELETE /mcp failed", err);
|
|
240
|
-
if (!res.headersSent) res.status(500).json({ error: "Internal server error" });
|
|
184
|
+
options.logger.error(`GET /telemetry/${req.params.ref} failed: ${error}`);
|
|
185
|
+
res.status(500).json({ error: "Failed to fetch telemetry counts" });
|
|
241
186
|
}
|
|
242
187
|
});
|
|
243
188
|
return router;
|