@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,148 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { getCustomerTemplate, getInternalTemplate } from "../../monetization/incident/communication-templates.js";
4
+ import { getEscalationMatrix } from "../../monetization/incident/escalation.js";
5
+ import { generatePostMortemTemplate } from "../../monetization/incident/postmortem.js";
6
+ import { getResponseProcedure } from "../../monetization/incident/response-procedures.js";
7
+ import { classifySeverity } from "../../monetization/incident/severity.js";
8
+ const severitySchema = z.enum(["SEV1", "SEV2", "SEV3"]);
9
+ /**
10
+ * Create admin incident response routes.
11
+ *
12
+ * All imports come from platform-core's monetization/incident module.
13
+ * No external dependencies needed.
14
+ */
15
+ export function createIncidentResponseRoutes() {
16
+ const routes = new Hono();
17
+ /**
18
+ * POST /severity
19
+ * Classify severity from provided signals.
20
+ */
21
+ routes.post("/severity", async (c) => {
22
+ try {
23
+ const body = await c.req.json();
24
+ const parsed = z
25
+ .object({
26
+ stripeReachable: z.boolean(),
27
+ webhooksReceiving: z.boolean().nullable(),
28
+ gatewayErrorRate: z.number(),
29
+ creditDeductionFailures: z.number(),
30
+ dlqDepth: z.number(),
31
+ tenantsWithNegativeBalance: z.number(),
32
+ autoTopupFailures: z.number(),
33
+ firingAlertCount: z.number(),
34
+ })
35
+ .parse(body);
36
+ const result = classifySeverity(parsed);
37
+ return c.json({ success: true, ...result });
38
+ }
39
+ catch (err) {
40
+ if (err instanceof z.ZodError) {
41
+ return c.json({ success: false, error: "Invalid signals payload", details: err.issues }, 400);
42
+ }
43
+ return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
44
+ }
45
+ });
46
+ /**
47
+ * GET /escalation/:severity
48
+ * Get escalation matrix for a severity level.
49
+ */
50
+ routes.get("/escalation/:severity", async (c) => {
51
+ const rawSeverity = c.req.param("severity");
52
+ const parsed = severitySchema.safeParse(rawSeverity);
53
+ if (!parsed.success) {
54
+ return c.json({ success: false, error: "Invalid severity. Must be SEV1, SEV2, or SEV3" }, 400);
55
+ }
56
+ const contacts = getEscalationMatrix(parsed.data);
57
+ return c.json({ success: true, severity: parsed.data, contacts });
58
+ });
59
+ /**
60
+ * GET /procedure/:severity
61
+ * Get response procedure for a severity level.
62
+ */
63
+ routes.get("/procedure/:severity", async (c) => {
64
+ const rawSeverity = c.req.param("severity");
65
+ const parsed = severitySchema.safeParse(rawSeverity);
66
+ if (!parsed.success) {
67
+ return c.json({ success: false, error: "Invalid severity. Must be SEV1, SEV2, or SEV3" }, 400);
68
+ }
69
+ const procedure = getResponseProcedure(parsed.data);
70
+ return c.json({ success: true, procedure });
71
+ });
72
+ /**
73
+ * POST /communicate
74
+ * Generate communication templates for an incident.
75
+ */
76
+ routes.post("/communicate", async (c) => {
77
+ try {
78
+ const body = await c.req.json();
79
+ const parsed = z
80
+ .object({
81
+ severity: severitySchema,
82
+ incidentId: z.string().min(1),
83
+ startedAt: z.string().datetime(),
84
+ affectedSystems: z.array(z.string()),
85
+ customerImpact: z.string(),
86
+ currentStatus: z.string(),
87
+ })
88
+ .parse(body);
89
+ const context = {
90
+ incidentId: parsed.incidentId,
91
+ startedAt: new Date(parsed.startedAt),
92
+ affectedSystems: parsed.affectedSystems,
93
+ customerImpact: parsed.customerImpact,
94
+ currentStatus: parsed.currentStatus,
95
+ };
96
+ const customer = getCustomerTemplate(parsed.severity, context);
97
+ const internal = getInternalTemplate(parsed.severity, context);
98
+ return c.json({ success: true, templates: { customer, internal } });
99
+ }
100
+ catch (err) {
101
+ if (err instanceof z.ZodError) {
102
+ return c.json({ success: false, error: "Invalid payload", details: err.issues }, 400);
103
+ }
104
+ return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
105
+ }
106
+ });
107
+ /**
108
+ * POST /postmortem
109
+ * Generate a post-mortem template for an incident.
110
+ */
111
+ routes.post("/postmortem", async (c) => {
112
+ try {
113
+ const body = await c.req.json();
114
+ const parsed = z
115
+ .object({
116
+ incidentId: z.string().min(1),
117
+ severity: severitySchema,
118
+ title: z.string().min(1),
119
+ startedAt: z.string().datetime(),
120
+ detectedAt: z.string().datetime(),
121
+ resolvedAt: z.string().datetime().nullable(),
122
+ affectedSystems: z.array(z.string()),
123
+ affectedTenantCount: z.number().int().min(0),
124
+ revenueImpactCents: z.number().int().nullable(),
125
+ })
126
+ .parse(body);
127
+ const report = generatePostMortemTemplate({
128
+ incidentId: parsed.incidentId,
129
+ severity: parsed.severity,
130
+ title: parsed.title,
131
+ startedAt: new Date(parsed.startedAt),
132
+ detectedAt: new Date(parsed.detectedAt),
133
+ resolvedAt: parsed.resolvedAt ? new Date(parsed.resolvedAt) : null,
134
+ affectedSystems: parsed.affectedSystems,
135
+ affectedTenantCount: parsed.affectedTenantCount,
136
+ revenueImpactCents: parsed.revenueImpactCents,
137
+ });
138
+ return c.json({ success: true, report });
139
+ }
140
+ catch (err) {
141
+ if (err instanceof z.ZodError) {
142
+ return c.json({ success: false, error: "Invalid payload", details: err.issues }, 400);
143
+ }
144
+ return c.json({ success: false, error: err instanceof Error ? err.message : "Unknown error" }, 500);
145
+ }
146
+ });
147
+ return routes;
148
+ }
@@ -0,0 +1,12 @@
1
+ import { Hono } from "hono";
2
+ export interface GpuNodeStageUpdater {
3
+ updateStage(nodeId: string, stage: string): Promise<void>;
4
+ updateStatus(nodeId: string, status: string): Promise<void>;
5
+ }
6
+ /**
7
+ * Create internal GPU routes for node provisioning status updates.
8
+ *
9
+ * @param gpuSecretFactory - returns the GPU_NODE_SECRET (lazy to avoid env read at load time)
10
+ * @param repoFactory - returns the GPU node repository
11
+ */
12
+ export declare function createInternalGpuRoutes(gpuSecretFactory: () => string | undefined, repoFactory: () => GpuNodeStageUpdater): Hono;
@@ -0,0 +1,70 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { Hono } from "hono";
3
+ import { logger } from "../../config/logger.js";
4
+ const VALID_STAGES = [
5
+ "installing_drivers",
6
+ "installing_docker",
7
+ "downloading_models",
8
+ "starting_services",
9
+ "registering",
10
+ "done",
11
+ ];
12
+ /**
13
+ * Create internal GPU routes for node provisioning status updates.
14
+ *
15
+ * @param gpuSecretFactory - returns the GPU_NODE_SECRET (lazy to avoid env read at load time)
16
+ * @param repoFactory - returns the GPU node repository
17
+ */
18
+ export function createInternalGpuRoutes(gpuSecretFactory, repoFactory) {
19
+ const routes = new Hono();
20
+ routes.post("/register", async (c) => {
21
+ const gpuSecret = gpuSecretFactory();
22
+ if (!gpuSecret) {
23
+ logger.warn("GPU_NODE_SECRET not configured");
24
+ return c.json({ success: false, error: "Unauthorized" }, 401);
25
+ }
26
+ const authHeader = c.req.header("Authorization");
27
+ const bearer = authHeader?.replace(/^Bearer\s+/i, "");
28
+ if (!bearer) {
29
+ return c.json({ success: false, error: "Unauthorized" }, 401);
30
+ }
31
+ const a = Buffer.from(bearer);
32
+ const b = Buffer.from(gpuSecret);
33
+ if (a.length !== b.length || !timingSafeEqual(a, b)) {
34
+ return c.json({ success: false, error: "Unauthorized" }, 401);
35
+ }
36
+ const stage = c.req.query("stage");
37
+ if (!stage || !VALID_STAGES.includes(stage)) {
38
+ return c.json({ success: false, error: `Invalid or missing stage. Valid: ${VALID_STAGES.join(", ")}` }, 400);
39
+ }
40
+ let rawBody;
41
+ try {
42
+ rawBody = await c.req.json();
43
+ }
44
+ catch {
45
+ return c.json({ success: false, error: "Invalid JSON body" }, 400);
46
+ }
47
+ if (typeof rawBody !== "object" ||
48
+ rawBody === null ||
49
+ typeof rawBody.nodeId !== "string") {
50
+ return c.json({ success: false, error: "Missing required field: nodeId" }, 400);
51
+ }
52
+ const { nodeId } = rawBody;
53
+ const repo = repoFactory();
54
+ try {
55
+ await repo.updateStage(nodeId, stage);
56
+ if (stage === "done") {
57
+ await repo.updateStatus(nodeId, "active");
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (err instanceof Error && err.message.includes("not found")) {
62
+ return c.json({ success: false, error: `GPU node not found: ${nodeId}` }, 404);
63
+ }
64
+ throw err;
65
+ }
66
+ logger.info(`GPU node ${nodeId} stage updated to ${stage}`);
67
+ return c.json({ success: true });
68
+ });
69
+ return routes;
70
+ }
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ import type { NodeRegistration } from "../../fleet/repository-types.js";
3
+ export interface INodeRegistrar {
4
+ register(registration: NodeRegistration): Promise<unknown>;
5
+ registerSelfHosted(registration: NodeRegistration & {
6
+ ownerUserId: string;
7
+ label: string | null;
8
+ nodeSecretHash: string;
9
+ }): Promise<unknown>;
10
+ }
11
+ export interface INodeRepoForRegistration {
12
+ getBySecret(secret: string): Promise<{
13
+ id: string;
14
+ } | null>;
15
+ }
16
+ export interface IRegistrationTokenStore {
17
+ consume(token: string, nodeId: string): Promise<{
18
+ userId: string;
19
+ label: string | null;
20
+ } | null>;
21
+ }
22
+ export type HostValidator = (host: string) => void;
23
+ export interface InternalNodeDeps {
24
+ nodeRegistrar: () => INodeRegistrar;
25
+ nodeRepo: () => INodeRepoForRegistration;
26
+ registrationTokenStore: () => IRegistrationTokenStore;
27
+ validateNodeHost: HostValidator;
28
+ logger?: {
29
+ info(msg: string): void;
30
+ };
31
+ /** Prefix for self-hosted node IDs. Default: "self" */
32
+ nodeIdPrefix?: string;
33
+ /** Prefix for node secrets. Default: "wopr_node_" */
34
+ nodeSecretPrefix?: string;
35
+ }
36
+ /**
37
+ * Create internal node registration routes.
38
+ *
39
+ * These are machine-to-machine routes used by node agents, not dashboard UI.
40
+ */
41
+ export declare function createInternalNodeRoutes(deps: InternalNodeDeps): Hono;
@@ -0,0 +1,105 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ const RegisterNodeSchema = z.object({
5
+ node_id: z
6
+ .string()
7
+ .min(1)
8
+ .max(128)
9
+ .regex(/^[a-zA-Z0-9_-]+$/),
10
+ host: z
11
+ .string()
12
+ .min(1)
13
+ .max(253)
14
+ .regex(/^[a-zA-Z0-9._-]+$/),
15
+ capacity_mb: z.number().int().positive().max(1_048_576),
16
+ agent_version: z.string().min(1).max(32),
17
+ });
18
+ /**
19
+ * Create internal node registration routes.
20
+ *
21
+ * These are machine-to-machine routes used by node agents, not dashboard UI.
22
+ */
23
+ export function createInternalNodeRoutes(deps) {
24
+ const routes = new Hono();
25
+ /**
26
+ * POST /register
27
+ * Node registration (called on agent boot).
28
+ *
29
+ * Supports 2 auth paths:
30
+ * 1. Per-node persistent secret (returning self-hosted agent)
31
+ * 2. One-time registration token (new self-hosted node, UUID format)
32
+ */
33
+ routes.post("/register", async (c) => {
34
+ const authHeader = c.req.header("Authorization");
35
+ const bearer = authHeader?.replace(/^Bearer\s+/i, "");
36
+ if (!bearer) {
37
+ return c.json({ success: false, error: "Unauthorized" }, 401);
38
+ }
39
+ let rawBody;
40
+ try {
41
+ rawBody = await c.req.json();
42
+ }
43
+ catch {
44
+ return c.json({ success: false, error: "Invalid registration data" }, 400);
45
+ }
46
+ const parsed = RegisterNodeSchema.safeParse(rawBody);
47
+ if (!parsed.success) {
48
+ return c.json({ success: false, error: "Invalid registration data", details: parsed.error.flatten() }, 400);
49
+ }
50
+ const body = parsed.data;
51
+ try {
52
+ deps.validateNodeHost(body.host);
53
+ }
54
+ catch (err) {
55
+ return c.json({ success: false, error: err.message }, 400);
56
+ }
57
+ const registrar = deps.nodeRegistrar();
58
+ const nodeRepo = deps.nodeRepo();
59
+ // Map snake_case HTTP body to camelCase domain type
60
+ const registration = {
61
+ nodeId: body.node_id,
62
+ host: body.host,
63
+ capacityMb: body.capacity_mb,
64
+ agentVersion: body.agent_version,
65
+ };
66
+ // Path 1: Per-node persistent secret (returning agent)
67
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
68
+ if (!uuidPattern.test(bearer)) {
69
+ const existingNode = await nodeRepo.getBySecret(bearer);
70
+ if (existingNode) {
71
+ await registrar.register({ ...registration, nodeId: existingNode.id });
72
+ deps.logger?.info(`Node re-registered via per-node secret: ${existingNode.id}`);
73
+ return c.json({ success: true });
74
+ }
75
+ return c.json({ success: false, error: "Unauthorized" }, 401);
76
+ }
77
+ // Path 2: One-time registration token (UUID format = registration token)
78
+ const tokenStore = deps.registrationTokenStore();
79
+ const prefix = deps.nodeIdPrefix ?? "self";
80
+ const nodeId = `${prefix}-${randomUUID().slice(0, 8)}`;
81
+ const consumed = await tokenStore.consume(bearer, nodeId);
82
+ if (!consumed) {
83
+ return c.json({ success: false, error: "Invalid or expired token" }, 401);
84
+ }
85
+ // Generate persistent per-node secret
86
+ const secretPrefix = deps.nodeSecretPrefix ?? "wopr_node_";
87
+ const nodeSecret = `${secretPrefix}${randomUUID().replace(/-/g, "")}`;
88
+ const hashedSecret = createHash("sha256").update(nodeSecret).digest("hex");
89
+ // Register self-hosted node via registrar
90
+ await registrar.registerSelfHosted({
91
+ ...registration,
92
+ nodeId,
93
+ ownerUserId: consumed.userId,
94
+ label: consumed.label,
95
+ nodeSecretHash: hashedSecret,
96
+ });
97
+ deps.logger?.info(`Self-hosted node registered: ${nodeId} for user ${consumed.userId}`);
98
+ return c.json({
99
+ success: true,
100
+ node_id: nodeId,
101
+ node_secret: nodeSecret, // Agent saves this — only returned once
102
+ });
103
+ });
104
+ return routes;
105
+ }
@@ -0,0 +1,11 @@
1
+ import { Hono } from "hono";
2
+ import type { AuthEnv } from "../../auth/index.js";
3
+ import type { ILoginHistoryRepository } from "../../auth/login-history-repository.js";
4
+ /**
5
+ * Create login history routes.
6
+ *
7
+ * Returns recent login sessions for the authenticated user.
8
+ * Query params:
9
+ * - limit: max results (default 20, max 100)
10
+ */
11
+ export declare function createLoginHistoryRoutes(repoFactory: () => ILoginHistoryRepository): Hono<AuthEnv>;
@@ -0,0 +1,22 @@
1
+ import { Hono } from "hono";
2
+ /**
3
+ * Create login history routes.
4
+ *
5
+ * Returns recent login sessions for the authenticated user.
6
+ * Query params:
7
+ * - limit: max results (default 20, max 100)
8
+ */
9
+ export function createLoginHistoryRoutes(repoFactory) {
10
+ const routes = new Hono();
11
+ routes.get("/", async (c) => {
12
+ const user = c.get("user");
13
+ if (!user)
14
+ return c.json({ error: "Unauthorized" }, 401);
15
+ const limitRaw = c.req.query("limit");
16
+ const limit = limitRaw ? Math.min(Math.max(1, Number.parseInt(limitRaw, 10) || 20), 100) : 20;
17
+ const repo = repoFactory();
18
+ const entries = await repo.findByUserId(user.id, limit);
19
+ return c.json(entries);
20
+ });
21
+ return routes;
22
+ }
@@ -0,0 +1,9 @@
1
+ import { Hono } from "hono";
2
+ import type { RateStore } from "../../admin/rates/rate-store.js";
3
+ /**
4
+ * Create public pricing routes.
5
+ *
6
+ * Public, unauthenticated endpoint returning active sell rates grouped by capability.
7
+ * Used by pricing pages to display current rates.
8
+ */
9
+ export declare function createPublicPricingRoutes(storeFactory: () => RateStore): Hono;
@@ -0,0 +1,32 @@
1
+ import { Hono } from "hono";
2
+ /**
3
+ * Create public pricing routes.
4
+ *
5
+ * Public, unauthenticated endpoint returning active sell rates grouped by capability.
6
+ * Used by pricing pages to display current rates.
7
+ */
8
+ export function createPublicPricingRoutes(storeFactory) {
9
+ const routes = new Hono();
10
+ routes.get("/", async (c) => {
11
+ try {
12
+ const store = storeFactory();
13
+ const rates = await store.listPublicRates();
14
+ // Group by capability for the UI
15
+ const grouped = {};
16
+ for (const rate of rates) {
17
+ if (!grouped[rate.capability])
18
+ grouped[rate.capability] = [];
19
+ grouped[rate.capability].push({
20
+ name: rate.display_name,
21
+ unit: rate.unit,
22
+ price: rate.price_usd,
23
+ });
24
+ }
25
+ return c.json({ rates: grouped });
26
+ }
27
+ catch {
28
+ return c.json({ error: "Internal server error" }, 500);
29
+ }
30
+ });
31
+ return routes;
32
+ }
@@ -0,0 +1,8 @@
1
+ import { Hono } from "hono";
2
+ import type { ICreditLedger } from "../../credits/credit-ledger.js";
3
+ /**
4
+ * Create quota routes.
5
+ *
6
+ * @param ledgerFactory - Factory returning the credit ledger
7
+ */
8
+ export declare function createQuotaRoutes(ledgerFactory: () => ICreditLedger): Hono;
@@ -0,0 +1,113 @@
1
+ import { Hono } from "hono";
2
+ import { checkInstanceQuota, DEFAULT_INSTANCE_LIMITS } from "../../monetization/quotas/quota-check.js";
3
+ import { buildResourceLimits, DEFAULT_RESOURCE_CONFIG } from "../../monetization/quotas/resource-limits.js";
4
+ /**
5
+ * Create quota routes.
6
+ *
7
+ * @param ledgerFactory - Factory returning the credit ledger
8
+ */
9
+ export function createQuotaRoutes(ledgerFactory) {
10
+ const routes = new Hono();
11
+ /**
12
+ * GET /
13
+ *
14
+ * Returns the authenticated tenant's credit balance and resource limits.
15
+ */
16
+ routes.get("/", async (c) => {
17
+ const tenantId = c.req.query("tenant");
18
+ if (!tenantId) {
19
+ return c.json({ error: "tenant query param is required" }, 400);
20
+ }
21
+ const activeRaw = c.req.query("activeInstances");
22
+ const activeInstances = activeRaw != null ? Number.parseInt(activeRaw, 10) : 0;
23
+ if (Number.isNaN(activeInstances) || activeInstances < 0) {
24
+ return c.json({ error: "Invalid activeInstances parameter" }, 400);
25
+ }
26
+ const balance = await ledgerFactory().balance(tenantId);
27
+ return c.json({
28
+ balanceCents: balance.toCentsRounded(),
29
+ instances: {
30
+ current: activeInstances,
31
+ max: DEFAULT_INSTANCE_LIMITS.maxInstances,
32
+ remaining: DEFAULT_INSTANCE_LIMITS.maxInstances === 0
33
+ ? -1
34
+ : Math.max(0, DEFAULT_INSTANCE_LIMITS.maxInstances - activeInstances),
35
+ },
36
+ resources: DEFAULT_RESOURCE_CONFIG,
37
+ });
38
+ });
39
+ /**
40
+ * POST /check
41
+ *
42
+ * Check whether an instance creation would be allowed.
43
+ */
44
+ routes.post("/check", async (c) => {
45
+ let body;
46
+ try {
47
+ body = (await c.req.json());
48
+ }
49
+ catch {
50
+ return c.json({ error: "Invalid JSON body" }, 400);
51
+ }
52
+ const tenantId = body.tenant;
53
+ if (!tenantId) {
54
+ return c.json({ error: "tenant is required" }, 400);
55
+ }
56
+ const activeInstances = Number(body.activeInstances ?? 0);
57
+ const softCap = Boolean(body.softCap);
58
+ if (Number.isNaN(activeInstances) || activeInstances < 0) {
59
+ return c.json({ error: "Invalid activeInstances" }, 400);
60
+ }
61
+ // Check credit balance
62
+ const balance = await ledgerFactory().balance(tenantId);
63
+ if (balance.isNegative() || balance.isZero()) {
64
+ return c.json({
65
+ allowed: false,
66
+ reason: "Insufficient credit balance",
67
+ currentBalanceCents: balance.toCentsRounded(),
68
+ purchaseUrl: "/settings/billing",
69
+ }, 402);
70
+ }
71
+ const result = checkInstanceQuota(DEFAULT_INSTANCE_LIMITS, activeInstances, {
72
+ softCapEnabled: softCap,
73
+ gracePeriodMs: 7 * 24 * 60 * 60 * 1000,
74
+ });
75
+ const status = result.allowed ? 200 : 403;
76
+ return c.json(result, status);
77
+ });
78
+ /**
79
+ * GET /balance/:tenant
80
+ *
81
+ * Get a tenant's credit balance.
82
+ */
83
+ routes.get("/balance/:tenant", async (c) => {
84
+ const tenantId = c.req.param("tenant");
85
+ const balance = await ledgerFactory().balance(tenantId);
86
+ return c.json({ tenantId, balanceCents: balance.toCentsRounded() });
87
+ });
88
+ /**
89
+ * GET /history/:tenant
90
+ *
91
+ * Get a tenant's credit transaction history.
92
+ */
93
+ routes.get("/history/:tenant", async (c) => {
94
+ const tenantId = c.req.param("tenant");
95
+ const limitRaw = c.req.query("limit");
96
+ const offsetRaw = c.req.query("offset");
97
+ const type = c.req.query("type");
98
+ const limit = limitRaw != null ? Number.parseInt(limitRaw, 10) : 50;
99
+ const offset = offsetRaw != null ? Number.parseInt(offsetRaw, 10) : 0;
100
+ const transactions = await ledgerFactory().history(tenantId, { limit, offset, type: type || undefined });
101
+ return c.json({ transactions });
102
+ });
103
+ /**
104
+ * GET /resource-limits
105
+ *
106
+ * Get default Docker resource constraints for bot containers.
107
+ */
108
+ routes.get("/resource-limits", (c) => {
109
+ const limits = buildResourceLimits();
110
+ return c.json(limits);
111
+ });
112
+ return routes;
113
+ }
@@ -0,0 +1,12 @@
1
+ import { Hono } from "hono";
2
+ import type { AuditEnv } from "../../audit/types.js";
3
+ import type { CredentialSummaryRow, ISecretAuditRepository } from "../../security/index.js";
4
+ type GetCredentialOwner = (id: string) => Promise<CredentialSummaryRow | null>;
5
+ /**
6
+ * Create secret audit routes.
7
+ *
8
+ * @param getRepo - factory for the secret audit repository (lazy init)
9
+ * @param getCredentialOwner - lookup function to find credential and check ownership
10
+ */
11
+ export declare function createSecretAuditRoutes(getRepo: () => ISecretAuditRepository, getCredentialOwner: GetCredentialOwner): Hono<AuditEnv>;
12
+ export {};
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ /**
3
+ * Create secret audit routes.
4
+ *
5
+ * @param getRepo - factory for the secret audit repository (lazy init)
6
+ * @param getCredentialOwner - lookup function to find credential and check ownership
7
+ */
8
+ export function createSecretAuditRoutes(getRepo, getCredentialOwner) {
9
+ const routes = new Hono();
10
+ routes.get("/:id/audit", async (c) => {
11
+ const user = c.get("user");
12
+ if (!user)
13
+ return c.json({ error: "Unauthorized" }, 401);
14
+ const credentialId = c.req.param("id");
15
+ // Verify credential exists and is owned by this user
16
+ const credential = await getCredentialOwner(credentialId);
17
+ if (!credential || credential.createdBy !== user.id) {
18
+ return c.json({ error: "Not found" }, 404);
19
+ }
20
+ const limitRaw = c.req.query("limit") ? Number(c.req.query("limit")) : 50;
21
+ const offsetRaw = c.req.query("offset") ? Number(c.req.query("offset")) : 0;
22
+ const limit = Number.isFinite(limitRaw) ? limitRaw : 50;
23
+ const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0;
24
+ const repo = getRepo();
25
+ const [events, total] = await Promise.all([
26
+ repo.listByCredentialId(credentialId, { limit, offset }),
27
+ repo.countByCredentialId(credentialId),
28
+ ]);
29
+ return c.json({
30
+ events: events.map((e) => ({
31
+ id: e.id,
32
+ accessedAt: new Date(e.accessedAt).toISOString(),
33
+ accessedBy: e.accessedBy,
34
+ action: e.action,
35
+ ...(e.ip !== null ? { ip: e.ip } : {}),
36
+ })),
37
+ total,
38
+ });
39
+ });
40
+ return routes;
41
+ }
@@ -0,0 +1,31 @@
1
+ import { Hono } from "hono";
2
+ import type { AuthEnv } from "../../auth/index.js";
3
+ import { decrypt as defaultDecrypt, deriveInstanceKey as defaultDeriveInstanceKey } from "../../security/encryption.js";
4
+ import { forwardSecretsToInstance as defaultForwardSecrets, writeEncryptedSeed as defaultWriteSeed } from "../../security/key-injection.js";
5
+ import { validateProviderKey as defaultValidateKey } from "../../security/key-validation.js";
6
+ export interface IProfileLookup {
7
+ getInstanceTenantId(instanceId: string): Promise<string | undefined>;
8
+ }
9
+ export interface SecretsSecurityFns {
10
+ decrypt: typeof defaultDecrypt;
11
+ deriveInstanceKey: typeof defaultDeriveInstanceKey;
12
+ writeEncryptedSeed: typeof defaultWriteSeed;
13
+ forwardSecretsToInstance: typeof defaultForwardSecrets;
14
+ validateProviderKey: typeof defaultValidateKey;
15
+ }
16
+ export interface SecretsDeps {
17
+ profileLookup: IProfileLookup;
18
+ platformSecret?: string;
19
+ instanceDataDir?: string;
20
+ logger?: {
21
+ error(msg: string, meta?: Record<string, unknown>): void;
22
+ };
23
+ /** Override security functions (useful for testing). Falls back to platform-core defaults. */
24
+ security?: Partial<SecretsSecurityFns>;
25
+ }
26
+ /**
27
+ * Create secrets management routes.
28
+ *
29
+ * @param deps - Dependencies for secret operations
30
+ */
31
+ export declare function createSecretsRoutes(deps: SecretsDeps): Hono<AuthEnv>;