@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.
- 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/dist/monetization/adapters/bootstrap.d.ts +2 -2
- package/dist/monetization/adapters/bootstrap.js +3 -2
- package/dist/monetization/adapters/bootstrap.test.js +11 -7
- package/dist/monetization/adapters/embeddings-factory.d.ts +10 -5
- package/dist/monetization/adapters/embeddings-factory.js +17 -4
- package/dist/monetization/adapters/embeddings-factory.test.js +85 -31
- package/dist/monetization/adapters/ollama-embeddings.d.ts +40 -0
- package/dist/monetization/adapters/ollama-embeddings.js +76 -0
- package/dist/monetization/adapters/ollama-embeddings.test.d.ts +1 -0
- package/dist/monetization/adapters/ollama-embeddings.test.js +178 -0
- package/dist/monetization/adapters/rate-table.js +9 -3
- package/dist/monetization/adapters/rate-table.test.js +22 -1
- 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
- package/src/monetization/adapters/bootstrap.test.ts +11 -7
- package/src/monetization/adapters/bootstrap.ts +3 -2
- package/src/monetization/adapters/embeddings-factory.test.ts +102 -33
- package/src/monetization/adapters/embeddings-factory.ts +24 -7
- package/src/monetization/adapters/ollama-embeddings.test.ts +235 -0
- package/src/monetization/adapters/ollama-embeddings.ts +120 -0
- package/src/monetization/adapters/rate-table.test.ts +32 -1
- package/src/monetization/adapters/rate-table.ts +9 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { ProviderCostInput, RateStore, SellRateInput } from "../../admin/rates/rate-store.js";
|
|
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
|
+
function parseBooleanParam(value: string | undefined): boolean | undefined {
|
|
8
|
+
if (value === "true") return true;
|
|
9
|
+
if (value === "false") return false;
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create admin rate API routes.
|
|
15
|
+
* Pass a store factory and optional audit logger for DI.
|
|
16
|
+
*/
|
|
17
|
+
export function createAdminRateApiRoutes(
|
|
18
|
+
storeFactory: () => RateStore,
|
|
19
|
+
auditLogger?: () => AdminAuditLogger,
|
|
20
|
+
): Hono<AuthEnv> {
|
|
21
|
+
const routes = new Hono<AuthEnv>();
|
|
22
|
+
|
|
23
|
+
// ── Combined List ──
|
|
24
|
+
|
|
25
|
+
routes.get("/", async (c) => {
|
|
26
|
+
const store = storeFactory();
|
|
27
|
+
const capability = c.req.query("capability");
|
|
28
|
+
const active = parseBooleanParam(c.req.query("active"));
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const [sellRates, providerCosts] = await Promise.all([
|
|
32
|
+
store.listSellRates({ capability, isActive: active }),
|
|
33
|
+
store.listProviderCosts({ capability, isActive: active }),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
return c.json({
|
|
37
|
+
sell_rates: sellRates.entries,
|
|
38
|
+
provider_costs: providerCosts.entries,
|
|
39
|
+
total_sell_rates: sellRates.total,
|
|
40
|
+
total_provider_costs: providerCosts.total,
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── Sell Rates ──
|
|
48
|
+
|
|
49
|
+
routes.post("/sell", async (c) => {
|
|
50
|
+
const store = storeFactory();
|
|
51
|
+
|
|
52
|
+
let body: Record<string, unknown>;
|
|
53
|
+
try {
|
|
54
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { capability, displayName, unit, priceUsd, model, isActive, sortOrder } = body;
|
|
60
|
+
|
|
61
|
+
if (typeof capability !== "string" || !capability.trim()) {
|
|
62
|
+
return c.json({ error: "capability is required and must be non-empty" }, 400);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof displayName !== "string" || !displayName.trim()) {
|
|
66
|
+
return c.json({ error: "displayName is required and must be non-empty" }, 400);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof unit !== "string" || !unit.trim()) {
|
|
70
|
+
return c.json({ error: "unit is required and must be non-empty" }, 400);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof priceUsd !== "number" || priceUsd <= 0) {
|
|
74
|
+
return c.json({ error: "priceUsd must be a positive number" }, 400);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (model !== undefined && typeof model !== "string") {
|
|
78
|
+
return c.json({ error: "model must be a string if provided" }, 400);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isActive !== undefined && typeof isActive !== "boolean") {
|
|
82
|
+
return c.json({ error: "isActive must be a boolean if provided" }, 400);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (sortOrder !== undefined && (typeof sortOrder !== "number" || !Number.isInteger(sortOrder))) {
|
|
86
|
+
return c.json({ error: "sortOrder must be an integer if provided" }, 400);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const adminUser = (c.get("user") as { id?: string } | undefined)?.id ?? "unknown";
|
|
91
|
+
const input: SellRateInput = {
|
|
92
|
+
capability,
|
|
93
|
+
displayName,
|
|
94
|
+
unit,
|
|
95
|
+
priceUsd,
|
|
96
|
+
model: model as string | undefined,
|
|
97
|
+
isActive: isActive as boolean | undefined,
|
|
98
|
+
sortOrder: sortOrder as number | undefined,
|
|
99
|
+
};
|
|
100
|
+
let result: Awaited<ReturnType<typeof store.createSellRate>>;
|
|
101
|
+
try {
|
|
102
|
+
result = await store.createSellRate(input);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
safeAuditLog(auditLogger, {
|
|
105
|
+
adminUser,
|
|
106
|
+
action: "rates.createSellRate",
|
|
107
|
+
category: "config",
|
|
108
|
+
details: { ...input, error: String(err) },
|
|
109
|
+
outcome: "failure",
|
|
110
|
+
});
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
safeAuditLog(auditLogger, {
|
|
114
|
+
adminUser,
|
|
115
|
+
action: "rates.createSellRate",
|
|
116
|
+
category: "config",
|
|
117
|
+
details: { ...input },
|
|
118
|
+
outcome: "success",
|
|
119
|
+
});
|
|
120
|
+
return c.json(result, 201);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
routes.put("/sell/:id", async (c) => {
|
|
127
|
+
const store = storeFactory();
|
|
128
|
+
const id = c.req.param("id");
|
|
129
|
+
|
|
130
|
+
let body: Record<string, unknown>;
|
|
131
|
+
try {
|
|
132
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
133
|
+
} catch {
|
|
134
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { capability, displayName, unit, priceUsd, model, isActive, sortOrder } = body;
|
|
138
|
+
|
|
139
|
+
if (capability !== undefined && (typeof capability !== "string" || !capability.trim())) {
|
|
140
|
+
return c.json({ error: "capability must be non-empty if provided" }, 400);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (displayName !== undefined && (typeof displayName !== "string" || !displayName.trim())) {
|
|
144
|
+
return c.json({ error: "displayName must be non-empty if provided" }, 400);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (unit !== undefined && (typeof unit !== "string" || !unit.trim())) {
|
|
148
|
+
return c.json({ error: "unit must be non-empty if provided" }, 400);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (priceUsd !== undefined && (typeof priceUsd !== "number" || priceUsd <= 0)) {
|
|
152
|
+
return c.json({ error: "priceUsd must be a positive number if provided" }, 400);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (model !== undefined && model !== null && typeof model !== "string") {
|
|
156
|
+
return c.json({ error: "model must be a string or null if provided" }, 400);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isActive !== undefined && typeof isActive !== "boolean") {
|
|
160
|
+
return c.json({ error: "isActive must be a boolean if provided" }, 400);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (sortOrder !== undefined && (typeof sortOrder !== "number" || !Number.isInteger(sortOrder))) {
|
|
164
|
+
return c.json({ error: "sortOrder must be an integer if provided" }, 400);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const adminUser = (c.get("user") as { id?: string } | undefined)?.id ?? "unknown";
|
|
169
|
+
const input: Partial<SellRateInput> = {};
|
|
170
|
+
if (capability !== undefined) input.capability = capability as string;
|
|
171
|
+
if (displayName !== undefined) input.displayName = displayName as string;
|
|
172
|
+
if (unit !== undefined) input.unit = unit as string;
|
|
173
|
+
if (priceUsd !== undefined) input.priceUsd = priceUsd as number;
|
|
174
|
+
if ("model" in body) input.model = model as string | undefined;
|
|
175
|
+
if (isActive !== undefined) input.isActive = isActive as boolean;
|
|
176
|
+
if (sortOrder !== undefined) input.sortOrder = sortOrder as number;
|
|
177
|
+
|
|
178
|
+
let result: Awaited<ReturnType<typeof store.updateSellRate>>;
|
|
179
|
+
try {
|
|
180
|
+
result = await store.updateSellRate(id, input);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
safeAuditLog(auditLogger, {
|
|
183
|
+
adminUser,
|
|
184
|
+
action: "rates.updateSellRate",
|
|
185
|
+
category: "config",
|
|
186
|
+
details: { id, ...input, error: String(err) },
|
|
187
|
+
outcome: "failure",
|
|
188
|
+
});
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
safeAuditLog(auditLogger, {
|
|
192
|
+
adminUser,
|
|
193
|
+
action: "rates.updateSellRate",
|
|
194
|
+
category: "config",
|
|
195
|
+
details: { id, ...input },
|
|
196
|
+
outcome: "success",
|
|
197
|
+
});
|
|
198
|
+
return c.json(result, 200);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (err instanceof Error && err.message.includes("not found")) {
|
|
201
|
+
return c.json({ error: err.message }, 404);
|
|
202
|
+
}
|
|
203
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
routes.delete("/sell/:id", async (c) => {
|
|
208
|
+
const store = storeFactory();
|
|
209
|
+
const id = c.req.param("id");
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const adminUser = (c.get("user") as { id?: string } | undefined)?.id ?? "unknown";
|
|
213
|
+
let deleted: boolean;
|
|
214
|
+
try {
|
|
215
|
+
deleted = await store.deleteSellRate(id);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
safeAuditLog(auditLogger, {
|
|
218
|
+
adminUser,
|
|
219
|
+
action: "rates.deleteSellRate",
|
|
220
|
+
category: "config",
|
|
221
|
+
details: { id, error: String(err) },
|
|
222
|
+
outcome: "failure",
|
|
223
|
+
});
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
if (deleted) {
|
|
227
|
+
safeAuditLog(auditLogger, {
|
|
228
|
+
adminUser,
|
|
229
|
+
action: "rates.deleteSellRate",
|
|
230
|
+
category: "config",
|
|
231
|
+
details: { id },
|
|
232
|
+
outcome: "success",
|
|
233
|
+
});
|
|
234
|
+
return c.json({ success: true }, 200);
|
|
235
|
+
}
|
|
236
|
+
return c.json({ error: "Sell rate not found" }, 404);
|
|
237
|
+
} catch {
|
|
238
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── Provider Costs ──
|
|
243
|
+
|
|
244
|
+
routes.post("/provider", async (c) => {
|
|
245
|
+
const store = storeFactory();
|
|
246
|
+
|
|
247
|
+
let body: Record<string, unknown>;
|
|
248
|
+
try {
|
|
249
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
250
|
+
} catch {
|
|
251
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { capability, adapter, model, unit, costUsd, priority, latencyClass, isActive } = body;
|
|
255
|
+
|
|
256
|
+
if (typeof capability !== "string" || !capability.trim()) {
|
|
257
|
+
return c.json({ error: "capability is required and must be non-empty" }, 400);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (typeof adapter !== "string" || !adapter.trim()) {
|
|
261
|
+
return c.json({ error: "adapter is required and must be non-empty" }, 400);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (typeof unit !== "string" || !unit.trim()) {
|
|
265
|
+
return c.json({ error: "unit is required and must be non-empty" }, 400);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (typeof costUsd !== "number" || costUsd <= 0) {
|
|
269
|
+
return c.json({ error: "costUsd must be a positive number" }, 400);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (model !== undefined && typeof model !== "string") {
|
|
273
|
+
return c.json({ error: "model must be a string if provided" }, 400);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (priority !== undefined && (typeof priority !== "number" || !Number.isInteger(priority))) {
|
|
277
|
+
return c.json({ error: "priority must be an integer if provided" }, 400);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (latencyClass !== undefined && typeof latencyClass !== "string") {
|
|
281
|
+
return c.json({ error: "latencyClass must be a string if provided" }, 400);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (isActive !== undefined && typeof isActive !== "boolean") {
|
|
285
|
+
return c.json({ error: "isActive must be a boolean if provided" }, 400);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const adminUser = (c.get("user") as { id?: string } | undefined)?.id ?? "unknown";
|
|
290
|
+
const input: ProviderCostInput = {
|
|
291
|
+
capability,
|
|
292
|
+
adapter,
|
|
293
|
+
unit,
|
|
294
|
+
costUsd,
|
|
295
|
+
model: model as string | undefined,
|
|
296
|
+
priority: priority as number | undefined,
|
|
297
|
+
latencyClass: latencyClass as string | undefined,
|
|
298
|
+
isActive: isActive as boolean | undefined,
|
|
299
|
+
};
|
|
300
|
+
let result: Awaited<ReturnType<typeof store.createProviderCost>>;
|
|
301
|
+
try {
|
|
302
|
+
result = await store.createProviderCost(input);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
safeAuditLog(auditLogger, {
|
|
305
|
+
adminUser,
|
|
306
|
+
action: "rates.createProviderCost",
|
|
307
|
+
category: "config",
|
|
308
|
+
details: { ...input, error: String(err) },
|
|
309
|
+
outcome: "failure",
|
|
310
|
+
});
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
safeAuditLog(auditLogger, {
|
|
314
|
+
adminUser,
|
|
315
|
+
action: "rates.createProviderCost",
|
|
316
|
+
category: "config",
|
|
317
|
+
details: { ...input },
|
|
318
|
+
outcome: "success",
|
|
319
|
+
});
|
|
320
|
+
return c.json(result, 201);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
routes.put("/provider/:id", async (c) => {
|
|
327
|
+
const store = storeFactory();
|
|
328
|
+
const id = c.req.param("id");
|
|
329
|
+
|
|
330
|
+
let body: Record<string, unknown>;
|
|
331
|
+
try {
|
|
332
|
+
body = (await c.req.json()) as Record<string, unknown>;
|
|
333
|
+
} catch {
|
|
334
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const { capability, adapter, model, unit, costUsd, priority, latencyClass, isActive } = body;
|
|
338
|
+
|
|
339
|
+
if (capability !== undefined && (typeof capability !== "string" || !capability.trim())) {
|
|
340
|
+
return c.json({ error: "capability must be non-empty if provided" }, 400);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (adapter !== undefined && (typeof adapter !== "string" || !adapter.trim())) {
|
|
344
|
+
return c.json({ error: "adapter must be non-empty if provided" }, 400);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (unit !== undefined && (typeof unit !== "string" || !unit.trim())) {
|
|
348
|
+
return c.json({ error: "unit must be non-empty if provided" }, 400);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (costUsd !== undefined && (typeof costUsd !== "number" || costUsd <= 0)) {
|
|
352
|
+
return c.json({ error: "costUsd must be a positive number if provided" }, 400);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (model !== undefined && model !== null && typeof model !== "string") {
|
|
356
|
+
return c.json({ error: "model must be a string or null if provided" }, 400);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (priority !== undefined && (typeof priority !== "number" || !Number.isInteger(priority))) {
|
|
360
|
+
return c.json({ error: "priority must be an integer if provided" }, 400);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (latencyClass !== undefined && typeof latencyClass !== "string") {
|
|
364
|
+
return c.json({ error: "latencyClass must be a string if provided" }, 400);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (isActive !== undefined && typeof isActive !== "boolean") {
|
|
368
|
+
return c.json({ error: "isActive must be a boolean if provided" }, 400);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const adminUser = (c.get("user") as { id?: string } | undefined)?.id ?? "unknown";
|
|
373
|
+
const input: Partial<ProviderCostInput> = {};
|
|
374
|
+
if (capability !== undefined) input.capability = capability as string;
|
|
375
|
+
if (adapter !== undefined) input.adapter = adapter as string;
|
|
376
|
+
if (unit !== undefined) input.unit = unit as string;
|
|
377
|
+
if (costUsd !== undefined) input.costUsd = costUsd as number;
|
|
378
|
+
if ("model" in body) input.model = model as string | undefined;
|
|
379
|
+
if (priority !== undefined) input.priority = priority as number;
|
|
380
|
+
if (latencyClass !== undefined) input.latencyClass = latencyClass as string;
|
|
381
|
+
if (isActive !== undefined) input.isActive = isActive as boolean;
|
|
382
|
+
|
|
383
|
+
let result: Awaited<ReturnType<typeof store.updateProviderCost>>;
|
|
384
|
+
try {
|
|
385
|
+
result = await store.updateProviderCost(id, input);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
safeAuditLog(auditLogger, {
|
|
388
|
+
adminUser,
|
|
389
|
+
action: "rates.updateProviderCost",
|
|
390
|
+
category: "config",
|
|
391
|
+
details: { id, ...input, error: String(err) },
|
|
392
|
+
outcome: "failure",
|
|
393
|
+
});
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
safeAuditLog(auditLogger, {
|
|
397
|
+
adminUser,
|
|
398
|
+
action: "rates.updateProviderCost",
|
|
399
|
+
category: "config",
|
|
400
|
+
details: { id, ...input },
|
|
401
|
+
outcome: "success",
|
|
402
|
+
});
|
|
403
|
+
return c.json(result, 200);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (err instanceof Error && err.message.includes("not found")) {
|
|
406
|
+
return c.json({ error: err.message }, 404);
|
|
407
|
+
}
|
|
408
|
+
return c.json({ error: err instanceof Error ? err.message : "Internal server error" }, 500);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
routes.delete("/provider/:id", async (c) => {
|
|
413
|
+
const store = storeFactory();
|
|
414
|
+
const id = c.req.param("id");
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const adminUser = (c.get("user") as { id?: string } | undefined)?.id ?? "unknown";
|
|
418
|
+
let deleted: boolean;
|
|
419
|
+
try {
|
|
420
|
+
deleted = await store.deleteProviderCost(id);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
safeAuditLog(auditLogger, {
|
|
423
|
+
adminUser,
|
|
424
|
+
action: "rates.deleteProviderCost",
|
|
425
|
+
category: "config",
|
|
426
|
+
details: { id, error: String(err) },
|
|
427
|
+
outcome: "failure",
|
|
428
|
+
});
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
if (deleted) {
|
|
432
|
+
safeAuditLog(auditLogger, {
|
|
433
|
+
adminUser,
|
|
434
|
+
action: "rates.deleteProviderCost",
|
|
435
|
+
category: "config",
|
|
436
|
+
details: { id },
|
|
437
|
+
outcome: "success",
|
|
438
|
+
});
|
|
439
|
+
return c.json({ success: true }, 200);
|
|
440
|
+
}
|
|
441
|
+
return c.json({ error: "Provider cost not found" }, 404);
|
|
442
|
+
} catch {
|
|
443
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ── Margin Report ──
|
|
448
|
+
|
|
449
|
+
routes.get("/margins", async (c) => {
|
|
450
|
+
const store = storeFactory();
|
|
451
|
+
const capability = c.req.query("capability");
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const report = await store.getMarginReport(capability);
|
|
455
|
+
return c.json({ margins: report });
|
|
456
|
+
} catch {
|
|
457
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return routes;
|
|
462
|
+
}
|