@wopr-network/platform-core 1.13.0 → 1.13.1
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/dist/api/routes/activity.d.ts +9 -0
- package/dist/api/routes/activity.js +68 -0
- package/dist/api/routes/admin-audit-helper.d.ts +7 -0
- package/dist/api/routes/admin-audit-helper.js +13 -0
- package/dist/api/routes/admin-audit.d.ts +13 -0
- package/dist/api/routes/admin-audit.js +61 -0
- package/dist/api/routes/admin-backups.d.ts +19 -0
- package/dist/api/routes/admin-backups.js +116 -0
- package/dist/api/routes/admin-compliance.d.ts +9 -0
- package/dist/api/routes/admin-compliance.js +27 -0
- package/dist/api/routes/admin-credits.d.ts +9 -0
- package/dist/api/routes/admin-credits.js +255 -0
- package/dist/api/routes/admin-gpu.d.ts +46 -0
- package/dist/api/routes/admin-gpu.js +140 -0
- package/dist/api/routes/admin-inference.d.ts +16 -0
- package/dist/api/routes/admin-inference.js +98 -0
- package/dist/api/routes/admin-marketplace.d.ts +36 -0
- package/dist/api/routes/admin-marketplace.js +181 -0
- package/dist/api/routes/admin-migration.d.ts +10 -0
- package/dist/api/routes/admin-migration.js +46 -0
- package/dist/api/routes/admin-notes.d.ts +34 -0
- package/dist/api/routes/admin-notes.js +131 -0
- package/dist/api/routes/admin-onboarding.d.ts +7 -0
- package/dist/api/routes/admin-onboarding.js +49 -0
- package/dist/api/routes/admin-rates.d.ts +9 -0
- package/dist/api/routes/admin-rates.js +427 -0
- package/dist/api/routes/admin-recovery.d.ts +91 -0
- package/dist/api/routes/admin-recovery.js +246 -0
- package/dist/api/routes/admin-roles.d.ts +27 -0
- package/dist/api/routes/admin-roles.js +157 -0
- package/dist/api/routes/audit.d.ts +19 -0
- package/dist/api/routes/audit.js +95 -0
- package/dist/api/routes/auth.d.ts +19 -0
- package/dist/api/routes/auth.js +25 -0
- package/dist/api/routes/channel-validate.d.ts +11 -0
- package/dist/api/routes/channel-validate.js +148 -0
- package/dist/api/routes/fleet-events.d.ts +4 -0
- package/dist/api/routes/fleet-events.js +53 -0
- package/dist/api/routes/friends-proxy.d.ts +28 -0
- package/dist/api/routes/friends-proxy.js +63 -0
- package/dist/api/routes/friends-types.d.ts +34 -0
- package/dist/api/routes/friends-types.js +28 -0
- package/dist/api/routes/health.d.ts +14 -0
- package/dist/api/routes/health.js +32 -0
- package/dist/api/routes/health.test.d.ts +1 -0
- package/dist/api/routes/health.test.js +70 -0
- package/dist/api/routes/incident-response.d.ts +9 -0
- package/dist/api/routes/incident-response.js +148 -0
- package/dist/api/routes/internal-gpu.d.ts +12 -0
- package/dist/api/routes/internal-gpu.js +70 -0
- package/dist/api/routes/internal-nodes.d.ts +41 -0
- package/dist/api/routes/internal-nodes.js +105 -0
- package/dist/api/routes/login-history.d.ts +11 -0
- package/dist/api/routes/login-history.js +22 -0
- package/dist/api/routes/public-pricing.d.ts +9 -0
- package/dist/api/routes/public-pricing.js +32 -0
- package/dist/api/routes/quota.d.ts +8 -0
- package/dist/api/routes/quota.js +113 -0
- package/dist/api/routes/secret-audit.d.ts +12 -0
- package/dist/api/routes/secret-audit.js +41 -0
- package/dist/api/routes/secrets.d.ts +31 -0
- package/dist/api/routes/secrets.js +135 -0
- package/dist/api/routes/tenant-keys.d.ts +16 -0
- package/dist/api/routes/tenant-keys.js +142 -0
- package/dist/api/routes/verify-email.d.ts +19 -0
- package/dist/api/routes/verify-email.js +70 -0
- package/dist/api/routes/ws-auth.d.ts +21 -0
- package/dist/api/routes/ws-auth.js +24 -0
- package/package.json +35 -1
- package/src/api/routes/activity.ts +77 -0
- package/src/api/routes/admin-audit-helper.ts +18 -0
- package/src/api/routes/admin-audit.ts +67 -0
- package/src/api/routes/admin-backups.ts +134 -0
- package/src/api/routes/admin-compliance.ts +35 -0
- package/src/api/routes/admin-credits.ts +280 -0
- package/src/api/routes/admin-gpu.ts +202 -0
- package/src/api/routes/admin-inference.ts +109 -0
- package/src/api/routes/admin-marketplace.ts +233 -0
- package/src/api/routes/admin-migration.ts +61 -0
- package/src/api/routes/admin-notes.ts +145 -0
- package/src/api/routes/admin-onboarding.ts +62 -0
- package/src/api/routes/admin-rates.ts +462 -0
- package/src/api/routes/admin-recovery.ts +376 -0
- package/src/api/routes/admin-roles.ts +205 -0
- package/src/api/routes/audit.ts +106 -0
- package/src/api/routes/auth.ts +30 -0
- package/src/api/routes/channel-validate.ts +182 -0
- package/src/api/routes/fleet-events.ts +66 -0
- package/src/api/routes/friends-proxy.ts +94 -0
- package/src/api/routes/friends-types.ts +37 -0
- package/src/api/routes/health.test.ts +80 -0
- package/src/api/routes/health.ts +48 -0
- package/src/api/routes/incident-response.ts +159 -0
- package/src/api/routes/internal-gpu.ts +92 -0
- package/src/api/routes/internal-nodes.ts +157 -0
- package/src/api/routes/login-history.ts +28 -0
- package/src/api/routes/public-pricing.ts +36 -0
- package/src/api/routes/quota.ts +136 -0
- package/src/api/routes/secret-audit.ts +55 -0
- package/src/api/routes/secrets.ts +178 -0
- package/src/api/routes/tenant-keys.ts +178 -0
- package/src/api/routes/verify-email.ts +102 -0
- package/src/api/routes/ws-auth.ts +44 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
4
|
+
/**
|
|
5
|
+
* Create admin GPU node management routes.
|
|
6
|
+
* Static routes (/regions, /sizes) are registered BEFORE parameterized routes (/:nodeId).
|
|
7
|
+
*/
|
|
8
|
+
export function createAdminGpuRoutes(deps) {
|
|
9
|
+
const routes = new Hono();
|
|
10
|
+
routes.get("/", async (c) => {
|
|
11
|
+
const nodes = await deps.gpuNodeRepo().list();
|
|
12
|
+
return c.json({ success: true, nodes, count: nodes.length });
|
|
13
|
+
});
|
|
14
|
+
routes.post("/", async (c) => {
|
|
15
|
+
try {
|
|
16
|
+
const body = await c.req.json();
|
|
17
|
+
const parsed = z
|
|
18
|
+
.object({
|
|
19
|
+
region: z.string().min(1).max(20).optional(),
|
|
20
|
+
size: z.string().min(1).max(50).optional(),
|
|
21
|
+
name: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(63)
|
|
25
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9-]*$/)
|
|
26
|
+
.optional(),
|
|
27
|
+
})
|
|
28
|
+
.parse(body);
|
|
29
|
+
const provisioner = deps.gpuNodeProvisioner();
|
|
30
|
+
const result = await provisioner.provision(parsed);
|
|
31
|
+
safeAuditLog(deps.auditLogger, {
|
|
32
|
+
adminUser: c.get("user")?.id ?? "unknown",
|
|
33
|
+
action: "gpu.provision",
|
|
34
|
+
category: "config",
|
|
35
|
+
details: {
|
|
36
|
+
nodeId: result.nodeId,
|
|
37
|
+
dropletId: result.dropletId,
|
|
38
|
+
region: result.region,
|
|
39
|
+
size: result.size,
|
|
40
|
+
monthlyCostCents: result.monthlyCostCents,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return c.json({ success: true, node: result }, 201);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof z.ZodError) {
|
|
47
|
+
return c.json({ success: false, error: err.issues }, 400);
|
|
48
|
+
}
|
|
49
|
+
if (err instanceof Error && err.message.includes("DO_API_TOKEN")) {
|
|
50
|
+
return c.json({ success: false, error: "GPU provisioning not configured. Set DO_API_TOKEN environment variable." }, 503);
|
|
51
|
+
}
|
|
52
|
+
deps.logger?.error("GPU node provisioning failed", { err });
|
|
53
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
routes.get("/regions", async (c) => {
|
|
57
|
+
try {
|
|
58
|
+
const regions = await deps.doClient().listRegions();
|
|
59
|
+
return c.json({ success: true, regions });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err instanceof Error && err.message.includes("DO_API_TOKEN")) {
|
|
63
|
+
return c.json({ success: false, error: "GPU provisioning not configured. Set DO_API_TOKEN environment variable." }, 503);
|
|
64
|
+
}
|
|
65
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
routes.get("/sizes", async (c) => {
|
|
69
|
+
try {
|
|
70
|
+
const sizes = await deps.doClient().listSizes();
|
|
71
|
+
return c.json({ success: true, sizes });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof Error && err.message.includes("DO_API_TOKEN")) {
|
|
75
|
+
return c.json({ success: false, error: "GPU provisioning not configured. Set DO_API_TOKEN environment variable." }, 503);
|
|
76
|
+
}
|
|
77
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
routes.get("/:nodeId", async (c) => {
|
|
81
|
+
const nodeId = c.req.param("nodeId");
|
|
82
|
+
const node = await deps.gpuNodeRepo().getById(nodeId);
|
|
83
|
+
if (!node) {
|
|
84
|
+
return c.json({ success: false, error: "GPU node not found" }, 404);
|
|
85
|
+
}
|
|
86
|
+
return c.json({ success: true, node });
|
|
87
|
+
});
|
|
88
|
+
routes.delete("/:nodeId", async (c) => {
|
|
89
|
+
const nodeId = c.req.param("nodeId");
|
|
90
|
+
try {
|
|
91
|
+
const node = await deps.gpuNodeRepo().getById(nodeId);
|
|
92
|
+
if (!node) {
|
|
93
|
+
return c.json({ success: false, error: "GPU node not found" }, 404);
|
|
94
|
+
}
|
|
95
|
+
if (node.status === "provisioning" || node.status === "bootstrapping") {
|
|
96
|
+
return c.json({
|
|
97
|
+
success: false,
|
|
98
|
+
error: `Cannot destroy GPU node in ${node.status} state — wait until provisioning/bootstrapping completes`,
|
|
99
|
+
}, 409);
|
|
100
|
+
}
|
|
101
|
+
await deps.gpuNodeProvisioner().destroy(nodeId);
|
|
102
|
+
safeAuditLog(deps.auditLogger, {
|
|
103
|
+
adminUser: c.get("user")?.id ?? "unknown",
|
|
104
|
+
action: "gpu.destroy",
|
|
105
|
+
category: "config",
|
|
106
|
+
details: { nodeId },
|
|
107
|
+
});
|
|
108
|
+
return c.json({ success: true });
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
deps.logger?.error("GPU node destruction failed", { nodeId, err });
|
|
112
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
routes.post("/:nodeId/reboot", async (c) => {
|
|
116
|
+
const nodeId = c.req.param("nodeId");
|
|
117
|
+
try {
|
|
118
|
+
const node = await deps.gpuNodeRepo().getById(nodeId);
|
|
119
|
+
if (!node) {
|
|
120
|
+
return c.json({ success: false, error: "GPU node not found" }, 404);
|
|
121
|
+
}
|
|
122
|
+
if (!node.dropletId) {
|
|
123
|
+
return c.json({ success: false, error: "GPU node has no droplet assigned" }, 400);
|
|
124
|
+
}
|
|
125
|
+
await deps.doClient().rebootDroplet(Number(node.dropletId));
|
|
126
|
+
safeAuditLog(deps.auditLogger, {
|
|
127
|
+
adminUser: c.get("user")?.id ?? "unknown",
|
|
128
|
+
action: "gpu.reboot",
|
|
129
|
+
category: "config",
|
|
130
|
+
details: { nodeId, dropletId: node.dropletId },
|
|
131
|
+
});
|
|
132
|
+
return c.json({ success: true, message: `Reboot initiated for GPU node ${nodeId}` });
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
deps.logger?.error("GPU node reboot failed", { nodeId, err });
|
|
136
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return routes;
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import type { ISessionUsageRepository } from "../../inference/session-usage-repository.js";
|
|
4
|
+
/**
|
|
5
|
+
* Create admin inference dashboard routes.
|
|
6
|
+
*
|
|
7
|
+
* Routes:
|
|
8
|
+
* GET / — Summary: daily costs, session count, avg cost, cache hit rate
|
|
9
|
+
* GET /daily — Daily cost breakdown
|
|
10
|
+
* GET /pages — Per-page cost breakdown
|
|
11
|
+
* GET /cache — Cache hit rate
|
|
12
|
+
* GET /session/:sessionId — Per-session usage detail
|
|
13
|
+
*
|
|
14
|
+
* @param repoFactory - factory for the session usage repository (lazy init)
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAdminInferenceRoutes(repoFactory: () => ISessionUsageRepository): Hono<AuthEnv>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
/**
|
|
3
|
+
* Create admin inference dashboard routes.
|
|
4
|
+
*
|
|
5
|
+
* Routes:
|
|
6
|
+
* GET / — Summary: daily costs, session count, avg cost, cache hit rate
|
|
7
|
+
* GET /daily — Daily cost breakdown
|
|
8
|
+
* GET /pages — Per-page cost breakdown
|
|
9
|
+
* GET /cache — Cache hit rate
|
|
10
|
+
* GET /session/:sessionId — Per-session usage detail
|
|
11
|
+
*
|
|
12
|
+
* @param repoFactory - factory for the session usage repository (lazy init)
|
|
13
|
+
*/
|
|
14
|
+
export function createAdminInferenceRoutes(repoFactory) {
|
|
15
|
+
const routes = new Hono();
|
|
16
|
+
routes.get("/", async (c) => {
|
|
17
|
+
const repo = repoFactory();
|
|
18
|
+
const daysParam = c.req.query("days");
|
|
19
|
+
const days = Math.min(90, Math.max(1, Number.isNaN(Number(daysParam)) ? 7 : Number(daysParam)));
|
|
20
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
21
|
+
try {
|
|
22
|
+
const [dailyCosts, pageCosts, cacheHitRate] = await Promise.all([
|
|
23
|
+
repo.aggregateByDay(since),
|
|
24
|
+
repo.aggregateByPage(since),
|
|
25
|
+
repo.cacheHitRate(since),
|
|
26
|
+
]);
|
|
27
|
+
const totalCostUsd = dailyCosts.reduce((sum, d) => sum + d.totalCostUsd, 0);
|
|
28
|
+
const totalSessions = dailyCosts.reduce((sum, d) => sum + d.sessionCount, 0);
|
|
29
|
+
const avgCostPerSession = totalSessions > 0 ? totalCostUsd / totalSessions : 0;
|
|
30
|
+
return c.json({
|
|
31
|
+
period: { days, since },
|
|
32
|
+
summary: {
|
|
33
|
+
totalCostUsd,
|
|
34
|
+
totalSessions,
|
|
35
|
+
avgCostPerSessionUsd: avgCostPerSession,
|
|
36
|
+
cacheHitRate: cacheHitRate.hitRate,
|
|
37
|
+
},
|
|
38
|
+
dailyCosts,
|
|
39
|
+
pageCosts,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
routes.get("/daily", async (c) => {
|
|
47
|
+
const repo = repoFactory();
|
|
48
|
+
const daysParam = c.req.query("days");
|
|
49
|
+
const days = Math.min(90, Math.max(1, Number.isNaN(Number(daysParam)) ? 30 : Number(daysParam)));
|
|
50
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
51
|
+
try {
|
|
52
|
+
const dailyCosts = await repo.aggregateByDay(since);
|
|
53
|
+
return c.json({ days, dailyCosts });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
routes.get("/pages", async (c) => {
|
|
60
|
+
const repo = repoFactory();
|
|
61
|
+
const daysParam = c.req.query("days");
|
|
62
|
+
const days = Math.min(90, Math.max(1, Number.isNaN(Number(daysParam)) ? 7 : Number(daysParam)));
|
|
63
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
64
|
+
try {
|
|
65
|
+
const pageCosts = await repo.aggregateByPage(since);
|
|
66
|
+
return c.json({ days, pageCosts });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
routes.get("/cache", async (c) => {
|
|
73
|
+
const repo = repoFactory();
|
|
74
|
+
const daysParam = c.req.query("days");
|
|
75
|
+
const days = Math.min(90, Math.max(1, Number.isNaN(Number(daysParam)) ? 7 : Number(daysParam)));
|
|
76
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
77
|
+
try {
|
|
78
|
+
const cacheStats = await repo.cacheHitRate(since);
|
|
79
|
+
return c.json({ days, cacheStats });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
routes.get("/session/:sessionId", async (c) => {
|
|
86
|
+
const repo = repoFactory();
|
|
87
|
+
const sessionId = c.req.param("sessionId");
|
|
88
|
+
try {
|
|
89
|
+
const records = await repo.findBySessionId(sessionId);
|
|
90
|
+
const totalCostUsd = records.reduce((sum, r) => sum + r.costUsd, 0);
|
|
91
|
+
return c.json({ sessionId, totalCostUsd, records });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return routes;
|
|
98
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import type { IMarketplacePluginRepository } from "../../marketplace/marketplace-plugin-repository.js";
|
|
4
|
+
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
5
|
+
/** Optional volume installer interface for fire-and-forget plugin installation. */
|
|
6
|
+
export interface PluginVolumeInstaller {
|
|
7
|
+
installPluginToVolume(opts: {
|
|
8
|
+
pluginId: string;
|
|
9
|
+
npmPackage: string;
|
|
10
|
+
version: string;
|
|
11
|
+
volumePath: string;
|
|
12
|
+
repo: IMarketplacePluginRepository;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Optional npm discovery interface. */
|
|
16
|
+
export interface NpmPluginDiscoverer {
|
|
17
|
+
discoverNpmPlugins(opts: {
|
|
18
|
+
repo: IMarketplacePluginRepository;
|
|
19
|
+
notify: (msg: string) => void;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
discovered: number;
|
|
22
|
+
skipped: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export interface AdminMarketplaceDeps {
|
|
26
|
+
repoFactory: () => IMarketplacePluginRepository;
|
|
27
|
+
auditLogger?: () => AdminAuditLogger;
|
|
28
|
+
volumeInstaller?: () => PluginVolumeInstaller;
|
|
29
|
+
pluginVolumePath?: string;
|
|
30
|
+
discoverer?: () => NpmPluginDiscoverer;
|
|
31
|
+
logger?: {
|
|
32
|
+
info(msg: string): void;
|
|
33
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function createAdminMarketplaceRoutes(repoFactoryOrDeps: (() => IMarketplacePluginRepository) | AdminMarketplaceDeps, auditLogger?: () => AdminAuditLogger): Hono<AuthEnv>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
4
|
+
const addPluginSchema = z.object({
|
|
5
|
+
npmPackage: z.string().min(1),
|
|
6
|
+
version: z.string().min(1),
|
|
7
|
+
category: z.string().optional(),
|
|
8
|
+
notes: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
const updatePluginSchema = z.object({
|
|
11
|
+
enabled: z.boolean().optional(),
|
|
12
|
+
featured: z.boolean().optional(),
|
|
13
|
+
sortOrder: z.number().int().optional(),
|
|
14
|
+
category: z.string().nullable().optional(),
|
|
15
|
+
notes: z.string().nullable().optional(),
|
|
16
|
+
});
|
|
17
|
+
export function createAdminMarketplaceRoutes(repoFactoryOrDeps, auditLogger) {
|
|
18
|
+
const deps = typeof repoFactoryOrDeps === "function" ? { repoFactory: repoFactoryOrDeps, auditLogger } : repoFactoryOrDeps;
|
|
19
|
+
const routes = new Hono();
|
|
20
|
+
let _repo = null;
|
|
21
|
+
const repo = () => {
|
|
22
|
+
if (!_repo)
|
|
23
|
+
_repo = deps.repoFactory();
|
|
24
|
+
return _repo;
|
|
25
|
+
};
|
|
26
|
+
// GET /plugins — list all marketplace plugins
|
|
27
|
+
routes.get("/plugins", async (c) => {
|
|
28
|
+
return c.json(await repo().findAll());
|
|
29
|
+
});
|
|
30
|
+
// GET /queue — list plugins pending review (enabled = false)
|
|
31
|
+
routes.get("/queue", async (c) => {
|
|
32
|
+
return c.json(await repo().findPendingReview());
|
|
33
|
+
});
|
|
34
|
+
// POST /plugins — manually add a plugin by npm package name
|
|
35
|
+
routes.post("/plugins", async (c) => {
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await c.req.json();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
42
|
+
}
|
|
43
|
+
const parsed = addPluginSchema.safeParse(body);
|
|
44
|
+
if (!parsed.success) {
|
|
45
|
+
return c.json({ error: "Validation failed", details: parsed.error.flatten() }, 400);
|
|
46
|
+
}
|
|
47
|
+
const { npmPackage, version, category, notes } = parsed.data;
|
|
48
|
+
const existing = await repo().findById(npmPackage);
|
|
49
|
+
if (existing) {
|
|
50
|
+
return c.json({ error: "Plugin already exists" }, 409);
|
|
51
|
+
}
|
|
52
|
+
const plugin = await repo().insert({
|
|
53
|
+
pluginId: npmPackage,
|
|
54
|
+
npmPackage,
|
|
55
|
+
version,
|
|
56
|
+
category,
|
|
57
|
+
notes,
|
|
58
|
+
});
|
|
59
|
+
const user = c.get("user");
|
|
60
|
+
safeAuditLog(deps.auditLogger, {
|
|
61
|
+
adminUser: user?.id ?? "unknown",
|
|
62
|
+
action: "marketplace.plugin.create",
|
|
63
|
+
category: "config",
|
|
64
|
+
details: { pluginId: npmPackage, version },
|
|
65
|
+
outcome: "success",
|
|
66
|
+
});
|
|
67
|
+
// Fire-and-forget: install into shared volume
|
|
68
|
+
if (deps.volumeInstaller) {
|
|
69
|
+
const volumePath = deps.pluginVolumePath ?? "/data/plugins";
|
|
70
|
+
try {
|
|
71
|
+
const installer = deps.volumeInstaller();
|
|
72
|
+
installer
|
|
73
|
+
.installPluginToVolume({
|
|
74
|
+
pluginId: npmPackage,
|
|
75
|
+
npmPackage,
|
|
76
|
+
version,
|
|
77
|
+
volumePath,
|
|
78
|
+
repo: repo(),
|
|
79
|
+
})
|
|
80
|
+
.catch((err) => {
|
|
81
|
+
deps.logger?.error("Volume install trigger failed", { pluginId: npmPackage, err });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* volume installer unavailable — non-fatal */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return c.json(plugin, 201);
|
|
89
|
+
});
|
|
90
|
+
// PATCH /plugins/:id — update plugin (enable/disable, feature, sort, notes)
|
|
91
|
+
routes.patch("/plugins/:id", async (c) => {
|
|
92
|
+
const id = c.req.param("id");
|
|
93
|
+
const existing = await repo().findById(id);
|
|
94
|
+
if (!existing) {
|
|
95
|
+
return c.json({ error: "Plugin not found" }, 404);
|
|
96
|
+
}
|
|
97
|
+
let body;
|
|
98
|
+
try {
|
|
99
|
+
body = await c.req.json();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
103
|
+
}
|
|
104
|
+
const parsed = updatePluginSchema.safeParse(body);
|
|
105
|
+
if (!parsed.success) {
|
|
106
|
+
return c.json({ error: "Validation failed", details: parsed.error.flatten() }, 400);
|
|
107
|
+
}
|
|
108
|
+
const patch = { ...parsed.data };
|
|
109
|
+
if (parsed.data.enabled === true) {
|
|
110
|
+
const user = c.get("user");
|
|
111
|
+
if (user)
|
|
112
|
+
patch.enabledBy = user.id;
|
|
113
|
+
}
|
|
114
|
+
const updated = await repo().update(id, patch);
|
|
115
|
+
const user = c.get("user");
|
|
116
|
+
safeAuditLog(deps.auditLogger, {
|
|
117
|
+
adminUser: user?.id ?? "unknown",
|
|
118
|
+
action: "marketplace.plugin.update",
|
|
119
|
+
category: "config",
|
|
120
|
+
details: { pluginId: id, patch },
|
|
121
|
+
outcome: "success",
|
|
122
|
+
});
|
|
123
|
+
return c.json(updated);
|
|
124
|
+
});
|
|
125
|
+
// DELETE /plugins/:id — remove a plugin from the registry
|
|
126
|
+
routes.delete("/plugins/:id", async (c) => {
|
|
127
|
+
const id = c.req.param("id");
|
|
128
|
+
const existing = await repo().findById(id);
|
|
129
|
+
if (!existing) {
|
|
130
|
+
return c.json({ error: "Plugin not found" }, 404);
|
|
131
|
+
}
|
|
132
|
+
await repo().delete(id);
|
|
133
|
+
const user = c.get("user");
|
|
134
|
+
safeAuditLog(deps.auditLogger, {
|
|
135
|
+
adminUser: user?.id ?? "unknown",
|
|
136
|
+
action: "marketplace.plugin.delete",
|
|
137
|
+
category: "config",
|
|
138
|
+
details: { pluginId: id },
|
|
139
|
+
outcome: "success",
|
|
140
|
+
});
|
|
141
|
+
return c.body(null, 204);
|
|
142
|
+
});
|
|
143
|
+
// GET /plugins/:id/install-status — poll install progress
|
|
144
|
+
routes.get("/plugins/:id/install-status", async (c) => {
|
|
145
|
+
const id = c.req.param("id");
|
|
146
|
+
const plugin = await repo().findById(id);
|
|
147
|
+
if (!plugin) {
|
|
148
|
+
return c.json({ error: "Plugin not found" }, 404);
|
|
149
|
+
}
|
|
150
|
+
const status = plugin.installedAt ? "installed" : plugin.installError ? "failed" : "pending";
|
|
151
|
+
return c.json({
|
|
152
|
+
pluginId: plugin.pluginId,
|
|
153
|
+
status,
|
|
154
|
+
installedAt: plugin.installedAt,
|
|
155
|
+
installError: plugin.installError,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// POST /discover — trigger manual discovery run
|
|
159
|
+
routes.post("/discover", async (c) => {
|
|
160
|
+
if (!deps.discoverer) {
|
|
161
|
+
return c.json({ error: "Discovery not configured" }, 503);
|
|
162
|
+
}
|
|
163
|
+
const discoverer = deps.discoverer();
|
|
164
|
+
const result = await discoverer.discoverNpmPlugins({
|
|
165
|
+
repo: repo(),
|
|
166
|
+
notify: (msg) => {
|
|
167
|
+
deps.logger?.info(`[Marketplace] ${msg}`);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
const user = c.get("user");
|
|
171
|
+
safeAuditLog(deps.auditLogger, {
|
|
172
|
+
adminUser: user?.id ?? "unknown",
|
|
173
|
+
action: "marketplace.discovery.trigger",
|
|
174
|
+
category: "config",
|
|
175
|
+
details: { discovered: result.discovered, skipped: result.skipped },
|
|
176
|
+
outcome: "success",
|
|
177
|
+
});
|
|
178
|
+
return c.json(result);
|
|
179
|
+
});
|
|
180
|
+
return routes;
|
|
181
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
4
|
+
export interface MigrationOrchestrator {
|
|
5
|
+
migrate(botId: string, targetNodeId?: string): Promise<{
|
|
6
|
+
success: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createAdminMigrationRoutes(getOrchestrator: () => MigrationOrchestrator, auditLogger?: () => AdminAuditLogger): Hono<AuthEnv>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { logger } from "../../config/logger.js";
|
|
4
|
+
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
5
|
+
const migrateInputSchema = z.object({
|
|
6
|
+
targetNodeId: z.string().min(1).optional(),
|
|
7
|
+
});
|
|
8
|
+
export function createAdminMigrationRoutes(getOrchestrator, auditLogger) {
|
|
9
|
+
const routes = new Hono();
|
|
10
|
+
routes.post("/:botId", async (c) => {
|
|
11
|
+
const botId = c.req.param("botId");
|
|
12
|
+
let body = {};
|
|
13
|
+
try {
|
|
14
|
+
body = migrateInputSchema.parse(await c.req.json());
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// No body is fine — auto-select target
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const result = await getOrchestrator().migrate(botId, body.targetNodeId);
|
|
21
|
+
safeAuditLog(auditLogger, {
|
|
22
|
+
adminUser: c.get("user")?.id ?? "unknown",
|
|
23
|
+
action: "bot.migrate",
|
|
24
|
+
category: "config",
|
|
25
|
+
details: { botId, targetNodeId: body.targetNodeId, success: result.success },
|
|
26
|
+
outcome: result.success ? "success" : "failure",
|
|
27
|
+
});
|
|
28
|
+
if (result.success) {
|
|
29
|
+
return c.json({ success: true, result });
|
|
30
|
+
}
|
|
31
|
+
return c.json({ success: false, result }, 400);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.error("Migration failed", { botId, err });
|
|
35
|
+
safeAuditLog(auditLogger, {
|
|
36
|
+
adminUser: c.get("user")?.id ?? "unknown",
|
|
37
|
+
action: "bot.migrate",
|
|
38
|
+
category: "config",
|
|
39
|
+
details: { botId, targetNodeId: body.targetNodeId },
|
|
40
|
+
outcome: "failure",
|
|
41
|
+
});
|
|
42
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return routes;
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
4
|
+
/** Minimal interface for admin notes storage — implemented by concrete stores. */
|
|
5
|
+
export interface IAdminNotesRepository {
|
|
6
|
+
create(input: {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
authorId: string;
|
|
9
|
+
content: string;
|
|
10
|
+
isPinned: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
id: string;
|
|
13
|
+
}>;
|
|
14
|
+
list(filters: {
|
|
15
|
+
tenantId: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
entries: unknown[];
|
|
20
|
+
total: number;
|
|
21
|
+
}>;
|
|
22
|
+
update(noteId: string, tenantId: string, updates: {
|
|
23
|
+
content?: string;
|
|
24
|
+
isPinned?: boolean;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
} | null>;
|
|
28
|
+
delete(noteId: string, tenantId: string): Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create admin notes API routes.
|
|
32
|
+
* Pass a store factory and optional audit logger for DI.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createAdminNotesApiRoutes(storeFactory: () => IAdminNotesRepository, auditLogger?: () => AdminAuditLogger): Hono<AuthEnv>;
|