From 50d44fe66617a9de0407c4996d77cfd10020152f Mon Sep 17 00:00:00 2001 From: Hans Bakker Date: Tue, 27 Oct 2026 09:39:17 +0000 Subject: [PATCH 1/4] Add Aspire recording commands via DevProxy API client --- DevProxy.Hosting/DevProxy.Hosting.csproj | 1 + DevProxy.Hosting/DevProxyApiClient.cs | 121 +++++++++ DevProxy.Hosting/DevProxyCommandExtensions.cs | 75 ++++++ DevProxy.Hosting/DevProxyContainerResource.cs | 2 +- .../DevProxyExecutableResource.cs | 2 +- .../DevProxyResourceBuilderExtensions.cs | 6 +- DevProxy.Hosting/IDevProxyApiResource.cs | 10 + README.md | 3 + assets/openapi.yaml | 250 ++++++++++++++++++ 9 files changed, 466 insertions(+), 4 deletions(-) create mode 100644 DevProxy.Hosting/DevProxyApiClient.cs create mode 100644 DevProxy.Hosting/DevProxyCommandExtensions.cs create mode 100644 DevProxy.Hosting/IDevProxyApiResource.cs create mode 100644 assets/openapi.yaml diff --git a/DevProxy.Hosting/DevProxy.Hosting.csproj b/DevProxy.Hosting/DevProxy.Hosting.csproj index 02ce98e..7424da8 100644 --- a/DevProxy.Hosting/DevProxy.Hosting.csproj +++ b/DevProxy.Hosting/DevProxy.Hosting.csproj @@ -26,6 +26,7 @@ + diff --git a/DevProxy.Hosting/DevProxyApiClient.cs b/DevProxy.Hosting/DevProxyApiClient.cs new file mode 100644 index 0000000..0abe941 --- /dev/null +++ b/DevProxy.Hosting/DevProxyApiClient.cs @@ -0,0 +1,121 @@ +using Refit; + +namespace DevProxy.Hosting; + +/// +/// Typed client for the Dev Proxy management API. +/// +public interface IDevProxyApiClient +{ + /// + /// Gets the current proxy state and configuration file. + /// + [Get("/proxy")] + Task GetProxyAsync(CancellationToken cancellationToken = default); + + /// + /// Updates the proxy recording state. + /// + [Post("/proxy")] + Task SetProxyAsync([Body] DevProxyUpdate update, CancellationToken cancellationToken = default); + + /// + /// Raises a mock request. + /// + [Post("/proxy/mockRequest")] + Task RaiseMockRequestAsync(CancellationToken cancellationToken = default); + + /// + /// Stops Dev Proxy. + /// + [Post("/proxy/stopProxy")] + Task StopProxyAsync(CancellationToken cancellationToken = default); + + /// + /// Creates a JWT using the supplied options. + /// + [Post("/proxy/jwtToken")] + Task CreateJwtTokenAsync([Body] DevProxyJwtOptions options, CancellationToken cancellationToken = default); + + /// + /// Gets the Dev Proxy root certificate in PEM format. + /// + [Get("/proxy/rootCertificate")] + Task GetRootCertificateAsync([Query] string format, CancellationToken cancellationToken = default); + + /// + /// Gets or follows Dev Proxy logs. + /// + [Get("/proxy/logs")] + Task GetLogsAsync( + [Query] int? lines = null, + [Query] bool follow = false, + [Query] string? since = null, + CancellationToken cancellationToken = default); +} + +/// +/// Represents the Dev Proxy state returned by the management API. +/// +public sealed class DevProxyInfo +{ + /// + /// Gets or sets whether Dev Proxy is recording. + /// + public bool? Recording { get; set; } + + /// + /// Gets or sets the active configuration file. + /// + public string? ConfigFile { get; set; } +} + +/// +/// Represents a request to update Dev Proxy state. +/// +public sealed class DevProxyUpdate +{ + /// + /// Gets or sets the requested recording state. + /// + public bool? Recording { get; set; } +} + +/// +/// Represents options for creating a Dev Proxy JWT. +/// +public sealed class DevProxyJwtOptions +{ + /// Gets or sets the token audiences. + public IEnumerable? Audiences { get; set; } + + /// Gets or sets the token claims. + public Dictionary? Claims { get; set; } + + /// Gets or sets the token issuer. + public string? Issuer { get; set; } + + /// Gets or sets the token name. + public string? Name { get; set; } + + /// Gets or sets the token roles. + public IEnumerable? Roles { get; set; } + + /// Gets or sets the token scopes. + public IEnumerable? Scopes { get; set; } + + /// Gets or sets the signing key, which must contain at least 32 characters. + public string? SigningKey { get; set; } + + /// Gets or sets the token lifetime in minutes. + public double? ValidFor { get; set; } +} + +/// +/// Represents a generated JWT. +/// +public sealed class DevProxyJwtToken +{ + /// Gets or sets the generated token. + public string Token { get; set; } = ""; +} \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyCommandExtensions.cs b/DevProxy.Hosting/DevProxyCommandExtensions.cs new file mode 100644 index 0000000..87c0bf2 --- /dev/null +++ b/DevProxy.Hosting/DevProxyCommandExtensions.cs @@ -0,0 +1,75 @@ +using Aspire.Hosting.ApplicationModel; +using DevProxy.Hosting; +using Refit; + +#pragma warning disable IDE0130 +namespace Aspire.Hosting; +#pragma warning restore IDE0130 + +/// +/// Adds dashboard commands for controlling Dev Proxy recording. +/// +public static class DevProxyCommandExtensions +{ + /// + /// Adds commands to start and stop recording on a Dev Proxy resource. + /// + /// The Dev Proxy resource type. + /// The Dev Proxy resource builder. + /// The resource builder for method chaining. + public static IResourceBuilder WithDevProxyRecordingCommands( + this IResourceBuilder builder) + where TResource : IDevProxyApiResource + { + builder.WithCommand( + name: "start-recording", + displayName: "Start recording", + executeCommand: context => SetRecordingAsync(builder, context, recording: true), + commandOptions: new CommandOptions()); + + builder.WithCommand( + name: "stop-recording", + displayName: "Stop recording", + executeCommand: context => SetRecordingAsync(builder, context, recording: false), + commandOptions: new CommandOptions()); + + return builder; + } + + private static async Task SetRecordingAsync( + IResourceBuilder builder, + ExecuteCommandContext context, + bool recording) + where TResource : IDevProxyApiResource + { + var endpointUrl = await builder.Resource + .GetEndpoint(DevProxyResource.ApiEndpointName) + .GetValueAsync(context.CancellationToken); + + if (string.IsNullOrWhiteSpace(endpointUrl)) + { + return CommandResults.Failure("The Dev Proxy API endpoint is not available."); + } + + using var httpClient = new HttpClient { BaseAddress = new Uri(endpointUrl) }; + var apiClient = RestService.ForGenerated(httpClient); + + try + { + await apiClient.SetProxyAsync( + new DevProxyUpdate { Recording = recording }, + context.CancellationToken); + + return CommandResults.Success(recording ? "Recording started." : "Recording stopped."); + } + catch (ApiException exception) + { + return CommandResults.Failure( + $"Dev Proxy returned {(int)exception.StatusCode} {exception.ReasonPhrase}: {exception.Content}"); + } + catch (HttpRequestException exception) + { + return CommandResults.Failure($"Could not reach the Dev Proxy API: {exception.Message}"); + } + } +} \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyContainerResource.cs b/DevProxy.Hosting/DevProxyContainerResource.cs index facd019..dd774cc 100644 --- a/DevProxy.Hosting/DevProxyContainerResource.cs +++ b/DevProxy.Hosting/DevProxyContainerResource.cs @@ -6,6 +6,6 @@ namespace DevProxy.Hosting; /// Exposes Dev Proxy as a Docker container. /// public sealed class DevProxyContainerResource(string name) : - ContainerResource(name) + ContainerResource(name), IDevProxyApiResource { } \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyExecutableResource.cs b/DevProxy.Hosting/DevProxyExecutableResource.cs index 2f65acb..dd2fcd3 100644 --- a/DevProxy.Hosting/DevProxyExecutableResource.cs +++ b/DevProxy.Hosting/DevProxyExecutableResource.cs @@ -6,6 +6,6 @@ namespace DevProxy.Hosting; /// Exposes Dev Proxy through its locally installed executable. /// public sealed class DevProxyExecutableResource(string name) : - ExecutableResource(name, "devproxy", "") + ExecutableResource(name, "devproxy", ""), IDevProxyApiResource { } \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs index e81b574..af5bbfd 100644 --- a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs +++ b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs @@ -25,7 +25,8 @@ public static IResourceBuilder AddDevProxyExecutable var resourceBuilder = builder.AddResource(resource) .WithArgs("--as-system-proxy", "false") .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) - .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName); + .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) + .WithDevProxyRecordingCommands(); return resourceBuilder; } @@ -77,7 +78,8 @@ public static IResourceBuilder AddDevProxyContainer( .WithImageRegistry(DevProxyContainerImageTags.Registry) .WithImageTag(DevProxyContainerImageTags.Tag) .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) - .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName); + .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) + .WithDevProxyRecordingCommands(); return resourceBuilder; } diff --git a/DevProxy.Hosting/IDevProxyApiResource.cs b/DevProxy.Hosting/IDevProxyApiResource.cs new file mode 100644 index 0000000..b0fbfb9 --- /dev/null +++ b/DevProxy.Hosting/IDevProxyApiResource.cs @@ -0,0 +1,10 @@ +using Aspire.Hosting.ApplicationModel; + +namespace DevProxy.Hosting; + +/// +/// A Dev Proxy resource exposing its management API. +/// +public interface IDevProxyApiResource : IResourceWithEndpoints +{ +} \ No newline at end of file diff --git a/README.md b/README.md index ea1d596..3096f7f 100644 --- a/README.md +++ b/README.md @@ -91,9 +91,12 @@ builder.Build().Run(); - **Container Resource**: Use `AddDevProxyContainer` to run Dev Proxy as a containerized service. - **Executable Resource**: Use `AddDevProxyExecutable` to run Dev Proxy from the locally installed executable. +- **Recording Commands**: Start and stop Dev Proxy recording from the Aspire dashboard for either resource type. - **Custom Configuration**: Use the extension methods to configure Dev Proxy as needed. - **Service Integration**: Easily integrate Dev Proxy with other services in your application. +Both Dev Proxy resource types expose `Start recording` and `Stop recording` commands in the Aspire dashboard. The management API contract is available in the repository at `assets/openapi.yaml` and is not included in the NuGet package. + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/assets/openapi.yaml b/assets/openapi.yaml new file mode 100644 index 0000000..111f7c2 --- /dev/null +++ b/assets/openapi.yaml @@ -0,0 +1,250 @@ +openapi: 3.0.3 +info: + title: Dev Proxy Management API + version: 1.0.0 + description: HTTP API exposed by Dev Proxy for inspecting and controlling the proxy process. +servers: + - url: http://localhost:8897 + description: Default local management API endpoint +paths: + /proxy: + get: + operationId: getProxy + summary: Get proxy state + responses: + '200': + description: Current proxy state. + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyInfo' + post: + operationId: setProxy + summary: Update proxy state + description: Set recording to true to start recording or false to stop recording. ConfigFile cannot be updated through this operation. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyUpdate' + responses: + '200': + description: Updated proxy state. + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyInfo' + '400': + description: Invalid proxy state update. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /proxy/mockRequest: + post: + operationId: raiseMockRequest + summary: Raise a mock request + responses: + '204': + description: Mock request raised. + /proxy/stopProxy: + post: + operationId: stopProxy + summary: Stop Dev Proxy + responses: + '202': + description: Proxy shutdown was requested. + /proxy/jwtToken: + post: + operationId: createJwtToken + summary: Create a JWT + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/JwtOptions' + responses: + '200': + description: Generated JWT. + content: + application/json: + schema: + $ref: '#/components/schemas/JwtInfo' + '400': + description: Invalid JWT options. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /proxy/rootCertificate: + get: + operationId: getRootCertificate + summary: Download the root certificate + parameters: + - name: format + in: query + required: true + schema: + type: string + enum: [crt] + responses: + '200': + description: Root certificate encoded as PEM. + content: + application/x-x509-ca-cert: + schema: + type: string + format: binary + headers: + Content-Disposition: + schema: + type: string + '400': + description: Missing or unsupported certificate format. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /proxy/logs: + get: + operationId: getLogs + summary: Read or follow logs + description: Available only when Dev Proxy runs in detached mode. Set follow=true or accept text/event-stream to stream server-sent events. + parameters: + - name: lines + in: query + required: false + schema: + type: integer + format: int32 + default: 50 + - name: follow + in: query + required: false + schema: + type: boolean + default: false + - name: since + in: query + required: false + description: Optional time or duration filter understood by Dev Proxy. + schema: + type: string + - name: Accept + in: header + required: false + schema: + type: string + enum: [text/plain, application/json, text/event-stream] + responses: + '200': + description: Log lines, JSON log entries, or a server-sent event stream. + content: + text/plain: + schema: + type: string + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/LogEntry' + text/event-stream: + schema: + type: string + '404': + description: Logs are unavailable outside detached mode or the log file does not exist. +components: + schemas: + ProxyInfo: + type: object + properties: + recording: + type: boolean + nullable: true + configFile: + type: string + nullable: true + ProxyUpdate: + type: object + properties: + recording: + type: boolean + nullable: true + JwtOptions: + type: object + properties: + audiences: + type: array + items: + type: string + claims: + type: object + additionalProperties: + type: string + issuer: + type: string + nullable: true + name: + type: string + nullable: true + roles: + type: array + items: + type: string + scopes: + type: array + items: + type: string + signingKey: + type: string + minLength: 32 + nullable: true + validFor: + type: number + format: double + nullable: true + JwtInfo: + type: object + required: [token] + properties: + token: + type: string + LogEntry: + type: object + properties: + time: + type: string + nullable: true + level: + type: string + nullable: true + category: + type: string + nullable: true + message: + type: string + nullable: true + raw: + type: string + nullable: true + ProblemDetails: + type: object + properties: + type: + type: string + nullable: true + title: + type: string + nullable: true + status: + type: integer + format: int32 + nullable: true + detail: + type: string + nullable: true + instance: + type: string + nullable: true \ No newline at end of file From 65f29561c039f7ff956be38b11941a4ab2d79476 Mon Sep 17 00:00:00 2001 From: Hans Bakker Date: Tue, 27 Oct 2026 10:19:11 +0000 Subject: [PATCH 2/4] Add authentication requirement to OpenAPI spec and client --- DevProxy.Hosting/DevProxyApiClient.cs | 1 + assets/openapi.yaml | 28 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/DevProxy.Hosting/DevProxyApiClient.cs b/DevProxy.Hosting/DevProxyApiClient.cs index 0abe941..ed14999 100644 --- a/DevProxy.Hosting/DevProxyApiClient.cs +++ b/DevProxy.Hosting/DevProxyApiClient.cs @@ -5,6 +5,7 @@ namespace DevProxy.Hosting; /// /// Typed client for the Dev Proxy management API. /// +[Headers("Authorization: Bearer")] public interface IDevProxyApiClient { /// diff --git a/assets/openapi.yaml b/assets/openapi.yaml index 111f7c2..5e1bef7 100644 --- a/assets/openapi.yaml +++ b/assets/openapi.yaml @@ -6,6 +6,8 @@ info: servers: - url: http://localhost:8897 description: Default local management API endpoint +security: + - bearerAuth: [] paths: /proxy: get: @@ -18,6 +20,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ProxyInfo' + '401': + $ref: '#/components/responses/Unauthorized' post: operationId: setProxy summary: Update proxy state @@ -41,6 +45,8 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' + '401': + $ref: '#/components/responses/Unauthorized' /proxy/mockRequest: post: operationId: raiseMockRequest @@ -48,6 +54,8 @@ paths: responses: '204': description: Mock request raised. + '401': + $ref: '#/components/responses/Unauthorized' /proxy/stopProxy: post: operationId: stopProxy @@ -55,6 +63,8 @@ paths: responses: '202': description: Proxy shutdown was requested. + '401': + $ref: '#/components/responses/Unauthorized' /proxy/jwtToken: post: operationId: createJwtToken @@ -78,6 +88,8 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' + '401': + $ref: '#/components/responses/Unauthorized' /proxy/rootCertificate: get: operationId: getRootCertificate @@ -107,6 +119,8 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' + '401': + $ref: '#/components/responses/Unauthorized' /proxy/logs: get: operationId: getLogs @@ -155,7 +169,21 @@ paths: type: string '404': description: Logs are unavailable outside detached mode or the log file does not exist. + '401': + $ref: '#/components/responses/Unauthorized' components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + description: Per-instance token returned by `devproxy api token`. + responses: + Unauthorized: + description: Missing or invalid bearer token. + headers: + WWW-Authenticate: + schema: + type: string schemas: ProxyInfo: type: object From 61e88f7fd2c4f018a9f39c6c26230d5a0a292392 Mon Sep 17 00:00:00 2001 From: Hans Bakker Date: Tue, 27 Oct 2026 10:19:28 +0000 Subject: [PATCH 3/4] Update resources with API token behavior --- DevProxy.Hosting/DevProxyApiTokenStore.cs | 50 +++++++++++++++++++ DevProxy.Hosting/DevProxyCommandExtensions.cs | 18 ++++++- DevProxy.Hosting/DevProxyContainerResource.cs | 22 +++++++- .../DevProxyExecutableResource.cs | 5 ++ .../DevProxyResourceBuilderExtensions.cs | 8 ++- DevProxy.Hosting/IDevProxyApiResource.cs | 10 ++++ 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 DevProxy.Hosting/DevProxyApiTokenStore.cs diff --git a/DevProxy.Hosting/DevProxyApiTokenStore.cs b/DevProxy.Hosting/DevProxyApiTokenStore.cs new file mode 100644 index 0000000..1e51b3f --- /dev/null +++ b/DevProxy.Hosting/DevProxyApiTokenStore.cs @@ -0,0 +1,50 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace DevProxy.Hosting; + +internal static class DevProxyApiTokenStore +{ + internal static string CreateTemporaryTokenDirectory() + { + var directory = Path.Combine(Path.GetTempPath(), "devproxy-aspire", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + return directory; + } + + internal static async ValueTask GetApiKeyAsync( + string tokenDirectory, + Uri apiEndpoint, + CancellationToken cancellationToken) + { + if (!Directory.Exists(tokenDirectory)) + { + throw new InvalidOperationException("The Dev Proxy API token directory is not available."); + } + + var tokenFiles = Directory.EnumerateFiles(tokenDirectory, "api-*.token").ToArray(); + if (tokenFiles.Length == 0) + { + throw new InvalidOperationException("The Dev Proxy API token is not available yet."); + } + + using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false }); + foreach (var tokenFile in tokenFiles) + { + var token = (await File.ReadAllTextAsync(tokenFile, cancellationToken)).Trim(); + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(apiEndpoint, "/proxy")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + using var response = await httpClient.SendAsync(request, cancellationToken); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + continue; + } + + response.EnsureSuccessStatusCode(); + return token; + } + + throw new InvalidOperationException("No API token was found for this Dev Proxy resource."); + } +} \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyCommandExtensions.cs b/DevProxy.Hosting/DevProxyCommandExtensions.cs index 87c0bf2..2884d68 100644 --- a/DevProxy.Hosting/DevProxyCommandExtensions.cs +++ b/DevProxy.Hosting/DevProxyCommandExtensions.cs @@ -51,8 +51,18 @@ private static async Task SetRecordingAsync( return CommandResults.Failure("The Dev Proxy API endpoint is not available."); } - using var httpClient = new HttpClient { BaseAddress = new Uri(endpointUrl) }; - var apiClient = RestService.ForGenerated(httpClient); + var apiEndpoint = new Uri(endpointUrl); + using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false }) + { + BaseAddress = apiEndpoint + }; + var apiClient = RestService.ForGenerated( + httpClient, + new RefitSettings + { + AuthorizationHeaderValueGetter = (_, cancellationToken) => + builder.Resource.GetApiKeyAsync(apiEndpoint, cancellationToken) + }); try { @@ -67,6 +77,10 @@ await apiClient.SetProxyAsync( return CommandResults.Failure( $"Dev Proxy returned {(int)exception.StatusCode} {exception.ReasonPhrase}: {exception.Content}"); } + catch (InvalidOperationException exception) + { + return CommandResults.Failure(exception.Message); + } catch (HttpRequestException exception) { return CommandResults.Failure($"Could not reach the Dev Proxy API: {exception.Message}"); diff --git a/DevProxy.Hosting/DevProxyContainerResource.cs b/DevProxy.Hosting/DevProxyContainerResource.cs index dd774cc..89f71f2 100644 --- a/DevProxy.Hosting/DevProxyContainerResource.cs +++ b/DevProxy.Hosting/DevProxyContainerResource.cs @@ -5,7 +5,25 @@ namespace DevProxy.Hosting; /// /// Exposes Dev Proxy as a Docker container. /// -public sealed class DevProxyContainerResource(string name) : - ContainerResource(name), IDevProxyApiResource +public sealed class DevProxyContainerResource : ContainerResource, IDevProxyApiResource { + private readonly string _apiTokenDirectory; + + /// + /// Initializes a container resource with an isolated temporary API-token directory. + /// + public DevProxyContainerResource(string name) + : this(name, DevProxyApiTokenStore.CreateTemporaryTokenDirectory()) + { + } + + internal DevProxyContainerResource(string name, string apiConfigDirectory) + : base(name) + { + _apiTokenDirectory = apiConfigDirectory; + Directory.CreateDirectory(_apiTokenDirectory); + } + + /// + public string ApiTokenDirectory => _apiTokenDirectory; } \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyExecutableResource.cs b/DevProxy.Hosting/DevProxyExecutableResource.cs index dd2fcd3..cd8475c 100644 --- a/DevProxy.Hosting/DevProxyExecutableResource.cs +++ b/DevProxy.Hosting/DevProxyExecutableResource.cs @@ -8,4 +8,9 @@ namespace DevProxy.Hosting; public sealed class DevProxyExecutableResource(string name) : ExecutableResource(name, "devproxy", ""), IDevProxyApiResource { + /// + public string ApiTokenDirectory { get; } = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "dev-proxy", + "credentials"); } \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs index af5bbfd..32cebd9 100644 --- a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs +++ b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs @@ -71,12 +71,18 @@ public static IResourceBuilder AddDevProxyContainer( this IDistributedApplicationBuilder builder, [ResourceName] string name) { - var resource = new DevProxyContainerResource(name); + var apiTokenDirectory = Path.Combine( + builder.AppHostDirectory, + "obj", + "devproxy-api", + name); + var resource = new DevProxyContainerResource(name, apiTokenDirectory); var resourceBuilder = builder.AddResource(resource) .WithImage(DevProxyContainerImageTags.Image) .WithImageRegistry(DevProxyContainerImageTags.Registry) .WithImageTag(DevProxyContainerImageTags.Tag) + .WithBindMount(resource.ApiTokenDirectory, "/home/devproxy/.config/dev-proxy/credentials") .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) .WithDevProxyRecordingCommands(); diff --git a/DevProxy.Hosting/IDevProxyApiResource.cs b/DevProxy.Hosting/IDevProxyApiResource.cs index b0fbfb9..bec93b4 100644 --- a/DevProxy.Hosting/IDevProxyApiResource.cs +++ b/DevProxy.Hosting/IDevProxyApiResource.cs @@ -7,4 +7,14 @@ namespace DevProxy.Hosting; /// public interface IDevProxyApiResource : IResourceWithEndpoints { + /// + /// Gets the directory containing this resource's Dev Proxy API token files. + /// + string ApiTokenDirectory { get; } + + /// + /// Gets the API token that authenticates against this resource's API endpoint. + /// + ValueTask GetApiKeyAsync(Uri apiEndpoint, CancellationToken cancellationToken = default) => + DevProxyApiTokenStore.GetApiKeyAsync(ApiTokenDirectory, apiEndpoint, cancellationToken); } \ No newline at end of file From dd801620ad9f303d722c4410fe8e55845db66d00 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Tue, 27 Oct 2026 12:48:51 +0100 Subject: [PATCH 4/4] Simplify recording commands to use an AppHost-provided API token - Generate the Dev Proxy API token in the AppHost and pass it to Dev Proxy via DEV_PROXY_API_TOKEN instead of reading token files from disk - Call the API with HttpClient + System.Net.Http.Json and drop Refit - Keep the API client types and command extension internal - Disable the commands unless the resource is running and add icons - Remove assets/openapi.yaml Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- DevProxy.Hosting/DevProxy.Hosting.csproj | 1 - DevProxy.Hosting/DevProxyApiClient.cs | 122 -------- DevProxy.Hosting/DevProxyApiTokenStore.cs | 50 ---- DevProxy.Hosting/DevProxyCommandExtensions.cs | 128 +++++--- DevProxy.Hosting/DevProxyContainerResource.cs | 22 +- .../DevProxyExecutableResource.cs | 7 +- .../DevProxyResourceBuilderExtensions.cs | 12 +- DevProxy.Hosting/IDevProxyApiResource.cs | 20 -- README.md | 2 +- assets/openapi.yaml | 278 ------------------ 10 files changed, 86 insertions(+), 556 deletions(-) delete mode 100644 DevProxy.Hosting/DevProxyApiClient.cs delete mode 100644 DevProxy.Hosting/DevProxyApiTokenStore.cs delete mode 100644 DevProxy.Hosting/IDevProxyApiResource.cs delete mode 100644 assets/openapi.yaml diff --git a/DevProxy.Hosting/DevProxy.Hosting.csproj b/DevProxy.Hosting/DevProxy.Hosting.csproj index 7424da8..02ce98e 100644 --- a/DevProxy.Hosting/DevProxy.Hosting.csproj +++ b/DevProxy.Hosting/DevProxy.Hosting.csproj @@ -26,7 +26,6 @@ - diff --git a/DevProxy.Hosting/DevProxyApiClient.cs b/DevProxy.Hosting/DevProxyApiClient.cs deleted file mode 100644 index ed14999..0000000 --- a/DevProxy.Hosting/DevProxyApiClient.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Refit; - -namespace DevProxy.Hosting; - -/// -/// Typed client for the Dev Proxy management API. -/// -[Headers("Authorization: Bearer")] -public interface IDevProxyApiClient -{ - /// - /// Gets the current proxy state and configuration file. - /// - [Get("/proxy")] - Task GetProxyAsync(CancellationToken cancellationToken = default); - - /// - /// Updates the proxy recording state. - /// - [Post("/proxy")] - Task SetProxyAsync([Body] DevProxyUpdate update, CancellationToken cancellationToken = default); - - /// - /// Raises a mock request. - /// - [Post("/proxy/mockRequest")] - Task RaiseMockRequestAsync(CancellationToken cancellationToken = default); - - /// - /// Stops Dev Proxy. - /// - [Post("/proxy/stopProxy")] - Task StopProxyAsync(CancellationToken cancellationToken = default); - - /// - /// Creates a JWT using the supplied options. - /// - [Post("/proxy/jwtToken")] - Task CreateJwtTokenAsync([Body] DevProxyJwtOptions options, CancellationToken cancellationToken = default); - - /// - /// Gets the Dev Proxy root certificate in PEM format. - /// - [Get("/proxy/rootCertificate")] - Task GetRootCertificateAsync([Query] string format, CancellationToken cancellationToken = default); - - /// - /// Gets or follows Dev Proxy logs. - /// - [Get("/proxy/logs")] - Task GetLogsAsync( - [Query] int? lines = null, - [Query] bool follow = false, - [Query] string? since = null, - CancellationToken cancellationToken = default); -} - -/// -/// Represents the Dev Proxy state returned by the management API. -/// -public sealed class DevProxyInfo -{ - /// - /// Gets or sets whether Dev Proxy is recording. - /// - public bool? Recording { get; set; } - - /// - /// Gets or sets the active configuration file. - /// - public string? ConfigFile { get; set; } -} - -/// -/// Represents a request to update Dev Proxy state. -/// -public sealed class DevProxyUpdate -{ - /// - /// Gets or sets the requested recording state. - /// - public bool? Recording { get; set; } -} - -/// -/// Represents options for creating a Dev Proxy JWT. -/// -public sealed class DevProxyJwtOptions -{ - /// Gets or sets the token audiences. - public IEnumerable? Audiences { get; set; } - - /// Gets or sets the token claims. - public Dictionary? Claims { get; set; } - - /// Gets or sets the token issuer. - public string? Issuer { get; set; } - - /// Gets or sets the token name. - public string? Name { get; set; } - - /// Gets or sets the token roles. - public IEnumerable? Roles { get; set; } - - /// Gets or sets the token scopes. - public IEnumerable? Scopes { get; set; } - - /// Gets or sets the signing key, which must contain at least 32 characters. - public string? SigningKey { get; set; } - - /// Gets or sets the token lifetime in minutes. - public double? ValidFor { get; set; } -} - -/// -/// Represents a generated JWT. -/// -public sealed class DevProxyJwtToken -{ - /// Gets or sets the generated token. - public string Token { get; set; } = ""; -} \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyApiTokenStore.cs b/DevProxy.Hosting/DevProxyApiTokenStore.cs deleted file mode 100644 index 1e51b3f..0000000 --- a/DevProxy.Hosting/DevProxyApiTokenStore.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; - -namespace DevProxy.Hosting; - -internal static class DevProxyApiTokenStore -{ - internal static string CreateTemporaryTokenDirectory() - { - var directory = Path.Combine(Path.GetTempPath(), "devproxy-aspire", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(directory); - return directory; - } - - internal static async ValueTask GetApiKeyAsync( - string tokenDirectory, - Uri apiEndpoint, - CancellationToken cancellationToken) - { - if (!Directory.Exists(tokenDirectory)) - { - throw new InvalidOperationException("The Dev Proxy API token directory is not available."); - } - - var tokenFiles = Directory.EnumerateFiles(tokenDirectory, "api-*.token").ToArray(); - if (tokenFiles.Length == 0) - { - throw new InvalidOperationException("The Dev Proxy API token is not available yet."); - } - - using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false }); - foreach (var tokenFile in tokenFiles) - { - var token = (await File.ReadAllTextAsync(tokenFile, cancellationToken)).Trim(); - using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(apiEndpoint, "/proxy")); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - - using var response = await httpClient.SendAsync(request, cancellationToken); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - continue; - } - - response.EnsureSuccessStatusCode(); - return token; - } - - throw new InvalidOperationException("No API token was found for this Dev Proxy resource."); - } -} \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyCommandExtensions.cs b/DevProxy.Hosting/DevProxyCommandExtensions.cs index 2884d68..b9f2b12 100644 --- a/DevProxy.Hosting/DevProxyCommandExtensions.cs +++ b/DevProxy.Hosting/DevProxyCommandExtensions.cs @@ -1,89 +1,119 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; using Aspire.Hosting.ApplicationModel; using DevProxy.Hosting; -using Refit; #pragma warning disable IDE0130 namespace Aspire.Hosting; #pragma warning restore IDE0130 /// -/// Adds dashboard commands for controlling Dev Proxy recording. +/// Adds dashboard commands for controlling Dev Proxy through its API. /// -public static class DevProxyCommandExtensions +internal static class DevProxyCommandExtensions { + internal const string ApiTokenEnvironmentVariable = "DEV_PROXY_API_TOKEN"; + /// - /// Adds commands to start and stop recording on a Dev Proxy resource. + /// Generates an API token for the Dev Proxy resource, passes it to Dev Proxy + /// and adds commands to start and stop recording. /// - /// The Dev Proxy resource type. - /// The Dev Proxy resource builder. - /// The resource builder for method chaining. - public static IResourceBuilder WithDevProxyRecordingCommands( + internal static IResourceBuilder WithRecordingCommands( this IResourceBuilder builder) - where TResource : IDevProxyApiResource + where TResource : IResourceWithEndpoints, IResourceWithEnvironment { + var apiToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); + + builder.WithEnvironment(ApiTokenEnvironmentVariable, apiToken); + builder.WithCommand( name: "start-recording", displayName: "Start recording", - executeCommand: context => SetRecordingAsync(builder, context, recording: true), - commandOptions: new CommandOptions()); + executeCommand: context => SetRecordingAsync(builder.Resource, apiToken, recording: true, context), + commandOptions: new CommandOptions + { + Description = "Start recording requests in Dev Proxy", + IconName = "Record", + UpdateState = UpdateCommandState + }); builder.WithCommand( name: "stop-recording", displayName: "Stop recording", - executeCommand: context => SetRecordingAsync(builder, context, recording: false), - commandOptions: new CommandOptions()); + executeCommand: context => SetRecordingAsync(builder.Resource, apiToken, recording: false, context), + commandOptions: new CommandOptions + { + Description = "Stop recording requests in Dev Proxy", + IconName = "RecordStop", + UpdateState = UpdateCommandState + }); return builder; } - private static async Task SetRecordingAsync( - IResourceBuilder builder, - ExecuteCommandContext context, - bool recording) - where TResource : IDevProxyApiResource - { - var endpointUrl = await builder.Resource - .GetEndpoint(DevProxyResource.ApiEndpointName) - .GetValueAsync(context.CancellationToken); + private static ResourceCommandState UpdateCommandState(UpdateCommandStateContext context) => + context.ResourceSnapshot.State?.Text == KnownResourceStates.Running + ? ResourceCommandState.Enabled + : ResourceCommandState.Disabled; - if (string.IsNullOrWhiteSpace(endpointUrl)) + private static async Task SetRecordingAsync( + IResourceWithEndpoints resource, + string apiToken, + bool recording, + ExecuteCommandContext context) + { + try { - return CommandResults.Failure("The Dev Proxy API endpoint is not available."); - } + var endpointUrl = await resource + .GetEndpoint(DevProxyResource.ApiEndpointName) + .GetValueAsync(context.CancellationToken); - var apiEndpoint = new Uri(endpointUrl); - using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false }) - { - BaseAddress = apiEndpoint - }; - var apiClient = RestService.ForGenerated( - httpClient, - new RefitSettings + if (string.IsNullOrWhiteSpace(endpointUrl)) { - AuthorizationHeaderValueGetter = (_, cancellationToken) => - builder.Resource.GetApiKeyAsync(apiEndpoint, cancellationToken) - }); + return CommandResults.Failure("The Dev Proxy API endpoint is not available."); + } - try - { - await apiClient.SetProxyAsync( + using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false }) + { + BaseAddress = new Uri(endpointUrl), + Timeout = TimeSpan.FromSeconds(10) + }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken); + + using var response = await httpClient.PostAsJsonAsync( + "/proxy", new DevProxyUpdate { Recording = recording }, context.CancellationToken); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + return CommandResults.Failure( + $"Dev Proxy rejected the API token. Make sure you're using a version of Dev Proxy that supports the {ApiTokenEnvironmentVariable} environment variable."); + } + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(context.CancellationToken); + return CommandResults.Failure( + $"Dev Proxy returned {(int)response.StatusCode} {response.ReasonPhrase}: {content}"); + } + return CommandResults.Success(recording ? "Recording started." : "Recording stopped."); } - catch (ApiException exception) - { - return CommandResults.Failure( - $"Dev Proxy returned {(int)exception.StatusCode} {exception.ReasonPhrase}: {exception.Content}"); - } - catch (InvalidOperationException exception) - { - return CommandResults.Failure(exception.Message); - } catch (HttpRequestException exception) { return CommandResults.Failure($"Could not reach the Dev Proxy API: {exception.Message}"); } + catch (TaskCanceledException) when (!context.CancellationToken.IsCancellationRequested) + { + return CommandResults.Failure("The Dev Proxy API did not respond in time."); + } + } + + private sealed class DevProxyUpdate + { + public bool? Recording { get; set; } } -} \ No newline at end of file +} diff --git a/DevProxy.Hosting/DevProxyContainerResource.cs b/DevProxy.Hosting/DevProxyContainerResource.cs index 89f71f2..facd019 100644 --- a/DevProxy.Hosting/DevProxyContainerResource.cs +++ b/DevProxy.Hosting/DevProxyContainerResource.cs @@ -5,25 +5,7 @@ namespace DevProxy.Hosting; /// /// Exposes Dev Proxy as a Docker container. /// -public sealed class DevProxyContainerResource : ContainerResource, IDevProxyApiResource +public sealed class DevProxyContainerResource(string name) : + ContainerResource(name) { - private readonly string _apiTokenDirectory; - - /// - /// Initializes a container resource with an isolated temporary API-token directory. - /// - public DevProxyContainerResource(string name) - : this(name, DevProxyApiTokenStore.CreateTemporaryTokenDirectory()) - { - } - - internal DevProxyContainerResource(string name, string apiConfigDirectory) - : base(name) - { - _apiTokenDirectory = apiConfigDirectory; - Directory.CreateDirectory(_apiTokenDirectory); - } - - /// - public string ApiTokenDirectory => _apiTokenDirectory; } \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyExecutableResource.cs b/DevProxy.Hosting/DevProxyExecutableResource.cs index cd8475c..2f65acb 100644 --- a/DevProxy.Hosting/DevProxyExecutableResource.cs +++ b/DevProxy.Hosting/DevProxyExecutableResource.cs @@ -6,11 +6,6 @@ namespace DevProxy.Hosting; /// Exposes Dev Proxy through its locally installed executable. /// public sealed class DevProxyExecutableResource(string name) : - ExecutableResource(name, "devproxy", ""), IDevProxyApiResource + ExecutableResource(name, "devproxy", "") { - /// - public string ApiTokenDirectory { get; } = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "dev-proxy", - "credentials"); } \ No newline at end of file diff --git a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs index 32cebd9..bcdb15c 100644 --- a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs +++ b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs @@ -26,7 +26,7 @@ public static IResourceBuilder AddDevProxyExecutable .WithArgs("--as-system-proxy", "false") .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) - .WithDevProxyRecordingCommands(); + .WithRecordingCommands(); return resourceBuilder; } @@ -71,21 +71,15 @@ public static IResourceBuilder AddDevProxyContainer( this IDistributedApplicationBuilder builder, [ResourceName] string name) { - var apiTokenDirectory = Path.Combine( - builder.AppHostDirectory, - "obj", - "devproxy-api", - name); - var resource = new DevProxyContainerResource(name, apiTokenDirectory); + var resource = new DevProxyContainerResource(name); var resourceBuilder = builder.AddResource(resource) .WithImage(DevProxyContainerImageTags.Image) .WithImageRegistry(DevProxyContainerImageTags.Registry) .WithImageTag(DevProxyContainerImageTags.Tag) - .WithBindMount(resource.ApiTokenDirectory, "/home/devproxy/.config/dev-proxy/credentials") .WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName) .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName) - .WithDevProxyRecordingCommands(); + .WithRecordingCommands(); return resourceBuilder; } diff --git a/DevProxy.Hosting/IDevProxyApiResource.cs b/DevProxy.Hosting/IDevProxyApiResource.cs deleted file mode 100644 index bec93b4..0000000 --- a/DevProxy.Hosting/IDevProxyApiResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Aspire.Hosting.ApplicationModel; - -namespace DevProxy.Hosting; - -/// -/// A Dev Proxy resource exposing its management API. -/// -public interface IDevProxyApiResource : IResourceWithEndpoints -{ - /// - /// Gets the directory containing this resource's Dev Proxy API token files. - /// - string ApiTokenDirectory { get; } - - /// - /// Gets the API token that authenticates against this resource's API endpoint. - /// - ValueTask GetApiKeyAsync(Uri apiEndpoint, CancellationToken cancellationToken = default) => - DevProxyApiTokenStore.GetApiKeyAsync(ApiTokenDirectory, apiEndpoint, cancellationToken); -} \ No newline at end of file diff --git a/README.md b/README.md index 3096f7f..1b8a434 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ builder.Build().Run(); - **Custom Configuration**: Use the extension methods to configure Dev Proxy as needed. - **Service Integration**: Easily integrate Dev Proxy with other services in your application. -Both Dev Proxy resource types expose `Start recording` and `Stop recording` commands in the Aspire dashboard. The management API contract is available in the repository at `assets/openapi.yaml` and is not included in the NuGet package. +Both Dev Proxy resource types expose `Start recording` and `Stop recording` commands in the Aspire dashboard. The commands require a version of Dev Proxy that supports the `DEV_PROXY_API_TOKEN` environment variable. ## Contributing diff --git a/assets/openapi.yaml b/assets/openapi.yaml deleted file mode 100644 index 5e1bef7..0000000 --- a/assets/openapi.yaml +++ /dev/null @@ -1,278 +0,0 @@ -openapi: 3.0.3 -info: - title: Dev Proxy Management API - version: 1.0.0 - description: HTTP API exposed by Dev Proxy for inspecting and controlling the proxy process. -servers: - - url: http://localhost:8897 - description: Default local management API endpoint -security: - - bearerAuth: [] -paths: - /proxy: - get: - operationId: getProxy - summary: Get proxy state - responses: - '200': - description: Current proxy state. - content: - application/json: - schema: - $ref: '#/components/schemas/ProxyInfo' - '401': - $ref: '#/components/responses/Unauthorized' - post: - operationId: setProxy - summary: Update proxy state - description: Set recording to true to start recording or false to stop recording. ConfigFile cannot be updated through this operation. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ProxyUpdate' - responses: - '200': - description: Updated proxy state. - content: - application/json: - schema: - $ref: '#/components/schemas/ProxyInfo' - '400': - description: Invalid proxy state update. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - '401': - $ref: '#/components/responses/Unauthorized' - /proxy/mockRequest: - post: - operationId: raiseMockRequest - summary: Raise a mock request - responses: - '204': - description: Mock request raised. - '401': - $ref: '#/components/responses/Unauthorized' - /proxy/stopProxy: - post: - operationId: stopProxy - summary: Stop Dev Proxy - responses: - '202': - description: Proxy shutdown was requested. - '401': - $ref: '#/components/responses/Unauthorized' - /proxy/jwtToken: - post: - operationId: createJwtToken - summary: Create a JWT - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/JwtOptions' - responses: - '200': - description: Generated JWT. - content: - application/json: - schema: - $ref: '#/components/schemas/JwtInfo' - '400': - description: Invalid JWT options. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - '401': - $ref: '#/components/responses/Unauthorized' - /proxy/rootCertificate: - get: - operationId: getRootCertificate - summary: Download the root certificate - parameters: - - name: format - in: query - required: true - schema: - type: string - enum: [crt] - responses: - '200': - description: Root certificate encoded as PEM. - content: - application/x-x509-ca-cert: - schema: - type: string - format: binary - headers: - Content-Disposition: - schema: - type: string - '400': - description: Missing or unsupported certificate format. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - '401': - $ref: '#/components/responses/Unauthorized' - /proxy/logs: - get: - operationId: getLogs - summary: Read or follow logs - description: Available only when Dev Proxy runs in detached mode. Set follow=true or accept text/event-stream to stream server-sent events. - parameters: - - name: lines - in: query - required: false - schema: - type: integer - format: int32 - default: 50 - - name: follow - in: query - required: false - schema: - type: boolean - default: false - - name: since - in: query - required: false - description: Optional time or duration filter understood by Dev Proxy. - schema: - type: string - - name: Accept - in: header - required: false - schema: - type: string - enum: [text/plain, application/json, text/event-stream] - responses: - '200': - description: Log lines, JSON log entries, or a server-sent event stream. - content: - text/plain: - schema: - type: string - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/LogEntry' - text/event-stream: - schema: - type: string - '404': - description: Logs are unavailable outside detached mode or the log file does not exist. - '401': - $ref: '#/components/responses/Unauthorized' -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - description: Per-instance token returned by `devproxy api token`. - responses: - Unauthorized: - description: Missing or invalid bearer token. - headers: - WWW-Authenticate: - schema: - type: string - schemas: - ProxyInfo: - type: object - properties: - recording: - type: boolean - nullable: true - configFile: - type: string - nullable: true - ProxyUpdate: - type: object - properties: - recording: - type: boolean - nullable: true - JwtOptions: - type: object - properties: - audiences: - type: array - items: - type: string - claims: - type: object - additionalProperties: - type: string - issuer: - type: string - nullable: true - name: - type: string - nullable: true - roles: - type: array - items: - type: string - scopes: - type: array - items: - type: string - signingKey: - type: string - minLength: 32 - nullable: true - validFor: - type: number - format: double - nullable: true - JwtInfo: - type: object - required: [token] - properties: - token: - type: string - LogEntry: - type: object - properties: - time: - type: string - nullable: true - level: - type: string - nullable: true - category: - type: string - nullable: true - message: - type: string - nullable: true - raw: - type: string - nullable: true - ProblemDetails: - type: object - properties: - type: - type: string - nullable: true - title: - type: string - nullable: true - status: - type: integer - format: int32 - nullable: true - detail: - type: string - nullable: true - instance: - type: string - nullable: true \ No newline at end of file