@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.
Files changed (103) hide show
  1. package/dist/api/routes/activity.d.ts +9 -0
  2. package/dist/api/routes/activity.js +68 -0
  3. package/dist/api/routes/admin-audit-helper.d.ts +7 -0
  4. package/dist/api/routes/admin-audit-helper.js +13 -0
  5. package/dist/api/routes/admin-audit.d.ts +13 -0
  6. package/dist/api/routes/admin-audit.js +61 -0
  7. package/dist/api/routes/admin-backups.d.ts +19 -0
  8. package/dist/api/routes/admin-backups.js +116 -0
  9. package/dist/api/routes/admin-compliance.d.ts +9 -0
  10. package/dist/api/routes/admin-compliance.js +27 -0
  11. package/dist/api/routes/admin-credits.d.ts +9 -0
  12. package/dist/api/routes/admin-credits.js +255 -0
  13. package/dist/api/routes/admin-gpu.d.ts +46 -0
  14. package/dist/api/routes/admin-gpu.js +140 -0
  15. package/dist/api/routes/admin-inference.d.ts +16 -0
  16. package/dist/api/routes/admin-inference.js +98 -0
  17. package/dist/api/routes/admin-marketplace.d.ts +36 -0
  18. package/dist/api/routes/admin-marketplace.js +181 -0
  19. package/dist/api/routes/admin-migration.d.ts +10 -0
  20. package/dist/api/routes/admin-migration.js +46 -0
  21. package/dist/api/routes/admin-notes.d.ts +34 -0
  22. package/dist/api/routes/admin-notes.js +131 -0
  23. package/dist/api/routes/admin-onboarding.d.ts +7 -0
  24. package/dist/api/routes/admin-onboarding.js +49 -0
  25. package/dist/api/routes/admin-rates.d.ts +9 -0
  26. package/dist/api/routes/admin-rates.js +427 -0
  27. package/dist/api/routes/admin-recovery.d.ts +91 -0
  28. package/dist/api/routes/admin-recovery.js +246 -0
  29. package/dist/api/routes/admin-roles.d.ts +27 -0
  30. package/dist/api/routes/admin-roles.js +157 -0
  31. package/dist/api/routes/audit.d.ts +19 -0
  32. package/dist/api/routes/audit.js +95 -0
  33. package/dist/api/routes/auth.d.ts +19 -0
  34. package/dist/api/routes/auth.js +25 -0
  35. package/dist/api/routes/channel-validate.d.ts +11 -0
  36. package/dist/api/routes/channel-validate.js +148 -0
  37. package/dist/api/routes/fleet-events.d.ts +4 -0
  38. package/dist/api/routes/fleet-events.js +53 -0
  39. package/dist/api/routes/friends-proxy.d.ts +28 -0
  40. package/dist/api/routes/friends-proxy.js +63 -0
  41. package/dist/api/routes/friends-types.d.ts +34 -0
  42. package/dist/api/routes/friends-types.js +28 -0
  43. package/dist/api/routes/health.d.ts +14 -0
  44. package/dist/api/routes/health.js +32 -0
  45. package/dist/api/routes/health.test.d.ts +1 -0
  46. package/dist/api/routes/health.test.js +70 -0
  47. package/dist/api/routes/incident-response.d.ts +9 -0
  48. package/dist/api/routes/incident-response.js +148 -0
  49. package/dist/api/routes/internal-gpu.d.ts +12 -0
  50. package/dist/api/routes/internal-gpu.js +70 -0
  51. package/dist/api/routes/internal-nodes.d.ts +41 -0
  52. package/dist/api/routes/internal-nodes.js +105 -0
  53. package/dist/api/routes/login-history.d.ts +11 -0
  54. package/dist/api/routes/login-history.js +22 -0
  55. package/dist/api/routes/public-pricing.d.ts +9 -0
  56. package/dist/api/routes/public-pricing.js +32 -0
  57. package/dist/api/routes/quota.d.ts +8 -0
  58. package/dist/api/routes/quota.js +113 -0
  59. package/dist/api/routes/secret-audit.d.ts +12 -0
  60. package/dist/api/routes/secret-audit.js +41 -0
  61. package/dist/api/routes/secrets.d.ts +31 -0
  62. package/dist/api/routes/secrets.js +135 -0
  63. package/dist/api/routes/tenant-keys.d.ts +16 -0
  64. package/dist/api/routes/tenant-keys.js +142 -0
  65. package/dist/api/routes/verify-email.d.ts +19 -0
  66. package/dist/api/routes/verify-email.js +70 -0
  67. package/dist/api/routes/ws-auth.d.ts +21 -0
  68. package/dist/api/routes/ws-auth.js +24 -0
  69. package/package.json +35 -1
  70. package/src/api/routes/activity.ts +77 -0
  71. package/src/api/routes/admin-audit-helper.ts +18 -0
  72. package/src/api/routes/admin-audit.ts +67 -0
  73. package/src/api/routes/admin-backups.ts +134 -0
  74. package/src/api/routes/admin-compliance.ts +35 -0
  75. package/src/api/routes/admin-credits.ts +280 -0
  76. package/src/api/routes/admin-gpu.ts +202 -0
  77. package/src/api/routes/admin-inference.ts +109 -0
  78. package/src/api/routes/admin-marketplace.ts +233 -0
  79. package/src/api/routes/admin-migration.ts +61 -0
  80. package/src/api/routes/admin-notes.ts +145 -0
  81. package/src/api/routes/admin-onboarding.ts +62 -0
  82. package/src/api/routes/admin-rates.ts +462 -0
  83. package/src/api/routes/admin-recovery.ts +376 -0
  84. package/src/api/routes/admin-roles.ts +205 -0
  85. package/src/api/routes/audit.ts +106 -0
  86. package/src/api/routes/auth.ts +30 -0
  87. package/src/api/routes/channel-validate.ts +182 -0
  88. package/src/api/routes/fleet-events.ts +66 -0
  89. package/src/api/routes/friends-proxy.ts +94 -0
  90. package/src/api/routes/friends-types.ts +37 -0
  91. package/src/api/routes/health.test.ts +80 -0
  92. package/src/api/routes/health.ts +48 -0
  93. package/src/api/routes/incident-response.ts +159 -0
  94. package/src/api/routes/internal-gpu.ts +92 -0
  95. package/src/api/routes/internal-nodes.ts +157 -0
  96. package/src/api/routes/login-history.ts +28 -0
  97. package/src/api/routes/public-pricing.ts +36 -0
  98. package/src/api/routes/quota.ts +136 -0
  99. package/src/api/routes/secret-audit.ts +55 -0
  100. package/src/api/routes/secrets.ts +178 -0
  101. package/src/api/routes/tenant-keys.ts +178 -0
  102. package/src/api/routes/verify-email.ts +102 -0
  103. package/src/api/routes/ws-auth.ts +44 -0
@@ -0,0 +1,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.0",
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
+ }