@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.
Files changed (103) 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/package.json +35 -1
  70. package/src/api/routes/activity.ts +77 -0
  71. package/src/api/routes/admin-audit-helper.ts +18 -0
  72. package/src/api/routes/admin-audit.ts +67 -0
  73. package/src/api/routes/admin-backups.ts +134 -0
  74. package/src/api/routes/admin-compliance.ts +35 -0
  75. package/src/api/routes/admin-credits.ts +280 -0
  76. package/src/api/routes/admin-gpu.ts +202 -0
  77. package/src/api/routes/admin-inference.ts +109 -0
  78. package/src/api/routes/admin-marketplace.ts +233 -0
  79. package/src/api/routes/admin-migration.ts +61 -0
  80. package/src/api/routes/admin-notes.ts +145 -0
  81. package/src/api/routes/admin-onboarding.ts +62 -0
  82. package/src/api/routes/admin-rates.ts +462 -0
  83. package/src/api/routes/admin-recovery.ts +376 -0
  84. package/src/api/routes/admin-roles.ts +205 -0
  85. package/src/api/routes/audit.ts +106 -0
  86. package/src/api/routes/auth.ts +30 -0
  87. package/src/api/routes/channel-validate.ts +182 -0
  88. package/src/api/routes/fleet-events.ts +66 -0
  89. package/src/api/routes/friends-proxy.ts +94 -0
  90. package/src/api/routes/friends-types.ts +37 -0
  91. package/src/api/routes/health.test.ts +80 -0
  92. package/src/api/routes/health.ts +48 -0
  93. package/src/api/routes/incident-response.ts +159 -0
  94. package/src/api/routes/internal-gpu.ts +92 -0
  95. package/src/api/routes/internal-nodes.ts +157 -0
  96. package/src/api/routes/login-history.ts +28 -0
  97. package/src/api/routes/public-pricing.ts +36 -0
  98. package/src/api/routes/quota.ts +136 -0
  99. package/src/api/routes/secret-audit.ts +55 -0
  100. package/src/api/routes/secrets.ts +178 -0
  101. package/src/api/routes/tenant-keys.ts +178 -0
  102. package/src/api/routes/verify-email.ts +102 -0
  103. 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
+ }