@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,409 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import type { FastifyInstance } from "fastify";
|
|
4
|
+
import {
|
|
5
|
+
DecisionDebrief,
|
|
6
|
+
PartitionStore,
|
|
7
|
+
EnvironmentStore,
|
|
8
|
+
ArtifactStore,
|
|
9
|
+
SettingsStore,
|
|
10
|
+
TelemetryStore,
|
|
11
|
+
UserStore,
|
|
12
|
+
RoleStore,
|
|
13
|
+
UserRoleStore,
|
|
14
|
+
SessionStore,
|
|
15
|
+
} from "@synth-deploy/core";
|
|
16
|
+
import type { UserId, RoleId, Permission } from "@synth-deploy/core";
|
|
17
|
+
import { InMemoryDeploymentStore } from "../src/agent/synth-agent.js";
|
|
18
|
+
import { registerPartitionRoutes } from "../src/api/partitions.js";
|
|
19
|
+
import { registerEnvironmentRoutes } from "../src/api/environments.js";
|
|
20
|
+
import { registerSettingsRoutes } from "../src/api/settings.js";
|
|
21
|
+
import { registerDeploymentRoutes } from "../src/api/deployments.js";
|
|
22
|
+
import { registerArtifactRoutes } from "../src/api/artifacts.js";
|
|
23
|
+
import { registerAuthMiddleware, generateTokens } from "../src/middleware/auth.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Constants
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const JWT_SECRET = new TextEncoder().encode("rbac-test-secret");
|
|
30
|
+
const VIEWER_USER_ID = "viewer-user" as UserId;
|
|
31
|
+
const DEPLOYER_USER_ID = "deployer-user" as UserId;
|
|
32
|
+
const VIEWER_ROLE_ID = "role-viewer" as RoleId;
|
|
33
|
+
const DEPLOYER_ROLE_ID = "role-deployer" as RoleId;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Test helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface TestContext {
|
|
40
|
+
app: FastifyInstance;
|
|
41
|
+
viewerToken: string;
|
|
42
|
+
deployerToken: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function createTestServer(): Promise<TestContext> {
|
|
46
|
+
const userStore = new UserStore();
|
|
47
|
+
const roleStore = new RoleStore();
|
|
48
|
+
const userRoleStore = new UserRoleStore(roleStore);
|
|
49
|
+
const sessionStore = new SessionStore();
|
|
50
|
+
const diary = new DecisionDebrief();
|
|
51
|
+
const partitions = new PartitionStore();
|
|
52
|
+
const environments = new EnvironmentStore();
|
|
53
|
+
const deployments = new InMemoryDeploymentStore();
|
|
54
|
+
const artifactStore = new ArtifactStore();
|
|
55
|
+
const settings = new SettingsStore();
|
|
56
|
+
const telemetry = new TelemetryStore();
|
|
57
|
+
|
|
58
|
+
// Create viewer role — only view permissions
|
|
59
|
+
roleStore.create({
|
|
60
|
+
id: VIEWER_ROLE_ID,
|
|
61
|
+
name: "Viewer",
|
|
62
|
+
permissions: [
|
|
63
|
+
"deployment.view",
|
|
64
|
+
"artifact.view",
|
|
65
|
+
"environment.view",
|
|
66
|
+
"partition.view",
|
|
67
|
+
"envoy.view",
|
|
68
|
+
] as Permission[],
|
|
69
|
+
isBuiltIn: false,
|
|
70
|
+
createdAt: new Date(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Create deployer role — create/approve but no settings/users
|
|
74
|
+
roleStore.create({
|
|
75
|
+
id: DEPLOYER_ROLE_ID,
|
|
76
|
+
name: "Deployer",
|
|
77
|
+
permissions: [
|
|
78
|
+
"deployment.create",
|
|
79
|
+
"deployment.approve",
|
|
80
|
+
"deployment.reject",
|
|
81
|
+
"deployment.view",
|
|
82
|
+
"deployment.rollback",
|
|
83
|
+
"artifact.create",
|
|
84
|
+
"artifact.update",
|
|
85
|
+
"artifact.view",
|
|
86
|
+
"environment.view",
|
|
87
|
+
"partition.view",
|
|
88
|
+
"envoy.view",
|
|
89
|
+
] as Permission[],
|
|
90
|
+
isBuiltIn: false,
|
|
91
|
+
createdAt: new Date(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Create viewer user
|
|
95
|
+
userStore.create({
|
|
96
|
+
id: VIEWER_USER_ID,
|
|
97
|
+
email: "viewer@example.com",
|
|
98
|
+
name: "Viewer User",
|
|
99
|
+
passwordHash: "hashed",
|
|
100
|
+
createdAt: new Date(),
|
|
101
|
+
updatedAt: new Date(),
|
|
102
|
+
});
|
|
103
|
+
userRoleStore.assign(VIEWER_USER_ID, VIEWER_ROLE_ID, VIEWER_USER_ID);
|
|
104
|
+
|
|
105
|
+
// Create deployer user
|
|
106
|
+
userStore.create({
|
|
107
|
+
id: DEPLOYER_USER_ID,
|
|
108
|
+
email: "deployer@example.com",
|
|
109
|
+
name: "Deployer User",
|
|
110
|
+
passwordHash: "hashed",
|
|
111
|
+
createdAt: new Date(),
|
|
112
|
+
updatedAt: new Date(),
|
|
113
|
+
});
|
|
114
|
+
userRoleStore.assign(DEPLOYER_USER_ID, DEPLOYER_ROLE_ID, DEPLOYER_USER_ID);
|
|
115
|
+
|
|
116
|
+
const app = Fastify({ logger: false });
|
|
117
|
+
registerAuthMiddleware(app, userStore, userRoleStore, sessionStore, JWT_SECRET);
|
|
118
|
+
|
|
119
|
+
// Register routes
|
|
120
|
+
registerPartitionRoutes(app, partitions, deployments, diary, telemetry);
|
|
121
|
+
registerEnvironmentRoutes(app, environments, deployments, telemetry);
|
|
122
|
+
registerSettingsRoutes(app, settings, telemetry);
|
|
123
|
+
registerDeploymentRoutes(app, deployments, diary, partitions, environments, artifactStore, settings, telemetry);
|
|
124
|
+
registerArtifactRoutes(app, artifactStore, telemetry);
|
|
125
|
+
|
|
126
|
+
await app.ready();
|
|
127
|
+
|
|
128
|
+
// Generate tokens and sessions
|
|
129
|
+
const viewerTokens = await generateTokens(VIEWER_USER_ID, JWT_SECRET);
|
|
130
|
+
sessionStore.create({
|
|
131
|
+
id: "session-viewer",
|
|
132
|
+
userId: VIEWER_USER_ID,
|
|
133
|
+
token: viewerTokens.token,
|
|
134
|
+
refreshToken: viewerTokens.refreshToken,
|
|
135
|
+
expiresAt: viewerTokens.expiresAt,
|
|
136
|
+
createdAt: new Date(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const deployerTokens = await generateTokens(DEPLOYER_USER_ID, JWT_SECRET);
|
|
140
|
+
sessionStore.create({
|
|
141
|
+
id: "session-deployer",
|
|
142
|
+
userId: DEPLOYER_USER_ID,
|
|
143
|
+
token: deployerTokens.token,
|
|
144
|
+
refreshToken: deployerTokens.refreshToken,
|
|
145
|
+
expiresAt: deployerTokens.expiresAt,
|
|
146
|
+
createdAt: new Date(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
app,
|
|
151
|
+
viewerToken: viewerTokens.token,
|
|
152
|
+
deployerToken: deployerTokens.token,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ===========================================================================
|
|
157
|
+
// Tests
|
|
158
|
+
// ===========================================================================
|
|
159
|
+
|
|
160
|
+
describe("RBAC enforcement", () => {
|
|
161
|
+
let ctx: TestContext;
|
|
162
|
+
|
|
163
|
+
beforeEach(async () => {
|
|
164
|
+
ctx = await createTestServer();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(async () => {
|
|
168
|
+
await ctx.app.close();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// 401 — no auth
|
|
173
|
+
// -------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
describe("unauthenticated requests return 401", () => {
|
|
176
|
+
const routes: Array<{ method: "GET" | "POST" | "PUT" | "DELETE"; url: string }> = [
|
|
177
|
+
// Deployments
|
|
178
|
+
{ method: "GET", url: "/api/deployments" },
|
|
179
|
+
{ method: "POST", url: "/api/deployments" },
|
|
180
|
+
{ method: "GET", url: "/api/debrief" },
|
|
181
|
+
// Artifacts
|
|
182
|
+
{ method: "GET", url: "/api/artifacts" },
|
|
183
|
+
{ method: "POST", url: "/api/artifacts" },
|
|
184
|
+
{ method: "GET", url: "/api/artifacts/fake-id" },
|
|
185
|
+
{ method: "PUT", url: "/api/artifacts/fake-id" },
|
|
186
|
+
{ method: "DELETE", url: "/api/artifacts/fake-id" },
|
|
187
|
+
// Environments
|
|
188
|
+
{ method: "GET", url: "/api/environments" },
|
|
189
|
+
{ method: "POST", url: "/api/environments" },
|
|
190
|
+
{ method: "GET", url: "/api/environments/fake-id" },
|
|
191
|
+
{ method: "PUT", url: "/api/environments/fake-id" },
|
|
192
|
+
{ method: "DELETE", url: "/api/environments/fake-id" },
|
|
193
|
+
// Partitions
|
|
194
|
+
{ method: "GET", url: "/api/partitions" },
|
|
195
|
+
{ method: "POST", url: "/api/partitions" },
|
|
196
|
+
{ method: "GET", url: "/api/partitions/fake-id" },
|
|
197
|
+
{ method: "PUT", url: "/api/partitions/fake-id" },
|
|
198
|
+
{ method: "DELETE", url: "/api/partitions/fake-id" },
|
|
199
|
+
// Settings
|
|
200
|
+
{ method: "GET", url: "/api/settings" },
|
|
201
|
+
{ method: "PUT", url: "/api/settings" },
|
|
202
|
+
{ method: "GET", url: "/api/settings/command-info" },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
for (const { method, url } of routes) {
|
|
206
|
+
it(`${method} ${url} returns 401 without auth`, async () => {
|
|
207
|
+
const res = await ctx.app.inject({ method, url });
|
|
208
|
+
expect(res.statusCode).toBe(401);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// 403 — wrong permissions
|
|
215
|
+
// -------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
describe("viewer cannot perform write operations (403)", () => {
|
|
218
|
+
it("POST /api/deployments returns 403 for viewer", async () => {
|
|
219
|
+
const res = await ctx.app.inject({
|
|
220
|
+
method: "POST",
|
|
221
|
+
url: "/api/deployments",
|
|
222
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
223
|
+
payload: { artifactId: "a1", environmentId: "e1" },
|
|
224
|
+
});
|
|
225
|
+
expect(res.statusCode).toBe(403);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("POST /api/artifacts returns 403 for viewer", async () => {
|
|
229
|
+
const res = await ctx.app.inject({
|
|
230
|
+
method: "POST",
|
|
231
|
+
url: "/api/artifacts",
|
|
232
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
233
|
+
payload: { name: "test", type: "docker" },
|
|
234
|
+
});
|
|
235
|
+
expect(res.statusCode).toBe(403);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("POST /api/environments returns 403 for viewer", async () => {
|
|
239
|
+
const res = await ctx.app.inject({
|
|
240
|
+
method: "POST",
|
|
241
|
+
url: "/api/environments",
|
|
242
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
243
|
+
payload: { name: "staging" },
|
|
244
|
+
});
|
|
245
|
+
expect(res.statusCode).toBe(403);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("POST /api/partitions returns 403 for viewer", async () => {
|
|
249
|
+
const res = await ctx.app.inject({
|
|
250
|
+
method: "POST",
|
|
251
|
+
url: "/api/partitions",
|
|
252
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
253
|
+
payload: { name: "region-us" },
|
|
254
|
+
});
|
|
255
|
+
expect(res.statusCode).toBe(403);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("DELETE /api/artifacts/fake-id returns 403 for viewer", async () => {
|
|
259
|
+
const res = await ctx.app.inject({
|
|
260
|
+
method: "DELETE",
|
|
261
|
+
url: "/api/artifacts/fake-id",
|
|
262
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
263
|
+
});
|
|
264
|
+
expect(res.statusCode).toBe(403);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("DELETE /api/environments/fake-id returns 403 for viewer", async () => {
|
|
268
|
+
const res = await ctx.app.inject({
|
|
269
|
+
method: "DELETE",
|
|
270
|
+
url: "/api/environments/fake-id",
|
|
271
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
272
|
+
});
|
|
273
|
+
expect(res.statusCode).toBe(403);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("DELETE /api/partitions/fake-id returns 403 for viewer", async () => {
|
|
277
|
+
const res = await ctx.app.inject({
|
|
278
|
+
method: "DELETE",
|
|
279
|
+
url: "/api/partitions/fake-id",
|
|
280
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
281
|
+
});
|
|
282
|
+
expect(res.statusCode).toBe(403);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("deployer cannot access settings (403)", () => {
|
|
287
|
+
it("GET /api/settings returns 403 for deployer", async () => {
|
|
288
|
+
const res = await ctx.app.inject({
|
|
289
|
+
method: "GET",
|
|
290
|
+
url: "/api/settings",
|
|
291
|
+
headers: { authorization: `Bearer ${ctx.deployerToken}` },
|
|
292
|
+
});
|
|
293
|
+
expect(res.statusCode).toBe(403);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("PUT /api/settings returns 403 for deployer", async () => {
|
|
297
|
+
const res = await ctx.app.inject({
|
|
298
|
+
method: "PUT",
|
|
299
|
+
url: "/api/settings",
|
|
300
|
+
headers: { authorization: `Bearer ${ctx.deployerToken}` },
|
|
301
|
+
payload: { environmentsEnabled: true },
|
|
302
|
+
});
|
|
303
|
+
expect(res.statusCode).toBe(403);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("GET /api/settings/command-info returns 403 for deployer", async () => {
|
|
307
|
+
const res = await ctx.app.inject({
|
|
308
|
+
method: "GET",
|
|
309
|
+
url: "/api/settings/command-info",
|
|
310
|
+
headers: { authorization: `Bearer ${ctx.deployerToken}` },
|
|
311
|
+
});
|
|
312
|
+
expect(res.statusCode).toBe(403);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// -------------------------------------------------------------------------
|
|
317
|
+
// 200 — correct permissions succeed
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
describe("viewer can access read-only routes (200)", () => {
|
|
321
|
+
it("GET /api/deployments returns 200 for viewer", async () => {
|
|
322
|
+
const res = await ctx.app.inject({
|
|
323
|
+
method: "GET",
|
|
324
|
+
url: "/api/deployments",
|
|
325
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
326
|
+
});
|
|
327
|
+
expect(res.statusCode).toBe(200);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("GET /api/artifacts returns 200 for viewer", async () => {
|
|
331
|
+
const res = await ctx.app.inject({
|
|
332
|
+
method: "GET",
|
|
333
|
+
url: "/api/artifacts",
|
|
334
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
335
|
+
});
|
|
336
|
+
expect(res.statusCode).toBe(200);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("GET /api/environments returns 200 for viewer", async () => {
|
|
340
|
+
const res = await ctx.app.inject({
|
|
341
|
+
method: "GET",
|
|
342
|
+
url: "/api/environments",
|
|
343
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
344
|
+
});
|
|
345
|
+
expect(res.statusCode).toBe(200);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("GET /api/partitions returns 200 for viewer", async () => {
|
|
349
|
+
const res = await ctx.app.inject({
|
|
350
|
+
method: "GET",
|
|
351
|
+
url: "/api/partitions",
|
|
352
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
353
|
+
});
|
|
354
|
+
expect(res.statusCode).toBe(200);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("GET /api/debrief returns 200 for viewer", async () => {
|
|
358
|
+
const res = await ctx.app.inject({
|
|
359
|
+
method: "GET",
|
|
360
|
+
url: "/api/debrief",
|
|
361
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
362
|
+
});
|
|
363
|
+
expect(res.statusCode).toBe(200);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("deployer can create entities", () => {
|
|
368
|
+
it("POST /api/artifacts returns 201 for deployer", async () => {
|
|
369
|
+
const res = await ctx.app.inject({
|
|
370
|
+
method: "POST",
|
|
371
|
+
url: "/api/artifacts",
|
|
372
|
+
headers: { authorization: `Bearer ${ctx.deployerToken}` },
|
|
373
|
+
payload: { name: "my-app", type: "docker" },
|
|
374
|
+
});
|
|
375
|
+
expect(res.statusCode).toBe(201);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("POST /api/partitions returns 201 for deployer (deployer lacks partition.create)", async () => {
|
|
379
|
+
const res = await ctx.app.inject({
|
|
380
|
+
method: "POST",
|
|
381
|
+
url: "/api/partitions",
|
|
382
|
+
headers: { authorization: `Bearer ${ctx.deployerToken}` },
|
|
383
|
+
payload: { name: "region-us" },
|
|
384
|
+
});
|
|
385
|
+
// Deployer role does NOT have partition.create — should be 403
|
|
386
|
+
expect(res.statusCode).toBe(403);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// -------------------------------------------------------------------------
|
|
391
|
+
// 403 error format
|
|
392
|
+
// -------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
describe("403 response includes required permissions", () => {
|
|
395
|
+
it("includes required and message fields", async () => {
|
|
396
|
+
const res = await ctx.app.inject({
|
|
397
|
+
method: "POST",
|
|
398
|
+
url: "/api/partitions",
|
|
399
|
+
headers: { authorization: `Bearer ${ctx.viewerToken}` },
|
|
400
|
+
payload: { name: "test" },
|
|
401
|
+
});
|
|
402
|
+
expect(res.statusCode).toBe(403);
|
|
403
|
+
const body = JSON.parse(res.payload);
|
|
404
|
+
expect(body.error).toBe("Forbidden");
|
|
405
|
+
expect(body.required).toContain("partition.create");
|
|
406
|
+
expect(body.message).toBeTruthy();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { UpdateSettingsSchema } from "../src/api/schemas.js";
|
|
3
|
+
|
|
4
|
+
describe("SSRF URL validation", () => {
|
|
5
|
+
function validateEnvoyUrl(url: string) {
|
|
6
|
+
return UpdateSettingsSchema.safeParse({
|
|
7
|
+
envoy: { url },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
it("accepts valid external URLs", () => {
|
|
12
|
+
expect(validateEnvoyUrl("https://envoy.example.com").success).toBe(true);
|
|
13
|
+
expect(validateEnvoyUrl("http://deploy.company.com:8080").success).toBe(true);
|
|
14
|
+
expect(validateEnvoyUrl("https://203.0.113.50:3000").success).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("rejects localhost", () => {
|
|
18
|
+
expect(validateEnvoyUrl("http://localhost:3000").success).toBe(false);
|
|
19
|
+
expect(validateEnvoyUrl("http://127.0.0.1:8080").success).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("rejects private 10.x.x.x range", () => {
|
|
23
|
+
expect(validateEnvoyUrl("http://10.0.0.1:8080").success).toBe(false);
|
|
24
|
+
expect(validateEnvoyUrl("http://10.255.255.255").success).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects private 172.16-31.x.x range", () => {
|
|
28
|
+
expect(validateEnvoyUrl("http://172.16.0.1").success).toBe(false);
|
|
29
|
+
expect(validateEnvoyUrl("http://172.31.255.255").success).toBe(false);
|
|
30
|
+
// 172.15.x.x should be allowed
|
|
31
|
+
expect(validateEnvoyUrl("http://172.15.0.1").success).toBe(true);
|
|
32
|
+
// 172.32.x.x should be allowed
|
|
33
|
+
expect(validateEnvoyUrl("http://172.32.0.1").success).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects private 192.168.x.x range", () => {
|
|
37
|
+
expect(validateEnvoyUrl("http://192.168.1.1").success).toBe(false);
|
|
38
|
+
expect(validateEnvoyUrl("http://192.168.0.100").success).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects link-local / AWS metadata (169.254.x.x)", () => {
|
|
42
|
+
expect(validateEnvoyUrl("http://169.254.169.254/latest/meta-data/").success).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects non-http protocols", () => {
|
|
46
|
+
expect(validateEnvoyUrl("ftp://envoy.example.com").success).toBe(false);
|
|
47
|
+
expect(validateEnvoyUrl("file:///etc/passwd").success).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("validates MCP server URLs too", () => {
|
|
51
|
+
const result = UpdateSettingsSchema.safeParse({
|
|
52
|
+
mcpServers: [{ name: "evil", url: "http://169.254.169.254" }],
|
|
53
|
+
});
|
|
54
|
+
expect(result.success).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { DecisionDebrief } from "@synth-deploy/core";
|
|
3
|
+
import { InMemoryDeploymentStore } from "../src/agent/synth-agent.js";
|
|
4
|
+
import { markStaleDeployments } from "../src/agent/stale-deployment-detector.js";
|
|
5
|
+
import type { Deployment } from "@synth-deploy/core";
|
|
6
|
+
|
|
7
|
+
function makeDeployment(overrides: Partial<Deployment> = {}): Deployment {
|
|
8
|
+
return {
|
|
9
|
+
id: "dep-1",
|
|
10
|
+
operationId: "op-1",
|
|
11
|
+
partitionId: "part-1",
|
|
12
|
+
environmentId: "env-1",
|
|
13
|
+
version: "1.0",
|
|
14
|
+
status: "running",
|
|
15
|
+
variables: {},
|
|
16
|
+
debriefEntryIds: [],
|
|
17
|
+
orderId: null,
|
|
18
|
+
createdAt: new Date(),
|
|
19
|
+
completedAt: null,
|
|
20
|
+
failureReason: null,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("markStaleDeployments", () => {
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("marks running deployments older than threshold as failed", () => {
|
|
31
|
+
const deployments = new InMemoryDeploymentStore();
|
|
32
|
+
const debrief = new DecisionDebrief();
|
|
33
|
+
|
|
34
|
+
// Deployment created 35 minutes ago
|
|
35
|
+
const oldDate = new Date(Date.now() - 35 * 60 * 1000);
|
|
36
|
+
deployments.save(makeDeployment({ createdAt: oldDate }));
|
|
37
|
+
|
|
38
|
+
const count = markStaleDeployments(deployments, debrief, 30 * 60 * 1000);
|
|
39
|
+
|
|
40
|
+
expect(count).toBe(1);
|
|
41
|
+
expect(deployments.get("dep-1")?.status).toBe("failed");
|
|
42
|
+
expect(debrief.getRecent(1)[0].decisionType).toBe("deployment-failure");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not touch running deployments within threshold", () => {
|
|
46
|
+
const deployments = new InMemoryDeploymentStore();
|
|
47
|
+
const debrief = new DecisionDebrief();
|
|
48
|
+
|
|
49
|
+
// Deployment created 5 minutes ago
|
|
50
|
+
const recentDate = new Date(Date.now() - 5 * 60 * 1000);
|
|
51
|
+
deployments.save(makeDeployment({ createdAt: recentDate }));
|
|
52
|
+
|
|
53
|
+
const count = markStaleDeployments(deployments, debrief, 30 * 60 * 1000);
|
|
54
|
+
|
|
55
|
+
expect(count).toBe(0);
|
|
56
|
+
expect(deployments.get("dep-1")?.status).toBe("running");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not touch completed deployments", () => {
|
|
60
|
+
const deployments = new InMemoryDeploymentStore();
|
|
61
|
+
const debrief = new DecisionDebrief();
|
|
62
|
+
|
|
63
|
+
const oldDate = new Date(Date.now() - 60 * 60 * 1000);
|
|
64
|
+
deployments.save(makeDeployment({ status: "completed" as any, createdAt: oldDate }));
|
|
65
|
+
|
|
66
|
+
const count = markStaleDeployments(deployments, debrief, 30 * 60 * 1000);
|
|
67
|
+
|
|
68
|
+
expect(count).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("handles multiple stale deployments", () => {
|
|
72
|
+
const deployments = new InMemoryDeploymentStore();
|
|
73
|
+
const debrief = new DecisionDebrief();
|
|
74
|
+
|
|
75
|
+
const oldDate = new Date(Date.now() - 45 * 60 * 1000);
|
|
76
|
+
deployments.save(makeDeployment({ id: "d1", createdAt: oldDate }));
|
|
77
|
+
deployments.save(makeDeployment({ id: "d2", createdAt: oldDate }));
|
|
78
|
+
|
|
79
|
+
const count = markStaleDeployments(deployments, debrief, 30 * 60 * 1000);
|
|
80
|
+
|
|
81
|
+
expect(count).toBe(2);
|
|
82
|
+
expect(deployments.get("d1")?.status).toBe("failed");
|
|
83
|
+
expect(deployments.get("d2")?.status).toBe("failed");
|
|
84
|
+
});
|
|
85
|
+
});
|