Add missing resources to CLI, MCP, and n8n surfaces

Audit all three API surfaces against the console GraphQL schema and add
missing resources: asset, audit, datum, dpia, evidence upload, measure,
obligation, processing activity, rights request, snapshot, task, tia,
trust center (with references/files), and vendor management CLI
commands; MCP tools for deletes, rights requests, trust center, vendor
contacts/services, and compliance external URLs; n8n nodes for
obligation, finding, task, evidence, processing activity, dpia, tia,
rights request, snapshot, audit log, access review, organization
context, trust center, and additional control/measure/vendor operations.

Include MCP e2e test infrastructure (testutil MCP client with API key
auth and JSON-RPC session management) and tests covering all new MCP
tools.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-21 16:08:37 +02:00
parent f505e23cb0
commit 7be92defcc
220 changed files with 26395 additions and 7 deletions

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
@@ -193,6 +194,134 @@ func (c *Client) doRequest(
return respBody, resp.StatusCode, nil
}
// DoUpload sends a GraphQL multipart file upload request following the
// graphql-multipart-request-spec. It maps the given file to the variable
// at the provided path (e.g. "variables.input.file").
func (c *Client) DoUpload(
query string,
variables map[string]any,
varPath string,
filename string,
file io.Reader,
) (json.RawMessage, error) {
raw, err := c.doUploadRequest(query, variables, varPath, filename, file)
if err != nil {
return nil, err
}
var resp graphQLResponse
if err := json.Unmarshal(raw, &resp); err != nil {
return nil, fmt.Errorf("cannot parse GraphQL response: %w", err)
}
if len(resp.Errors) > 0 {
var msg strings.Builder
msg.WriteString(resp.Errors[0].Message)
for _, e := range resp.Errors[1:] {
msg.WriteString("; " + e.Message)
}
return nil, fmt.Errorf("GraphQL error: %s", msg.String())
}
return resp.Data, nil
}
func (c *Client) doUploadRequest(
query string,
variables map[string]any,
varPath string,
filename string,
file io.Reader,
) ([]byte, error) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// Part 1: operations
operationsJSON, err := json.Marshal(graphQLRequest{
Query: query,
Variables: variables,
})
if err != nil {
return nil, fmt.Errorf("cannot marshal operations: %w", err)
}
if err := writer.WriteField("operations", string(operationsJSON)); err != nil {
return nil, fmt.Errorf("cannot write operations field: %w", err)
}
// Part 2: map
mapJSON, err := json.Marshal(map[string][]string{
"0": {varPath},
})
if err != nil {
return nil, fmt.Errorf("cannot marshal map: %w", err)
}
if err := writer.WriteField("map", string(mapJSON)); err != nil {
return nil, fmt.Errorf("cannot write map field: %w", err)
}
// Part 3: file
part, err := writer.CreateFormFile("0", filename)
if err != nil {
return nil, fmt.Errorf("cannot create form file: %w", err)
}
if _, err := io.Copy(part, file); err != nil {
return nil, fmt.Errorf("cannot write file content: %w", err)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("cannot close multipart writer: %w", err)
}
host := c.host
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
host = "https://" + host
}
reqURL := host + c.endpoint
req, err := http.NewRequest(http.MethodPost, reqURL, &buf)
if err != nil {
return nil, fmt.Errorf("cannot create HTTP request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("User-Agent", version.UserAgent("prb"))
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot send HTTP request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("cannot read HTTP response: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized && c.refresher != nil {
if refreshErr := c.tryRefreshToken(); refreshErr == nil {
// Retry — but we can't re-read the file, so return the original error.
return nil, fmt.Errorf("authentication failed (HTTP 401): token was refreshed, please retry the command")
}
}
if resp.StatusCode != http.StatusOK {
switch resp.StatusCode {
case http.StatusUnauthorized:
return nil, fmt.Errorf("authentication failed (HTTP 401): token may be invalid or expired, try 'prb auth login'")
case http.StatusForbidden:
return nil, fmt.Errorf("access denied (HTTP 403): you do not have permission to perform this action")
default:
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
}
}
return respBody, nil
}
func (c *Client) tryRefreshToken() error {
r := c.refresher