@wopr-network/platform-core 1.13.0 → 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/activity.d.ts +9 -0
- package/dist/api/routes/activity.js +68 -0
- package/dist/api/routes/admin-audit-helper.d.ts +7 -0
- package/dist/api/routes/admin-audit-helper.js +13 -0
- package/dist/api/routes/admin-audit.d.ts +13 -0
- package/dist/api/routes/admin-audit.js +61 -0
- package/dist/api/routes/admin-backups.d.ts +19 -0
- package/dist/api/routes/admin-backups.js +116 -0
- package/dist/api/routes/admin-compliance.d.ts +9 -0
- package/dist/api/routes/admin-compliance.js +27 -0
- package/dist/api/routes/admin-credits.d.ts +9 -0
- package/dist/api/routes/admin-credits.js +255 -0
- package/dist/api/routes/admin-gpu.d.ts +46 -0
- package/dist/api/routes/admin-gpu.js +140 -0
- package/dist/api/routes/admin-inference.d.ts +16 -0
- package/dist/api/routes/admin-inference.js +98 -0
- package/dist/api/routes/admin-marketplace.d.ts +36 -0
- package/dist/api/routes/admin-marketplace.js +181 -0
- package/dist/api/routes/admin-migration.d.ts +10 -0
- package/dist/api/routes/admin-migration.js +46 -0
- package/dist/api/routes/admin-notes.d.ts +34 -0
- package/dist/api/routes/admin-notes.js +131 -0
- package/dist/api/routes/admin-onboarding.d.ts +7 -0
- package/dist/api/routes/admin-onboarding.js +49 -0
- package/dist/api/routes/admin-rates.d.ts +9 -0
- package/dist/api/routes/admin-rates.js +427 -0
- package/dist/api/routes/admin-recovery.d.ts +91 -0
- package/dist/api/routes/admin-recovery.js +246 -0
- package/dist/api/routes/admin-roles.d.ts +27 -0
- package/dist/api/routes/admin-roles.js +157 -0
- package/dist/api/routes/audit.d.ts +19 -0
- package/dist/api/routes/audit.js +95 -0
- package/dist/api/routes/auth.d.ts +19 -0
- package/dist/api/routes/auth.js +25 -0
- package/dist/api/routes/channel-validate.d.ts +11 -0
- package/dist/api/routes/channel-validate.js +148 -0
- package/dist/api/routes/fleet-events.d.ts +4 -0
- package/dist/api/routes/fleet-events.js +53 -0
- package/dist/api/routes/friends-proxy.d.ts +28 -0
- package/dist/api/routes/friends-proxy.js +63 -0
- package/dist/api/routes/friends-types.d.ts +34 -0
- package/dist/api/routes/friends-types.js +28 -0
- package/dist/api/routes/health.d.ts +14 -0
- package/dist/api/routes/health.js +32 -0
- package/dist/api/routes/health.test.d.ts +1 -0
- package/dist/api/routes/health.test.js +70 -0
- package/dist/api/routes/incident-response.d.ts +9 -0
- package/dist/api/routes/incident-response.js +148 -0
- package/dist/api/routes/internal-gpu.d.ts +12 -0
- package/dist/api/routes/internal-gpu.js +70 -0
- package/dist/api/routes/internal-nodes.d.ts +41 -0
- package/dist/api/routes/internal-nodes.js +105 -0
- package/dist/api/routes/login-history.d.ts +11 -0
- package/dist/api/routes/login-history.js +22 -0
- package/dist/api/routes/public-pricing.d.ts +9 -0
- package/dist/api/routes/public-pricing.js +32 -0
- package/dist/api/routes/quota.d.ts +8 -0
- package/dist/api/routes/quota.js +113 -0
- package/dist/api/routes/secret-audit.d.ts +12 -0
- package/dist/api/routes/secret-audit.js +41 -0
- package/dist/api/routes/secrets.d.ts +31 -0
- package/dist/api/routes/secrets.js +135 -0
- package/dist/api/routes/tenant-keys.d.ts +16 -0
- package/dist/api/routes/tenant-keys.js +142 -0
- package/dist/api/routes/verify-email.d.ts +19 -0
- package/dist/api/routes/verify-email.js +70 -0
- package/dist/api/routes/ws-auth.d.ts +21 -0
- package/dist/api/routes/ws-auth.js +24 -0
- package/package.json +35 -1
- package/src/api/routes/activity.ts +77 -0
- package/src/api/routes/admin-audit-helper.ts +18 -0
- package/src/api/routes/admin-audit.ts +67 -0
- package/src/api/routes/admin-backups.ts +134 -0
- package/src/api/routes/admin-compliance.ts +35 -0
- package/src/api/routes/admin-credits.ts +280 -0
- package/src/api/routes/admin-gpu.ts +202 -0
- package/src/api/routes/admin-inference.ts +109 -0
- package/src/api/routes/admin-marketplace.ts +233 -0
- package/src/api/routes/admin-migration.ts +61 -0
- package/src/api/routes/admin-notes.ts +145 -0
- package/src/api/routes/admin-onboarding.ts +62 -0
- package/src/api/routes/admin-rates.ts +462 -0
- package/src/api/routes/admin-recovery.ts +376 -0
- package/src/api/routes/admin-roles.ts +205 -0
- package/src/api/routes/audit.ts +106 -0
- package/src/api/routes/auth.ts +30 -0
- package/src/api/routes/channel-validate.ts +182 -0
- package/src/api/routes/fleet-events.ts +66 -0
- package/src/api/routes/friends-proxy.ts +94 -0
- package/src/api/routes/friends-types.ts +37 -0
- package/src/api/routes/health.test.ts +80 -0
- package/src/api/routes/health.ts +48 -0
- package/src/api/routes/incident-response.ts +159 -0
- package/src/api/routes/internal-gpu.ts +92 -0
- package/src/api/routes/internal-nodes.ts +157 -0
- package/src/api/routes/login-history.ts +28 -0
- package/src/api/routes/public-pricing.ts +36 -0
- package/src/api/routes/quota.ts +136 -0
- package/src/api/routes/secret-audit.ts +55 -0
- package/src/api/routes/secrets.ts +178 -0
- package/src/api/routes/tenant-keys.ts +178 -0
- package/src/api/routes/verify-email.ts +102 -0
- package/src/api/routes/ws-auth.ts +44 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { validateTenantOwnership } 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
|
+
import { validateKeyRequestSchema, writeSecretsRequestSchema } from "../../security/types.js";
|
|
7
|
+
/** Allowlist: only alphanumeric, hyphens, and underscores. */
|
|
8
|
+
const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
9
|
+
function isValidInstanceId(id) {
|
|
10
|
+
return INSTANCE_ID_RE.test(id);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create secrets management routes.
|
|
14
|
+
*
|
|
15
|
+
* @param deps - Dependencies for secret operations
|
|
16
|
+
*/
|
|
17
|
+
export function createSecretsRoutes(deps) {
|
|
18
|
+
const routes = new Hono();
|
|
19
|
+
const instanceDataDir = deps.instanceDataDir ?? "/data/instances";
|
|
20
|
+
// Resolve security functions with defaults
|
|
21
|
+
const decryptFn = deps.security?.decrypt ?? defaultDecrypt;
|
|
22
|
+
const deriveInstanceKeyFn = deps.security?.deriveInstanceKey ?? defaultDeriveInstanceKey;
|
|
23
|
+
const writeEncryptedSeedFn = deps.security?.writeEncryptedSeed ?? defaultWriteSeed;
|
|
24
|
+
const forwardSecretsFn = deps.security?.forwardSecretsToInstance ?? defaultForwardSecrets;
|
|
25
|
+
const validateKeyFn = deps.security?.validateProviderKey ?? defaultValidateKey;
|
|
26
|
+
/**
|
|
27
|
+
* PUT /instances/:id/config/secrets
|
|
28
|
+
*
|
|
29
|
+
* Writes secrets to a running instance by forwarding the body opaquely,
|
|
30
|
+
* or writes an encrypted seed file if the instance is not running.
|
|
31
|
+
*/
|
|
32
|
+
routes.put("/instances/:id/config/secrets", async (c) => {
|
|
33
|
+
const instanceId = c.req.param("id");
|
|
34
|
+
if (!isValidInstanceId(instanceId)) {
|
|
35
|
+
return c.json({ error: "Invalid instance ID" }, 400);
|
|
36
|
+
}
|
|
37
|
+
// Validate tenant ownership of the instance
|
|
38
|
+
const tenantId = await deps.profileLookup.getInstanceTenantId(instanceId);
|
|
39
|
+
const ownershipError = validateTenantOwnership(c, instanceId, tenantId);
|
|
40
|
+
if (ownershipError) {
|
|
41
|
+
return ownershipError;
|
|
42
|
+
}
|
|
43
|
+
const mode = c.req.query("mode") || "proxy";
|
|
44
|
+
if (mode === "seed") {
|
|
45
|
+
// Pre-boot: parse body to encrypt, then discard plaintext
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = await c.req.json();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
52
|
+
}
|
|
53
|
+
const parsed = writeSecretsRequestSchema.safeParse(body);
|
|
54
|
+
if (!parsed.success) {
|
|
55
|
+
return c.json({ error: "Validation failed", details: parsed.error.flatten() }, 400);
|
|
56
|
+
}
|
|
57
|
+
if (!deps.platformSecret) {
|
|
58
|
+
return c.json({ error: "Platform secret not configured" }, 500);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const instanceKey = deriveInstanceKeyFn(instanceId, deps.platformSecret);
|
|
62
|
+
const woprHome = `${instanceDataDir}/${instanceId}`;
|
|
63
|
+
await writeEncryptedSeedFn(woprHome, parsed.data, instanceKey);
|
|
64
|
+
return c.json({ ok: true, mode: "seed" });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const message = err instanceof Error ? err.message : "Failed to write seed";
|
|
68
|
+
deps.logger?.error("Failed to write encrypted seed", { instanceId, error: message });
|
|
69
|
+
return c.json({ error: "Failed to write encrypted seed" }, 500);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Default: proxy mode — forward body opaquely to the instance container
|
|
73
|
+
const rawBody = await c.req.text();
|
|
74
|
+
const instanceUrl = `http://wopr-${instanceId}:3000`;
|
|
75
|
+
const authHeader = c.req.header("Authorization") || "";
|
|
76
|
+
const sessionToken = authHeader.replace(/^Bearer\s+/i, "");
|
|
77
|
+
const result = await forwardSecretsFn(instanceUrl, sessionToken, rawBody);
|
|
78
|
+
if (result.ok) {
|
|
79
|
+
return c.json({ ok: true, mode: "proxy" });
|
|
80
|
+
}
|
|
81
|
+
const status = result.status === 502 ? 502 : result.status === 503 ? 503 : result.status === 404 ? 404 : 500;
|
|
82
|
+
return c.json({ error: result.error || "Proxy failed" }, status);
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* POST /validate-key
|
|
86
|
+
*
|
|
87
|
+
* Validates a provider API key without logging or persisting it.
|
|
88
|
+
*/
|
|
89
|
+
routes.post("/validate-key", async (c) => {
|
|
90
|
+
let body;
|
|
91
|
+
try {
|
|
92
|
+
body = await c.req.json();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
96
|
+
}
|
|
97
|
+
const parsed = validateKeyRequestSchema.safeParse(body);
|
|
98
|
+
if (!parsed.success) {
|
|
99
|
+
return c.json({ error: "Validation failed", details: parsed.error.flatten() }, 400);
|
|
100
|
+
}
|
|
101
|
+
const { provider, encryptedKey } = parsed.data;
|
|
102
|
+
// Decrypt the key in memory
|
|
103
|
+
let plaintextKey;
|
|
104
|
+
try {
|
|
105
|
+
const encryptedPayload = JSON.parse(encryptedKey);
|
|
106
|
+
const instanceId = c.req.query("instanceId");
|
|
107
|
+
if (!instanceId) {
|
|
108
|
+
return c.json({ error: "instanceId query parameter required" }, 400);
|
|
109
|
+
}
|
|
110
|
+
if (!isValidInstanceId(instanceId)) {
|
|
111
|
+
return c.json({ error: "Invalid instance ID" }, 400);
|
|
112
|
+
}
|
|
113
|
+
// Validate tenant ownership of the instance
|
|
114
|
+
const tenantId = await deps.profileLookup.getInstanceTenantId(instanceId);
|
|
115
|
+
const ownershipError = validateTenantOwnership(c, instanceId, tenantId);
|
|
116
|
+
if (ownershipError) {
|
|
117
|
+
return ownershipError;
|
|
118
|
+
}
|
|
119
|
+
if (!deps.platformSecret) {
|
|
120
|
+
return c.json({ error: "Platform secret not configured" }, 500);
|
|
121
|
+
}
|
|
122
|
+
const instanceKey = deriveInstanceKeyFn(instanceId, deps.platformSecret);
|
|
123
|
+
plaintextKey = decryptFn(encryptedPayload, instanceKey);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return c.json({ error: "Failed to decrypt key payload" }, 400);
|
|
127
|
+
}
|
|
128
|
+
// Validate against the provider API
|
|
129
|
+
const result = await validateKeyFn(provider, plaintextKey);
|
|
130
|
+
// Explicitly discard the key reference
|
|
131
|
+
plaintextKey = "";
|
|
132
|
+
return c.json({ valid: result.valid, error: result.error });
|
|
133
|
+
});
|
|
134
|
+
return routes;
|
|
135
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { ITenantKeyRepository } from "../../security/tenant-keys/tenant-key-repository.js";
|
|
3
|
+
export interface TenantKeyDeps {
|
|
4
|
+
repo: () => ITenantKeyRepository;
|
|
5
|
+
/** Platform secret for key derivation. If not provided, encrypt operations will fail with 500. */
|
|
6
|
+
platformSecret?: string;
|
|
7
|
+
logger?: {
|
|
8
|
+
warn(msg: string): void;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create tenant key management routes.
|
|
13
|
+
*
|
|
14
|
+
* @param deps - Dependencies for tenant key operations
|
|
15
|
+
*/
|
|
16
|
+
export declare function createTenantKeyRoutes(deps: TenantKeyDeps): Hono;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { encrypt } from "../../security/encryption.js";
|
|
5
|
+
import { providerSchema } from "../../security/types.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Request schemas
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const storeKeySchema = z.object({
|
|
10
|
+
provider: providerSchema,
|
|
11
|
+
apiKey: z.string().min(1, "API key is required"),
|
|
12
|
+
label: z.string().max(100).optional(),
|
|
13
|
+
});
|
|
14
|
+
/** Derive a per-tenant encryption key from tenant ID and platform secret. */
|
|
15
|
+
function deriveTenantKey(tenantId, platformSecret) {
|
|
16
|
+
return createHmac("sha256", platformSecret).update(`tenant:${tenantId}`).digest();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create tenant key management routes.
|
|
20
|
+
*
|
|
21
|
+
* @param deps - Dependencies for tenant key operations
|
|
22
|
+
*/
|
|
23
|
+
export function createTenantKeyRoutes(deps) {
|
|
24
|
+
const routes = new Hono();
|
|
25
|
+
/**
|
|
26
|
+
* GET /
|
|
27
|
+
*
|
|
28
|
+
* List all API keys for the authenticated tenant.
|
|
29
|
+
* Returns metadata only (never the encrypted key material).
|
|
30
|
+
*/
|
|
31
|
+
routes.get("/", async (c) => {
|
|
32
|
+
const tenantId = getTenantId(c);
|
|
33
|
+
if (!tenantId) {
|
|
34
|
+
return c.json({ error: "Tenant context required" }, 400);
|
|
35
|
+
}
|
|
36
|
+
const keys = await deps.repo().listForTenant(tenantId);
|
|
37
|
+
return c.json({ keys });
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* GET /:provider
|
|
41
|
+
*
|
|
42
|
+
* Check whether the tenant has a stored key for a specific provider.
|
|
43
|
+
* Returns metadata only.
|
|
44
|
+
*/
|
|
45
|
+
routes.get("/:provider", async (c) => {
|
|
46
|
+
const tenantId = getTenantId(c);
|
|
47
|
+
if (!tenantId) {
|
|
48
|
+
return c.json({ error: "Tenant context required" }, 400);
|
|
49
|
+
}
|
|
50
|
+
const provider = c.req.param("provider");
|
|
51
|
+
const parsed = providerSchema.safeParse(provider);
|
|
52
|
+
if (!parsed.success) {
|
|
53
|
+
return c.json({ error: "Invalid provider" }, 400);
|
|
54
|
+
}
|
|
55
|
+
const record = await deps.repo().get(tenantId, parsed.data);
|
|
56
|
+
if (!record) {
|
|
57
|
+
return c.json({ error: "No key stored for this provider" }, 404);
|
|
58
|
+
}
|
|
59
|
+
// Return metadata only, never the encrypted key
|
|
60
|
+
return c.json({
|
|
61
|
+
id: record.id,
|
|
62
|
+
tenant_id: record.tenant_id,
|
|
63
|
+
provider: record.provider,
|
|
64
|
+
label: record.label,
|
|
65
|
+
created_at: record.created_at,
|
|
66
|
+
updated_at: record.updated_at,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* PUT /:provider
|
|
71
|
+
*
|
|
72
|
+
* Store or replace a tenant's API key for a provider.
|
|
73
|
+
* The key is encrypted at rest using AES-256-GCM with a tenant-derived key.
|
|
74
|
+
*/
|
|
75
|
+
routes.put("/:provider", async (c) => {
|
|
76
|
+
const tenantId = getTenantId(c);
|
|
77
|
+
if (!tenantId) {
|
|
78
|
+
return c.json({ error: "Tenant context required" }, 400);
|
|
79
|
+
}
|
|
80
|
+
const provider = c.req.param("provider");
|
|
81
|
+
const providerParsed = providerSchema.safeParse(provider);
|
|
82
|
+
if (!providerParsed.success) {
|
|
83
|
+
return c.json({ error: "Invalid provider" }, 400);
|
|
84
|
+
}
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = await c.req.json();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
91
|
+
}
|
|
92
|
+
const parsed = storeKeySchema.safeParse(body);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
return c.json({ error: "Validation failed", details: parsed.error.flatten() }, 400);
|
|
95
|
+
}
|
|
96
|
+
if (parsed.data.provider !== providerParsed.data) {
|
|
97
|
+
return c.json({ error: "Provider in body must match URL parameter" }, 400);
|
|
98
|
+
}
|
|
99
|
+
if (!deps.platformSecret) {
|
|
100
|
+
return c.json({ error: "Platform secret not configured" }, 500);
|
|
101
|
+
}
|
|
102
|
+
// Encrypt the key in memory, then discard the plaintext
|
|
103
|
+
const tenantKey = deriveTenantKey(tenantId, deps.platformSecret);
|
|
104
|
+
const encryptedPayload = encrypt(parsed.data.apiKey, tenantKey);
|
|
105
|
+
const id = await deps.repo().upsert(tenantId, providerParsed.data, encryptedPayload, parsed.data.label ?? "");
|
|
106
|
+
return c.json({ ok: true, id, provider: providerParsed.data });
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* DELETE /:provider
|
|
110
|
+
*
|
|
111
|
+
* Delete a tenant's stored API key for a provider.
|
|
112
|
+
*/
|
|
113
|
+
routes.delete("/:provider", async (c) => {
|
|
114
|
+
const tenantId = getTenantId(c);
|
|
115
|
+
if (!tenantId) {
|
|
116
|
+
return c.json({ error: "Tenant context required" }, 400);
|
|
117
|
+
}
|
|
118
|
+
const provider = c.req.param("provider");
|
|
119
|
+
const parsed = providerSchema.safeParse(provider);
|
|
120
|
+
if (!parsed.success) {
|
|
121
|
+
return c.json({ error: "Invalid provider" }, 400);
|
|
122
|
+
}
|
|
123
|
+
const deleted = await deps.repo().delete(tenantId, parsed.data);
|
|
124
|
+
if (!deleted) {
|
|
125
|
+
return c.json({ error: "No key stored for this provider" }, 404);
|
|
126
|
+
}
|
|
127
|
+
return c.json({ ok: true, provider: parsed.data });
|
|
128
|
+
});
|
|
129
|
+
return routes;
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Helpers
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/** Extract the tenant ID from the auth context. */
|
|
135
|
+
function getTenantId(c) {
|
|
136
|
+
try {
|
|
137
|
+
return c.get("tokenTenantId");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Pool } from "pg";
|
|
3
|
+
import type { ICreditLedger } from "../../credits/index.js";
|
|
4
|
+
export interface VerifyEmailRouteDeps {
|
|
5
|
+
pool: Pool;
|
|
6
|
+
creditLedger: ICreditLedger;
|
|
7
|
+
}
|
|
8
|
+
export interface VerifyEmailRouteConfig {
|
|
9
|
+
/** UI origin for redirect URLs (default: http://localhost:3001) */
|
|
10
|
+
uiOrigin?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create verify-email routes with explicit dependencies (for testing).
|
|
14
|
+
*/
|
|
15
|
+
export declare function createVerifyEmailRoutes(deps: VerifyEmailRouteDeps, config?: VerifyEmailRouteConfig): Hono;
|
|
16
|
+
/**
|
|
17
|
+
* Create verify-email routes with factory functions (for lazy init).
|
|
18
|
+
*/
|
|
19
|
+
export declare function createVerifyEmailRoutesLazy(poolFactory: () => Pool, creditLedgerFactory: () => ICreditLedger, config?: VerifyEmailRouteConfig): Hono;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { logger } from "../../config/logger.js";
|
|
3
|
+
import { grantSignupCredits } from "../../credits/index.js";
|
|
4
|
+
import { getEmailClient } from "../../email/client.js";
|
|
5
|
+
import { verifyToken, welcomeTemplate } from "../../email/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Create verify-email routes with explicit dependencies (for testing).
|
|
8
|
+
*/
|
|
9
|
+
export function createVerifyEmailRoutes(deps, config) {
|
|
10
|
+
return buildRoutes(() => deps.pool, () => deps.creditLedger, config);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create verify-email routes with factory functions (for lazy init).
|
|
14
|
+
*/
|
|
15
|
+
export function createVerifyEmailRoutesLazy(poolFactory, creditLedgerFactory, config) {
|
|
16
|
+
return buildRoutes(poolFactory, creditLedgerFactory, config);
|
|
17
|
+
}
|
|
18
|
+
function buildRoutes(poolFactory, creditLedgerFactory, config) {
|
|
19
|
+
const uiOrigin = config?.uiOrigin ?? process.env.UI_ORIGIN ?? "http://localhost:3001";
|
|
20
|
+
const routes = new Hono();
|
|
21
|
+
routes.get("/verify", async (c) => {
|
|
22
|
+
const token = c.req.query("token");
|
|
23
|
+
if (!token) {
|
|
24
|
+
return c.redirect(`${uiOrigin}/auth/verify?status=error&reason=missing_token`);
|
|
25
|
+
}
|
|
26
|
+
const pool = poolFactory();
|
|
27
|
+
const result = await verifyToken(pool, token);
|
|
28
|
+
if (!result) {
|
|
29
|
+
return c.redirect(`${uiOrigin}/auth/verify?status=error&reason=invalid_or_expired`);
|
|
30
|
+
}
|
|
31
|
+
// Grant $5 signup credit (idempotent — safe on link re-click)
|
|
32
|
+
try {
|
|
33
|
+
const ledger = creditLedgerFactory();
|
|
34
|
+
const granted = await grantSignupCredits(ledger, result.userId);
|
|
35
|
+
if (granted) {
|
|
36
|
+
logger.info("Signup credit granted", { userId: result.userId });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logger.info("Signup credit already granted, skipping", { userId: result.userId });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
logger.error("Failed to grant signup credit", {
|
|
44
|
+
userId: result.userId,
|
|
45
|
+
error: err instanceof Error ? err.message : String(err),
|
|
46
|
+
});
|
|
47
|
+
// Don't block verification if credit grant fails
|
|
48
|
+
}
|
|
49
|
+
// Send welcome email
|
|
50
|
+
try {
|
|
51
|
+
const emailClient = getEmailClient();
|
|
52
|
+
const template = welcomeTemplate(result.email);
|
|
53
|
+
await emailClient.send({
|
|
54
|
+
to: result.email,
|
|
55
|
+
...template,
|
|
56
|
+
userId: result.userId,
|
|
57
|
+
templateName: "welcome",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger.error("Failed to send welcome email", {
|
|
62
|
+
userId: result.userId,
|
|
63
|
+
error: err instanceof Error ? err.message : String(err),
|
|
64
|
+
});
|
|
65
|
+
// Don't block verification if welcome email fails
|
|
66
|
+
}
|
|
67
|
+
return c.redirect(`${uiOrigin}/auth/verify?status=success`);
|
|
68
|
+
});
|
|
69
|
+
return routes;
|
|
70
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface WsAuthRequest {
|
|
2
|
+
nodeId: string;
|
|
3
|
+
authHeader: string | undefined;
|
|
4
|
+
}
|
|
5
|
+
export interface WsAuthResult {
|
|
6
|
+
authenticated: boolean;
|
|
7
|
+
nodeId?: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NodeSecretVerifier {
|
|
11
|
+
verifyNodeSecret(nodeId: string, secret: string): Promise<boolean | null>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Authenticate a WebSocket upgrade request for a node agent connection.
|
|
15
|
+
*
|
|
16
|
+
* The bearer token resolves to a specific node via per-node persistent secret;
|
|
17
|
+
* the resolved nodeId must match the URL nodeId.
|
|
18
|
+
*
|
|
19
|
+
* @param verifier - object with verifyNodeSecret method (typically a node repository)
|
|
20
|
+
*/
|
|
21
|
+
export declare function authenticateWebSocketUpgrade(req: WsAuthRequest, verifier: NodeSecretVerifier): Promise<WsAuthResult>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticate a WebSocket upgrade request for a node agent connection.
|
|
3
|
+
*
|
|
4
|
+
* The bearer token resolves to a specific node via per-node persistent secret;
|
|
5
|
+
* the resolved nodeId must match the URL nodeId.
|
|
6
|
+
*
|
|
7
|
+
* @param verifier - object with verifyNodeSecret method (typically a node repository)
|
|
8
|
+
*/
|
|
9
|
+
export async function authenticateWebSocketUpgrade(req, verifier) {
|
|
10
|
+
const { nodeId, authHeader } = req;
|
|
11
|
+
const bearer = authHeader?.replace(/^Bearer\s+/i, "");
|
|
12
|
+
// Per-node persistent secret — timing-safe comparison via verifyNodeSecret
|
|
13
|
+
if (bearer) {
|
|
14
|
+
const verified = await verifier.verifyNodeSecret(nodeId, bearer);
|
|
15
|
+
if (verified === true) {
|
|
16
|
+
return { authenticated: true, nodeId };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// No valid auth
|
|
20
|
+
if (!bearer) {
|
|
21
|
+
return { authenticated: false, reason: "unauthorized" };
|
|
22
|
+
}
|
|
23
|
+
return { authenticated: false, reason: "unauthorized" };
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-core",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./discovery": "./dist/discovery/index.js",
|
|
22
22
|
"./email": "./dist/email/index.js",
|
|
23
23
|
"./fleet": "./dist/fleet/index.js",
|
|
24
|
+
"./fleet/profile-store": "./dist/fleet/profile-store.js",
|
|
24
25
|
"./gateway": "./dist/gateway/index.js",
|
|
25
26
|
"./inference": "./dist/inference/index.js",
|
|
26
27
|
"./marketplace": "./dist/marketplace/index.js",
|
|
@@ -36,6 +37,39 @@
|
|
|
36
37
|
"./security": "./dist/security/index.js",
|
|
37
38
|
"./setup": "./dist/setup/index.js",
|
|
38
39
|
"./tenancy": "./dist/tenancy/index.js",
|
|
40
|
+
"./api/routes/audit": "./dist/api/routes/audit.js",
|
|
41
|
+
"./api/routes/auth": "./dist/api/routes/auth.js",
|
|
42
|
+
"./api/routes/health": "./dist/api/routes/health.js",
|
|
43
|
+
"./api/routes/login-history": "./dist/api/routes/login-history.js",
|
|
44
|
+
"./api/routes/admin-audit": "./dist/api/routes/admin-audit.js",
|
|
45
|
+
"./api/routes/admin-audit-helper": "./dist/api/routes/admin-audit-helper.js",
|
|
46
|
+
"./api/routes/admin-backups": "./dist/api/routes/admin-backups.js",
|
|
47
|
+
"./api/routes/admin-compliance": "./dist/api/routes/admin-compliance.js",
|
|
48
|
+
"./api/routes/admin-credits": "./dist/api/routes/admin-credits.js",
|
|
49
|
+
"./api/routes/admin-inference": "./dist/api/routes/admin-inference.js",
|
|
50
|
+
"./api/routes/admin-migration": "./dist/api/routes/admin-migration.js",
|
|
51
|
+
"./api/routes/admin-gpu": "./dist/api/routes/admin-gpu.js",
|
|
52
|
+
"./api/routes/admin-marketplace": "./dist/api/routes/admin-marketplace.js",
|
|
53
|
+
"./api/routes/admin-notes": "./dist/api/routes/admin-notes.js",
|
|
54
|
+
"./api/routes/admin-onboarding": "./dist/api/routes/admin-onboarding.js",
|
|
55
|
+
"./api/routes/admin-rates": "./dist/api/routes/admin-rates.js",
|
|
56
|
+
"./api/routes/admin-recovery": "./dist/api/routes/admin-recovery.js",
|
|
57
|
+
"./api/routes/admin-roles": "./dist/api/routes/admin-roles.js",
|
|
58
|
+
"./api/routes/activity": "./dist/api/routes/activity.js",
|
|
59
|
+
"./api/routes/channel-validate": "./dist/api/routes/channel-validate.js",
|
|
60
|
+
"./api/routes/friends-proxy": "./dist/api/routes/friends-proxy.js",
|
|
61
|
+
"./api/routes/friends-types": "./dist/api/routes/friends-types.js",
|
|
62
|
+
"./api/routes/incident-response": "./dist/api/routes/incident-response.js",
|
|
63
|
+
"./api/routes/internal-nodes": "./dist/api/routes/internal-nodes.js",
|
|
64
|
+
"./api/routes/quota": "./dist/api/routes/quota.js",
|
|
65
|
+
"./api/routes/secrets": "./dist/api/routes/secrets.js",
|
|
66
|
+
"./api/routes/tenant-keys": "./dist/api/routes/tenant-keys.js",
|
|
67
|
+
"./api/routes/fleet-events": "./dist/api/routes/fleet-events.js",
|
|
68
|
+
"./api/routes/internal-gpu": "./dist/api/routes/internal-gpu.js",
|
|
69
|
+
"./api/routes/public-pricing": "./dist/api/routes/public-pricing.js",
|
|
70
|
+
"./api/routes/secret-audit": "./dist/api/routes/secret-audit.js",
|
|
71
|
+
"./api/routes/verify-email": "./dist/api/routes/verify-email.js",
|
|
72
|
+
"./api/routes/ws-auth": "./dist/api/routes/ws-auth.js",
|
|
39
73
|
"./trpc": "./dist/trpc/index.js",
|
|
40
74
|
"./*": "./dist/*.js"
|
|
41
75
|
},
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { DrizzleAuditLogRepository } from "../../audit/audit-log-repository.js";
|
|
3
|
+
import { queryAuditLog } from "../../audit/query.js";
|
|
4
|
+
import type { AuditEnv } from "../../audit/types.js";
|
|
5
|
+
import type { DrizzleDb } from "../../db/index.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create activity feed routes.
|
|
9
|
+
*
|
|
10
|
+
* @param dbFactory - Factory returning the audit DB instance
|
|
11
|
+
*/
|
|
12
|
+
export function createActivityRoutes(dbFactory: () => DrizzleDb): Hono<AuditEnv> {
|
|
13
|
+
const routes = new Hono<AuditEnv>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GET /
|
|
17
|
+
*
|
|
18
|
+
* Returns recent activity events for the authenticated user.
|
|
19
|
+
* Query params:
|
|
20
|
+
* - limit: max results (default 20, max 100)
|
|
21
|
+
*/
|
|
22
|
+
routes.get("/", async (c) => {
|
|
23
|
+
const user = c.get("user");
|
|
24
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
25
|
+
|
|
26
|
+
const limitRaw = c.req.query("limit");
|
|
27
|
+
const limit = Math.min(Math.max(1, limitRaw ? Number.parseInt(limitRaw, 10) : 20), 100);
|
|
28
|
+
|
|
29
|
+
const db = dbFactory();
|
|
30
|
+
const rows = await queryAuditLog(new DrizzleAuditLogRepository(db), { userId: user.id, limit });
|
|
31
|
+
|
|
32
|
+
const events = rows.map((row) => ({
|
|
33
|
+
id: row.id,
|
|
34
|
+
timestamp: new Date(row.timestamp).toISOString(),
|
|
35
|
+
actor: row.user_id,
|
|
36
|
+
action: formatAction(row.action),
|
|
37
|
+
target: row.resource_id ?? row.resource_type,
|
|
38
|
+
targetHref: buildTargetHref(row.resource_type, row.resource_id ?? null),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
return c.json(events);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return routes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatAction(action: string): string {
|
|
48
|
+
const parts = action.split(".");
|
|
49
|
+
if (parts.length === 2) {
|
|
50
|
+
const [resource, verb] = parts;
|
|
51
|
+
const pastTense: Record<string, string> = {
|
|
52
|
+
start: "Started",
|
|
53
|
+
stop: "Stopped",
|
|
54
|
+
create: "Created",
|
|
55
|
+
delete: "Deleted",
|
|
56
|
+
update: "Updated",
|
|
57
|
+
restart: "Restarted",
|
|
58
|
+
};
|
|
59
|
+
return `${pastTense[verb] ?? verb} ${resource}`;
|
|
60
|
+
}
|
|
61
|
+
return action;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildTargetHref(resourceType: string, resourceId: string | null): string {
|
|
65
|
+
if (!resourceId) return "/dashboard";
|
|
66
|
+
switch (resourceType) {
|
|
67
|
+
case "instance":
|
|
68
|
+
case "bot":
|
|
69
|
+
return `/instances/${resourceId}`;
|
|
70
|
+
case "snapshot":
|
|
71
|
+
return `/instances/${resourceId}`;
|
|
72
|
+
case "key":
|
|
73
|
+
return "/settings";
|
|
74
|
+
default:
|
|
75
|
+
return "/dashboard";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuditEntry } from "../../admin/audit-log.js";
|
|
2
|
+
|
|
3
|
+
/** Minimal interface for admin audit logging in route factories. */
|
|
4
|
+
export interface AdminAuditLogger {
|
|
5
|
+
log(entry: AuditEntry): void | Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Safely log an admin audit entry — never throws. */
|
|
9
|
+
export function safeAuditLog(logger: (() => AdminAuditLogger) | undefined, entry: AuditEntry): void {
|
|
10
|
+
if (!logger) return;
|
|
11
|
+
try {
|
|
12
|
+
void Promise.resolve(logger().log(entry)).catch(() => {
|
|
13
|
+
/* audit must not break request */
|
|
14
|
+
});
|
|
15
|
+
} catch {
|
|
16
|
+
/* audit must not break request */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AdminAuditLog } from "../../admin/index.js";
|
|
3
|
+
import type { AuthEnv } from "../../auth/index.js";
|
|
4
|
+
|
|
5
|
+
function parseIntParam(value: string | undefined): number | undefined {
|
|
6
|
+
if (value == null || value === "") return undefined;
|
|
7
|
+
const n = Number.parseInt(value, 10);
|
|
8
|
+
return Number.isFinite(n) ? n : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create admin audit API routes with an injectable audit log service.
|
|
13
|
+
*
|
|
14
|
+
* Routes:
|
|
15
|
+
* GET / — query admin audit log entries with filters
|
|
16
|
+
* GET /export — export filtered entries as CSV
|
|
17
|
+
*
|
|
18
|
+
* @param auditLogFactory - factory returning an AdminAuditLog instance
|
|
19
|
+
*/
|
|
20
|
+
export function createAdminAuditApiRoutes(auditLogFactory: () => AdminAuditLog): Hono<AuthEnv> {
|
|
21
|
+
const routes = new Hono<AuthEnv>();
|
|
22
|
+
|
|
23
|
+
routes.get("/", async (c) => {
|
|
24
|
+
const filters = {
|
|
25
|
+
admin: c.req.query("admin") ?? undefined,
|
|
26
|
+
action: c.req.query("action") ?? undefined,
|
|
27
|
+
category: c.req.query("category") ?? undefined,
|
|
28
|
+
tenant: c.req.query("tenant") ?? undefined,
|
|
29
|
+
from: parseIntParam(c.req.query("from")),
|
|
30
|
+
to: parseIntParam(c.req.query("to")),
|
|
31
|
+
limit: parseIntParam(c.req.query("limit")),
|
|
32
|
+
offset: parseIntParam(c.req.query("offset")),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await auditLogFactory().query(filters);
|
|
37
|
+
return c.json(result);
|
|
38
|
+
} catch {
|
|
39
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
routes.get("/export", async (c) => {
|
|
44
|
+
const filters = {
|
|
45
|
+
admin: c.req.query("admin") ?? undefined,
|
|
46
|
+
action: c.req.query("action") ?? undefined,
|
|
47
|
+
category: c.req.query("category") ?? undefined,
|
|
48
|
+
tenant: c.req.query("tenant") ?? undefined,
|
|
49
|
+
from: parseIntParam(c.req.query("from")),
|
|
50
|
+
to: parseIntParam(c.req.query("to")),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const csv = await auditLogFactory().exportCsv(filters);
|
|
55
|
+
return new Response(csv, {
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "text/csv",
|
|
58
|
+
"Content-Disposition": 'attachment; filename="audit-log.csv"',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return routes;
|
|
67
|
+
}
|