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