@wopr-network/platform-core 1.13.0 → 1.13.2
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,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
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
4
|
+
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
5
|
+
|
|
6
|
+
/** Minimal interface for admin notes storage — implemented by concrete stores. */
|
|
7
|
+
export interface IAdminNotesRepository {
|
|
8
|
+
create(input: { tenantId: string; authorId: string; content: string; isPinned: boolean }): Promise<{ id: string }>;
|
|
9
|
+
list(filters: { tenantId: string; limit?: number; offset?: number }): Promise<{ entries: unknown[]; total: number }>;
|
|
10
|
+
update(
|
|
11
|
+
noteId: string,
|
|
12
|
+
tenantId: string,
|
|
13
|
+
updates: { content?: string; isPinned?: boolean },
|
|
14
|
+
): Promise<{ id: string } | null>;
|
|
15
|
+
delete(noteId: string, tenantId: string): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseIntParam(value: string | undefined): number | undefined {
|
|
19
|
+
if (value == null) return undefined;
|
|
20
|
+
const n = Number(value);
|
|
21
|
+
return Number.isFinite(n) ? n : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create admin notes API routes.
|
|
26
|
+
* Pass a store factory and optional audit logger for DI.
|
|
27
|
+
*/
|
|
28
|
+
export function createAdminNotesApiRoutes(
|
|
29
|
+
storeFactory: () => IAdminNotesRepository,
|
|
30
|
+
auditLogger?: () => AdminAuditLogger,
|
|
31
|
+
): Hono<AuthEnv> {
|
|
32
|
+
const routes = new Hono<AuthEnv>();
|
|
33
|
+
|
|
34
|
+
// GET /:tenantId -- list notes
|
|
35
|
+
routes.get("/:tenantId", async (c) => {
|
|
36
|
+
const store = storeFactory();
|
|
37
|
+
const tenantId = c.req.param("tenantId");
|
|
38
|
+
const filters = {
|
|
39
|
+
tenantId,
|
|
40
|
+
limit: parseIntParam(c.req.query("limit")),
|
|
41
|
+
offset: parseIntParam(c.req.query("offset")),
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const result = await store.list(filters);
|
|
45
|
+
return c.json(result);
|
|
46
|
+
} catch {
|
|
47
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// POST /:tenantId -- create note
|
|
52
|
+
routes.post("/:tenantId", async (c) => {
|
|
53
|
+
const store = storeFactory();
|
|
54
|
+
const tenantId = c.req.param("tenantId");
|
|
55
|
+
let body: Record<string, unknown>;
|
|
56
|
+
try {
|
|
57
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
58
|
+
} catch {
|
|
59
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
60
|
+
}
|
|
61
|
+
const content = body.content;
|
|
62
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
63
|
+
return c.json({ error: "content is required and must be non-empty" }, 400);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const user = c.get("user");
|
|
67
|
+
const note = await store.create({
|
|
68
|
+
tenantId,
|
|
69
|
+
authorId: user?.id ?? "unknown",
|
|
70
|
+
content,
|
|
71
|
+
isPinned: body.isPinned === true,
|
|
72
|
+
});
|
|
73
|
+
safeAuditLog(auditLogger, {
|
|
74
|
+
adminUser: user?.id ?? "unknown",
|
|
75
|
+
action: "note.create",
|
|
76
|
+
category: "support",
|
|
77
|
+
targetTenant: tenantId,
|
|
78
|
+
details: { noteId: note.id },
|
|
79
|
+
outcome: "success",
|
|
80
|
+
});
|
|
81
|
+
return c.json(note, 201);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// PATCH /:tenantId/:noteId -- update note
|
|
88
|
+
routes.patch("/:tenantId/:noteId", async (c) => {
|
|
89
|
+
const store = storeFactory();
|
|
90
|
+
const tenantId = c.req.param("tenantId");
|
|
91
|
+
const noteId = c.req.param("noteId");
|
|
92
|
+
let body: Record<string, unknown>;
|
|
93
|
+
try {
|
|
94
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
95
|
+
} catch {
|
|
96
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
97
|
+
}
|
|
98
|
+
const updates: { content?: string; isPinned?: boolean } = {};
|
|
99
|
+
if (typeof body.content === "string") updates.content = body.content;
|
|
100
|
+
if (typeof body.isPinned === "boolean") updates.isPinned = body.isPinned;
|
|
101
|
+
try {
|
|
102
|
+
const note = await store.update(noteId, tenantId, updates);
|
|
103
|
+
if (note === null) {
|
|
104
|
+
return c.json({ error: "Forbidden" }, 403);
|
|
105
|
+
}
|
|
106
|
+
const user = c.get("user");
|
|
107
|
+
safeAuditLog(auditLogger, {
|
|
108
|
+
adminUser: user?.id ?? "unknown",
|
|
109
|
+
action: "note.update",
|
|
110
|
+
category: "support",
|
|
111
|
+
targetTenant: tenantId,
|
|
112
|
+
details: { noteId, hasContentChange: !!updates.content, hasPinChange: updates.isPinned !== undefined },
|
|
113
|
+
outcome: "success",
|
|
114
|
+
});
|
|
115
|
+
return c.json(note);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// DELETE /:tenantId/:noteId -- delete note
|
|
122
|
+
routes.delete("/:tenantId/:noteId", async (c) => {
|
|
123
|
+
const store = storeFactory();
|
|
124
|
+
const tenantId = c.req.param("tenantId");
|
|
125
|
+
const noteId = c.req.param("noteId");
|
|
126
|
+
try {
|
|
127
|
+
const deleted = await store.delete(noteId, tenantId);
|
|
128
|
+
if (!deleted) return c.json({ error: "Forbidden" }, 403);
|
|
129
|
+
const user = c.get("user");
|
|
130
|
+
safeAuditLog(auditLogger, {
|
|
131
|
+
adminUser: user?.id ?? "unknown",
|
|
132
|
+
action: "note.delete",
|
|
133
|
+
category: "support",
|
|
134
|
+
targetTenant: tenantId,
|
|
135
|
+
details: { noteId },
|
|
136
|
+
outcome: "success",
|
|
137
|
+
});
|
|
138
|
+
return c.json({ ok: true });
|
|
139
|
+
} catch {
|
|
140
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return routes;
|
|
145
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
+
import type { IOnboardingScriptRepository } from "../../onboarding/drizzle-onboarding-script-repository.js";
|
|
4
|
+
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
5
|
+
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
6
|
+
|
|
7
|
+
type RepoFactory = () => IOnboardingScriptRepository;
|
|
8
|
+
|
|
9
|
+
export function createAdminOnboardingRoutes(getRepo: RepoFactory, auditLogger?: () => AdminAuditLogger): Hono<AuthEnv> {
|
|
10
|
+
const routes = new Hono<AuthEnv>();
|
|
11
|
+
|
|
12
|
+
routes.get("/current", async (c) => {
|
|
13
|
+
const repo = getRepo();
|
|
14
|
+
const script = await repo.findCurrent();
|
|
15
|
+
if (!script) {
|
|
16
|
+
return c.json({ error: "No onboarding script found" }, 404);
|
|
17
|
+
}
|
|
18
|
+
return c.json(script);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
routes.get("/history", async (c) => {
|
|
22
|
+
const repo = getRepo();
|
|
23
|
+
const limitParam = c.req.query("limit");
|
|
24
|
+
const limit = limitParam ? Math.min(50, Math.max(1, Number(limitParam) || 10)) : 10;
|
|
25
|
+
const history = await repo.findHistory(limit);
|
|
26
|
+
return c.json(history);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
routes.post("/", async (c) => {
|
|
30
|
+
const repo = getRepo();
|
|
31
|
+
let body: Record<string, unknown>;
|
|
32
|
+
try {
|
|
33
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
34
|
+
} catch {
|
|
35
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const content = body.content;
|
|
39
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
40
|
+
return c.json({ error: "content is required and must be non-empty" }, 400);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const user = c.get("user");
|
|
44
|
+
const adminUser = (user as { id?: string } | undefined)?.id ?? "unknown";
|
|
45
|
+
const script = await repo.insert({
|
|
46
|
+
content,
|
|
47
|
+
updatedBy: adminUser !== "unknown" ? adminUser : null,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
safeAuditLog(auditLogger, {
|
|
51
|
+
adminUser,
|
|
52
|
+
action: "onboarding.script_updated",
|
|
53
|
+
category: "config",
|
|
54
|
+
details: { version: script.version },
|
|
55
|
+
outcome: "success",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return c.json(script, 201);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return routes;
|
|
62
|
+
}
|