@synth-deploy/server 0.1.0
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/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { ITelemetryStore, TelemetryAction } from "@synth-deploy/core";
|
|
3
|
+
import { TelemetryQuerySchema } from "./schemas.js";
|
|
4
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
5
|
+
|
|
6
|
+
export function registerTelemetryRoutes(
|
|
7
|
+
app: FastifyInstance,
|
|
8
|
+
telemetryStore: ITelemetryStore,
|
|
9
|
+
): void {
|
|
10
|
+
app.get("/api/telemetry", { preHandler: [requirePermission("settings.manage")] }, async (request) => {
|
|
11
|
+
const parsed = TelemetryQuerySchema.safeParse(request.query);
|
|
12
|
+
const filters = parsed.success ? parsed.data : {};
|
|
13
|
+
|
|
14
|
+
const events = telemetryStore.query({
|
|
15
|
+
actor: filters.actor,
|
|
16
|
+
action: filters.action as TelemetryAction | undefined,
|
|
17
|
+
from: filters.from ? new Date(filters.from) : undefined,
|
|
18
|
+
to: filters.to ? new Date(filters.to) : undefined,
|
|
19
|
+
limit: filters.limit ?? 50,
|
|
20
|
+
offset: filters.offset ?? 0,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const total = telemetryStore.count({
|
|
24
|
+
actor: filters.actor,
|
|
25
|
+
action: filters.action as TelemetryAction | undefined,
|
|
26
|
+
from: filters.from ? new Date(filters.from) : undefined,
|
|
27
|
+
to: filters.to ? new Date(filters.to) : undefined,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return { events, total };
|
|
31
|
+
});
|
|
32
|
+
}
|
package/src/api/users.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import bcrypt from "bcryptjs";
|
|
4
|
+
import type { IUserStore, IRoleStore, IUserRoleStore, UserId, RoleId, UserPublic, Permission } from "@synth-deploy/core";
|
|
5
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
6
|
+
import {
|
|
7
|
+
CreateUserSchema,
|
|
8
|
+
UpdateUserSchema,
|
|
9
|
+
AssignRolesSchema,
|
|
10
|
+
CreateRoleSchema,
|
|
11
|
+
UpdateRoleSchema,
|
|
12
|
+
} from "./schemas.js";
|
|
13
|
+
|
|
14
|
+
function toPublicUser(user: { id: UserId; email: string; name: string; authSource?: string; externalId?: string; createdAt: Date; updatedAt: Date }): UserPublic {
|
|
15
|
+
return {
|
|
16
|
+
id: user.id,
|
|
17
|
+
email: user.email,
|
|
18
|
+
name: user.name,
|
|
19
|
+
authSource: (user.authSource as UserPublic["authSource"]) ?? "local",
|
|
20
|
+
createdAt: user.createdAt,
|
|
21
|
+
updatedAt: user.updatedAt,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerUserRoutes(
|
|
26
|
+
app: FastifyInstance,
|
|
27
|
+
userStore: IUserStore,
|
|
28
|
+
roleStore: IRoleStore,
|
|
29
|
+
userRoleStore: IUserRoleStore,
|
|
30
|
+
): void {
|
|
31
|
+
|
|
32
|
+
// --- GET /api/users ---
|
|
33
|
+
app.get("/api/users", { preHandler: [requirePermission("users.manage")] }, async () => {
|
|
34
|
+
const users = userStore.list();
|
|
35
|
+
return { users: users.map(toPublicUser) };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// --- POST /api/users ---
|
|
39
|
+
app.post("/api/users", { preHandler: [requirePermission("users.manage")] }, async (request, reply) => {
|
|
40
|
+
const parsed = CreateUserSchema.safeParse(request.body);
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { email, name, password } = parsed.data;
|
|
46
|
+
const existing = userStore.getByEmail(email);
|
|
47
|
+
if (existing) {
|
|
48
|
+
return reply.status(409).send({ error: "Email already in use" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
52
|
+
const userId = crypto.randomUUID() as UserId;
|
|
53
|
+
const now = new Date();
|
|
54
|
+
|
|
55
|
+
const user = userStore.create({
|
|
56
|
+
id: userId,
|
|
57
|
+
email,
|
|
58
|
+
name,
|
|
59
|
+
passwordHash,
|
|
60
|
+
createdAt: now,
|
|
61
|
+
updatedAt: now,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return reply.status(201).send({ user: toPublicUser(user) });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// --- PUT /api/users/:id ---
|
|
68
|
+
app.put<{ Params: { id: string } }>("/api/users/:id", { preHandler: [requirePermission("users.manage")] }, async (request, reply) => {
|
|
69
|
+
const parsed = UpdateUserSchema.safeParse(request.body);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const userId = request.params.id as UserId;
|
|
75
|
+
const existing = userStore.getById(userId);
|
|
76
|
+
if (!existing) {
|
|
77
|
+
return reply.status(404).send({ error: "User not found" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
81
|
+
if (parsed.data.email !== undefined) updates.email = parsed.data.email;
|
|
82
|
+
if (parsed.data.name !== undefined) updates.name = parsed.data.name;
|
|
83
|
+
if (parsed.data.password !== undefined) {
|
|
84
|
+
updates.passwordHash = await bcrypt.hash(parsed.data.password, 10);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const user = userStore.update(userId, updates as Parameters<typeof userStore.update>[1]);
|
|
88
|
+
return { user: toPublicUser(user) };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- DELETE /api/users/:id ---
|
|
92
|
+
app.delete<{ Params: { id: string } }>("/api/users/:id", { preHandler: [requirePermission("users.manage")] }, async (request, reply) => {
|
|
93
|
+
const userId = request.params.id as UserId;
|
|
94
|
+
const existing = userStore.getById(userId);
|
|
95
|
+
if (!existing) {
|
|
96
|
+
return reply.status(404).send({ error: "User not found" });
|
|
97
|
+
}
|
|
98
|
+
userStore.delete(userId);
|
|
99
|
+
return reply.status(204).send();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// --- GET /api/users/:id/roles ---
|
|
103
|
+
app.get<{ Params: { id: string } }>("/api/users/:id/roles", { preHandler: [requirePermission("users.manage")] }, async (request, reply) => {
|
|
104
|
+
const userId = request.params.id as UserId;
|
|
105
|
+
const existing = userStore.getById(userId);
|
|
106
|
+
if (!existing) {
|
|
107
|
+
return reply.status(404).send({ error: "User not found" });
|
|
108
|
+
}
|
|
109
|
+
const roles = userRoleStore.getUserRoles(userId);
|
|
110
|
+
return { roles };
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// --- PUT /api/users/:id/roles ---
|
|
114
|
+
app.put<{ Params: { id: string } }>("/api/users/:id/roles", { preHandler: [requirePermission("roles.manage")] }, async (request, reply) => {
|
|
115
|
+
const parsed = AssignRolesSchema.safeParse(request.body);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const userId = request.params.id as UserId;
|
|
121
|
+
const existing = userStore.getById(userId);
|
|
122
|
+
if (!existing) {
|
|
123
|
+
return reply.status(404).send({ error: "User not found" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const assignedBy = request.user!.id;
|
|
127
|
+
const roleIds = parsed.data.roleIds as RoleId[];
|
|
128
|
+
|
|
129
|
+
// Validate all role IDs exist
|
|
130
|
+
for (const roleId of roleIds) {
|
|
131
|
+
const role = roleStore.getById(roleId);
|
|
132
|
+
if (!role) {
|
|
133
|
+
return reply.status(400).send({ error: `Role not found: ${roleId}` });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
userRoleStore.setRoles(userId, roleIds, assignedBy);
|
|
138
|
+
const roles = userRoleStore.getUserRoles(userId);
|
|
139
|
+
return { roles };
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// --- GET /api/roles ---
|
|
143
|
+
app.get("/api/roles", { preHandler: [requirePermission("users.manage")] }, async () => {
|
|
144
|
+
const roles = roleStore.list();
|
|
145
|
+
return { roles };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- POST /api/roles ---
|
|
149
|
+
app.post("/api/roles", { preHandler: [requirePermission("roles.manage")] }, async (request, reply) => {
|
|
150
|
+
const parsed = CreateRoleSchema.safeParse(request.body);
|
|
151
|
+
if (!parsed.success) {
|
|
152
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { name, permissions } = parsed.data;
|
|
156
|
+
const existing = roleStore.getByName(name);
|
|
157
|
+
if (existing) {
|
|
158
|
+
return reply.status(409).send({ error: "Role name already in use" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const roleId = crypto.randomUUID() as RoleId;
|
|
162
|
+
const role = roleStore.create({
|
|
163
|
+
id: roleId,
|
|
164
|
+
name,
|
|
165
|
+
permissions: permissions as Permission[],
|
|
166
|
+
isBuiltIn: false,
|
|
167
|
+
createdAt: new Date(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return reply.status(201).send({ role });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// --- PUT /api/roles/:id ---
|
|
174
|
+
app.put<{ Params: { id: string } }>("/api/roles/:id", { preHandler: [requirePermission("roles.manage")] }, async (request, reply) => {
|
|
175
|
+
const parsed = UpdateRoleSchema.safeParse(request.body);
|
|
176
|
+
if (!parsed.success) {
|
|
177
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const roleId = request.params.id as RoleId;
|
|
181
|
+
const existing = roleStore.getById(roleId);
|
|
182
|
+
if (!existing) {
|
|
183
|
+
return reply.status(404).send({ error: "Role not found" });
|
|
184
|
+
}
|
|
185
|
+
if (existing.isBuiltIn) {
|
|
186
|
+
return reply.status(403).send({ error: "Cannot modify built-in roles" });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const updates: Record<string, unknown> = {};
|
|
190
|
+
if (parsed.data.name !== undefined) updates.name = parsed.data.name;
|
|
191
|
+
if (parsed.data.permissions !== undefined) updates.permissions = parsed.data.permissions as Permission[];
|
|
192
|
+
|
|
193
|
+
const role = roleStore.update(roleId, updates as Parameters<typeof roleStore.update>[1]);
|
|
194
|
+
return { role };
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- DELETE /api/roles/:id ---
|
|
198
|
+
app.delete<{ Params: { id: string } }>("/api/roles/:id", { preHandler: [requirePermission("roles.manage")] }, async (request, reply) => {
|
|
199
|
+
const roleId = request.params.id as RoleId;
|
|
200
|
+
const existing = roleStore.getById(roleId);
|
|
201
|
+
if (!existing) {
|
|
202
|
+
return reply.status(404).send({ error: "Role not found" });
|
|
203
|
+
}
|
|
204
|
+
if (existing.isBuiltIn) {
|
|
205
|
+
return reply.status(403).send({ error: "Cannot delete built-in roles" });
|
|
206
|
+
}
|
|
207
|
+
roleStore.delete(roleId);
|
|
208
|
+
return reply.status(204).send();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import AdmZip from "adm-zip";
|
|
2
|
+
import tarStream from "tar-stream";
|
|
3
|
+
import zlib from "node:zlib";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface ExtractedFile {
|
|
11
|
+
path: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UnpackResult {
|
|
16
|
+
files: ExtractedFile[];
|
|
17
|
+
skipped: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Text file detection
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const TEXT_EXTENSIONS = new Set([
|
|
25
|
+
// Docs / context
|
|
26
|
+
".md", ".txt", ".rst", ".adoc",
|
|
27
|
+
// Config / manifests
|
|
28
|
+
".yaml", ".yml", ".json", ".toml", ".ini", ".conf", ".config",
|
|
29
|
+
".properties", ".env", ".envrc",
|
|
30
|
+
// Infrastructure / IaC
|
|
31
|
+
".tf", ".tfvars", ".hcl", ".bicep",
|
|
32
|
+
// Build / packaging
|
|
33
|
+
".xml", ".gradle", ".gradle.kts", ".nuspec", ".pom",
|
|
34
|
+
".csproj", ".fsproj", ".vbproj", ".sln",
|
|
35
|
+
".gemspec", ".podspec", ".lock",
|
|
36
|
+
// Scripts
|
|
37
|
+
".sh", ".bash", ".zsh", ".fish",
|
|
38
|
+
".ps1", ".psm1", ".psd1", ".cmd", ".bat",
|
|
39
|
+
// Source (for deploy scripts, Lambdas, etc.)
|
|
40
|
+
".py", ".rb", ".js", ".ts", ".go", ".rs", ".java", ".cs",
|
|
41
|
+
// Web / templates
|
|
42
|
+
".html", ".htm", ".jinja", ".j2", ".tpl",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const TEXT_FILENAMES = new Set([
|
|
46
|
+
"dockerfile", "makefile", "procfile", "brewfile", "gemfile",
|
|
47
|
+
"rakefile", "vagrantfile", "jenkinsfile", "capfile", "guardfile",
|
|
48
|
+
"podfile", "appfile", "fastfile",
|
|
49
|
+
"readme", "license", "changelog", "authors", "contributors", "notice",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const SKIP_PATH_SEGMENTS = new Set([
|
|
53
|
+
"node_modules", ".git", "__pycache__", ".idea", ".vscode",
|
|
54
|
+
"vendor", "dist", "build", "target", "bin", "obj",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const SKIP_EXTENSIONS = new Set([
|
|
58
|
+
".pyc", ".class", ".o", ".a", ".so", ".dll", ".exe",
|
|
59
|
+
".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".webp",
|
|
60
|
+
".woff", ".woff2", ".ttf", ".eot",
|
|
61
|
+
".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar",
|
|
62
|
+
".jar", ".war", ".ear", ".whl", ".nupkg",
|
|
63
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const MAX_FILE_BYTES = 50 * 1024; // 50KB per file
|
|
67
|
+
const MAX_TOTAL_BYTES = 300 * 1024; // 300KB total
|
|
68
|
+
const MAX_FILES = 50;
|
|
69
|
+
|
|
70
|
+
function shouldExtract(filePath: string): boolean {
|
|
71
|
+
const lower = filePath.toLowerCase();
|
|
72
|
+
const segments = lower.split("/");
|
|
73
|
+
const filename = segments[segments.length - 1];
|
|
74
|
+
|
|
75
|
+
// Skip directories and hidden files at root
|
|
76
|
+
if (!filename || filename.startsWith(".")) return false;
|
|
77
|
+
|
|
78
|
+
// Skip paths with unwanted segments
|
|
79
|
+
for (const seg of segments.slice(0, -1)) {
|
|
80
|
+
if (SKIP_PATH_SEGMENTS.has(seg)) return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dotIdx = filename.lastIndexOf(".");
|
|
84
|
+
const ext = dotIdx !== -1 ? filename.slice(dotIdx) : "";
|
|
85
|
+
|
|
86
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
87
|
+
if (TEXT_EXTENSIONS.has(ext)) return true;
|
|
88
|
+
if (TEXT_FILENAMES.has(filename)) return true;
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// ZIP unpacker (zip, nupkg, jar, war, ear, whl, vsix, apk)
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function unpackZip(buffer: Buffer): UnpackResult {
|
|
98
|
+
const files: ExtractedFile[] = [];
|
|
99
|
+
let skipped = 0;
|
|
100
|
+
let totalBytes = 0;
|
|
101
|
+
|
|
102
|
+
let zip: AdmZip;
|
|
103
|
+
try {
|
|
104
|
+
zip = new AdmZip(buffer);
|
|
105
|
+
} catch {
|
|
106
|
+
return { files: [], skipped: 0 };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const entry of zip.getEntries()) {
|
|
110
|
+
if (entry.isDirectory) continue;
|
|
111
|
+
if (files.length >= MAX_FILES) { skipped++; continue; }
|
|
112
|
+
if (!shouldExtract(entry.entryName)) { skipped++; continue; }
|
|
113
|
+
if (entry.header.size > MAX_FILE_BYTES) { skipped++; continue; }
|
|
114
|
+
if (totalBytes + entry.header.size > MAX_TOTAL_BYTES) { skipped++; continue; }
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const content = entry.getData().toString("utf-8");
|
|
118
|
+
// Reject if it looks binary (high proportion of null bytes / non-printable)
|
|
119
|
+
if (looksLikeBinary(content)) { skipped++; continue; }
|
|
120
|
+
files.push({ path: entry.entryName, content });
|
|
121
|
+
totalBytes += entry.header.size;
|
|
122
|
+
} catch {
|
|
123
|
+
skipped++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { files, skipped };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// TAR unpacker (tar, tar.gz / tgz)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
function unpackTar(buffer: Buffer, gunzip: boolean): Promise<UnpackResult> {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const files: ExtractedFile[] = [];
|
|
137
|
+
let skipped = 0;
|
|
138
|
+
let totalBytes = 0;
|
|
139
|
+
|
|
140
|
+
const extract = tarStream.extract();
|
|
141
|
+
|
|
142
|
+
extract.on("entry", (header, stream, next) => {
|
|
143
|
+
if (
|
|
144
|
+
header.type !== "file" ||
|
|
145
|
+
files.length >= MAX_FILES ||
|
|
146
|
+
!shouldExtract(header.name) ||
|
|
147
|
+
(header.size ?? 0) > MAX_FILE_BYTES ||
|
|
148
|
+
totalBytes + (header.size ?? 0) > MAX_TOTAL_BYTES
|
|
149
|
+
) {
|
|
150
|
+
if (header.type === "file") skipped++;
|
|
151
|
+
stream.resume();
|
|
152
|
+
next();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const chunks: Buffer[] = [];
|
|
157
|
+
let bytes = 0;
|
|
158
|
+
|
|
159
|
+
stream.on("data", (chunk: Buffer) => {
|
|
160
|
+
bytes += chunk.length;
|
|
161
|
+
if (bytes <= MAX_FILE_BYTES) chunks.push(chunk);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
stream.on("end", () => {
|
|
165
|
+
try {
|
|
166
|
+
const content = Buffer.concat(chunks).toString("utf-8");
|
|
167
|
+
if (!looksLikeBinary(content)) {
|
|
168
|
+
files.push({ path: header.name, content });
|
|
169
|
+
totalBytes += bytes;
|
|
170
|
+
} else {
|
|
171
|
+
skipped++;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
skipped++;
|
|
175
|
+
}
|
|
176
|
+
next();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
stream.on("error", () => { skipped++; next(); });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
extract.on("finish", () => resolve({ files, skipped }));
|
|
183
|
+
extract.on("error", () => resolve({ files, skipped }));
|
|
184
|
+
|
|
185
|
+
const readable = Readable.from(buffer);
|
|
186
|
+
if (gunzip) {
|
|
187
|
+
readable.pipe(zlib.createGunzip()).pipe(extract);
|
|
188
|
+
} else {
|
|
189
|
+
readable.pipe(extract);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Binary detection heuristic
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
function looksLikeBinary(text: string): boolean {
|
|
199
|
+
// Sample the first 1KB — if >10% non-printable chars, treat as binary
|
|
200
|
+
const sample = text.slice(0, 1024);
|
|
201
|
+
let nonPrintable = 0;
|
|
202
|
+
for (let i = 0; i < sample.length; i++) {
|
|
203
|
+
const code = sample.charCodeAt(i);
|
|
204
|
+
if (code < 9 || (code > 13 && code < 32) || code === 127) nonPrintable++;
|
|
205
|
+
}
|
|
206
|
+
return nonPrintable / sample.length > 0.1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Public API
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
export type ArchiveFormat = "zip" | "tar" | "tar-gz";
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Map artifact type string to archive format, if applicable.
|
|
217
|
+
* Returns null for non-archive types.
|
|
218
|
+
*/
|
|
219
|
+
export function archiveFormat(artifactType: string, artifactName: string): ArchiveFormat | null {
|
|
220
|
+
switch (artifactType) {
|
|
221
|
+
case "zip":
|
|
222
|
+
case "nupkg":
|
|
223
|
+
case "java-archive":
|
|
224
|
+
case "python-package":
|
|
225
|
+
return "zip";
|
|
226
|
+
case "tarball": {
|
|
227
|
+
const name = artifactName.toLowerCase();
|
|
228
|
+
if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) return "tar-gz";
|
|
229
|
+
return "tar";
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Unpack an archive buffer and extract readable text files.
|
|
238
|
+
* Returns an empty result for unrecognized or corrupt archives.
|
|
239
|
+
*/
|
|
240
|
+
export async function unpackArchive(
|
|
241
|
+
buffer: Buffer,
|
|
242
|
+
format: ArchiveFormat,
|
|
243
|
+
): Promise<UnpackResult> {
|
|
244
|
+
switch (format) {
|
|
245
|
+
case "zip":
|
|
246
|
+
return unpackZip(buffer);
|
|
247
|
+
case "tar":
|
|
248
|
+
return unpackTar(buffer, false);
|
|
249
|
+
case "tar-gz":
|
|
250
|
+
return unpackTar(buffer, true);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Format extracted files as a text block suitable for inclusion in an LLM prompt.
|
|
256
|
+
*/
|
|
257
|
+
export function formatExtractedFiles(result: UnpackResult): string {
|
|
258
|
+
if (result.files.length === 0) {
|
|
259
|
+
return "(no readable text files found in archive)";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const sections = result.files.map(
|
|
263
|
+
(f) => `=== ${f.path} ===\n${f.content.trimEnd()}`,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const footer = result.skipped > 0
|
|
267
|
+
? `\n(${result.skipped} binary or oversized file${result.skipped === 1 ? "" : "s"} skipped)`
|
|
268
|
+
: "";
|
|
269
|
+
|
|
270
|
+
return sections.join("\n\n") + footer;
|
|
271
|
+
}
|