Forge
csharpdeeb25a2
1using System.Net.Http.Headers;
2using System.Net.Http.Json;
3using System.Text;
4using System.Text.Json;
5using CascadeIDE.Models;
6using CascadeIDE.Services;
7
8namespace CascadeIDE.Features.Intercom.Transport;
9
10/// <summary>HTTP-клиент reference Intercom service (ADR 0144).</summary>
11public sealed partial class IntercomTransportApiClient : IDisposable
12{
13 private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(120) };
14
15 public void ConfigureBaseUrl(string baseUrl)
16 {
17 var trimmed = baseUrl.Trim().TrimEnd('/');
18 _http.BaseAddress = new Uri(trimmed + "/");
19 }
20
21 public async Task<IReadOnlyList<IntercomTopicDto>> ListTopicsAsync(
22 string teamId,
23 string bearerToken,
24 CancellationToken ct)
25 {
26 using var req = Authorized(HttpMethod.Get, $"/api/v1/teams/{Uri.EscapeDataString(teamId)}/topics", bearerToken);
27 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
28 res.EnsureSuccessStatusCode();
29 var list = await res.Content.ReadFromJsonAsync<List<IntercomTopicDto>>(IntercomTransportJson.Web, ct)
30 .ConfigureAwait(false);
31 return list ?? [];
32 }
33
34 public async Task<IntercomTransportEventEnvelopeDto?> AppendEventAsync(
35 string topicId,
36 IntercomAppendEventRequestDto body,
37 string bearerToken,
38 CancellationToken ct)
39 {
40 var json = JsonSerializer.Serialize(body, IntercomTransportJson.Web);
41 using var req = Authorized(
42 HttpMethod.Post,
43 $"/api/v1/topics/{Uri.EscapeDataString(topicId)}/events",
44 bearerToken);
45 req.Content = new StringContent(json, Encoding.UTF8, "application/json");
46 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
47 if (!res.IsSuccessStatusCode)
48 return null;
49 return await res.Content.ReadFromJsonAsync<IntercomTransportEventEnvelopeDto>(IntercomTransportJson.Web, ct)
50 .ConfigureAwait(false);
51 }
52
53 public async Task<IReadOnlyList<IntercomTransportEventEnvelopeDto>> ListEventsAsync(
54 string topicId,
55 long afterSeq,
56 string bearerToken,
57 CancellationToken ct)
58 {
59 var path = $"/api/v1/topics/{Uri.EscapeDataString(topicId)}/events?after_seq={afterSeq}&limit=200";
60 using var req = Authorized(HttpMethod.Get, path, bearerToken);
61 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
62 res.EnsureSuccessStatusCode();
63 var list = await res.Content.ReadFromJsonAsync<List<IntercomTransportEventEnvelopeDto>>(
64 IntercomTransportJson.Web,
65 ct)
66 .ConfigureAwait(false);
67 return list ?? [];
68 }
69
70 public async Task<IReadOnlyList<IntercomTransportEventEnvelopeDto>> ListTeamEventsAsync(
71 string teamId,
72 long afterSeq,
73 string bearerToken,
74 CancellationToken ct)
75 {
76 var path = $"/api/v1/teams/{Uri.EscapeDataString(teamId)}/events?after_seq={afterSeq}&limit=500";
77 using var req = Authorized(HttpMethod.Get, path, bearerToken);
78 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
79 res.EnsureSuccessStatusCode();
80 var list = await res.Content.ReadFromJsonAsync<List<IntercomTransportEventEnvelopeDto>>(
81 IntercomTransportJson.Web,
82 ct)
83 .ConfigureAwait(false);
84 return list ?? [];
85 }
86
87 public async Task<IntercomTopicDto?> EnsureTopicAsync(
88 string teamId,
89 string spineKey,
90 string title,
91 string bearerToken,
92 CancellationToken ct)
93 {
94 var body = new IntercomEnsureTopicRequestDto(spineKey, title);
95 using var req = Authorized(
96 HttpMethod.Post,
97 $"/api/v1/teams/{Uri.EscapeDataString(teamId)}/topics/ensure",
98 bearerToken);
99 req.Content = JsonContent.Create(body, options: IntercomTransportJson.Web);
100 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
101 if (!res.IsSuccessStatusCode)
102 return null;
103 return await res.Content.ReadFromJsonAsync<IntercomTopicDto>(IntercomTransportJson.Web, ct).ConfigureAwait(false);
104 }
105
106 public async Task<IntercomTokenResponseDto?> RefreshTokenAsync(string refreshToken, CancellationToken ct)
107 {
108 var body = new OAuthTokenRequest("refresh_token", refreshToken, null, null, null, null);
109 return await postTokenAsync(body, ct).ConfigureAwait(false);
110 }
111
112 public async Task<IntercomTokenResponseDto?> ExchangeAuthorizationCodeAsync(
113 string code,
114 string state,
115 string codeVerifier,
116 string redirectUri,
117 CancellationToken ct)
118 {
119 var body = new OAuthTokenRequest(
120 "authorization_code",
121 null,
122 code,
123 state,
124 codeVerifier,
125 redirectUri);
126 return await postTokenAsync(body, ct).ConfigureAwait(false);
127 }
128
129 private async Task<IntercomTokenResponseDto?> postTokenAsync(OAuthTokenRequest body, CancellationToken ct)
130 {
131 using var res = await _http.PostAsJsonAsync("/api/v1/auth/token", body, IntercomTransportJson.Web, ct)
132 .ConfigureAwait(false);
133 if (!res.IsSuccessStatusCode)
134 return null;
135 return await res.Content.ReadFromJsonAsync<IntercomTokenResponseDto>(IntercomTransportJson.Web, ct)
136 .ConfigureAwait(false);
137 }
138
139 public async Task LogoutAsync(string refreshToken, string accessToken, CancellationToken ct)
140 {
141 var body = new { grant_type = "refresh_token", refresh_token = refreshToken };
142 using var req = Authorized(HttpMethod.Post, "/api/v1/auth/logout", accessToken);
143 req.Content = JsonContent.Create(body, options: IntercomTransportJson.Web);
144 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
145 _ = res.StatusCode;
146 }
147
148 public async Task<IntercomMeResponseDto?> GetMeAsync(string bearerToken, CancellationToken ct)
149 {
150 using var req = Authorized(HttpMethod.Get, "/api/v1/auth/me", bearerToken);
151 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
152 if (!res.IsSuccessStatusCode)
153 return null;
154 return await res.Content.ReadFromJsonAsync<IntercomMeResponseDto>(IntercomTransportJson.Web, ct)
155 .ConfigureAwait(false);
156 }
157
158 public async Task<IReadOnlyList<IntercomAuthProviderDto>> ListAuthProvidersAsync(CancellationToken ct)
159 {
160 using var res = await _http.GetAsync("/api/v1/auth/providers", ct).ConfigureAwait(false);
161 if (!res.IsSuccessStatusCode)
162 return [];
163 var list = await res.Content.ReadFromJsonAsync<List<IntercomAuthProviderDto>>(IntercomTransportJson.Web, ct)
164 .ConfigureAwait(false);
165 return list ?? [];
166 }
167
168 public async Task<IntercomWorkspaceContextResponseDto?> ResolveWorkspaceContextAsync(
169 string normalizedRepoUrl,
170 string bearerToken,
171 CancellationToken ct)
172 {
173 var path = $"/api/v1/resolve/workspace-context?repo_url={Uri.EscapeDataString(normalizedRepoUrl)}";
174 using var req = Authorized(HttpMethod.Get, path, bearerToken);
175 using var res = await _http.SendAsync(req, ct).ConfigureAwait(false);
176 if (!res.IsSuccessStatusCode)
177 return null;
178 return await res.Content.ReadFromJsonAsync<IntercomWorkspaceContextResponseDto>(IntercomTransportJson.Web, ct)
179 .ConfigureAwait(false);
180 }
181
182 public HttpRequestMessage CreateSseRequest(string teamId, string? topicId, string bearerToken)
183 {
184 var path = $"/api/v1/teams/{Uri.EscapeDataString(teamId)}/stream";
185 if (!string.IsNullOrWhiteSpace(topicId))
186 path += $"?topic_id={Uri.EscapeDataString(topicId)}";
187 return Authorized(HttpMethod.Get, path, bearerToken);
188 }
189
190 public Task<HttpResponseMessage> SendSseAsync(HttpRequestMessage request, CancellationToken ct) =>
191 _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
192
193 public static void ApplyTokenResponse(IntercomTransportSecrets secrets, IntercomTokenResponseDto tokens)
194 {
195 secrets.AccessToken = tokens.AccessToken;
196 secrets.RefreshToken = tokens.RefreshToken;
197 secrets.SetAccessExpiry(DateTimeOffset.UtcNow.AddSeconds(Math.Max(30, tokens.ExpiresIn - 30)));
198 IntercomTransportSecretsStorage.Save(secrets);
199 }
200
201 private static HttpRequestMessage Authorized(HttpMethod method, string relativePath, string bearerToken)
202 {
203 var req = new HttpRequestMessage(method, relativePath);
204 req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
205 return req;
206 }
207
208 public void Dispose() => _http.Dispose();
209}
210
View only · write via MCP/CIDE