@piecemaker-legal/piecemaker 1.1.1 → 2.0.1

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.
Files changed (135) hide show
  1. package/dist/assets/{abnfDiagram-VCTEODGH-DEXWh3LH.js → abnfDiagram-VCTEODGH-C2JDmlZx.js} +1 -1
  2. package/dist/assets/{arc-CoTRLXKW.js → arc-XhSg5Mrj.js} +1 -1
  3. package/dist/assets/{architectureDiagram-5GKGNRK7-Cg-jO9oR.js → architectureDiagram-5GKGNRK7-DKREuXxk.js} +1 -1
  4. package/dist/assets/{blockDiagram-I7D4REHJ-ChXwQSIq.js → blockDiagram-I7D4REHJ-CNK0Tp5d.js} +1 -1
  5. package/dist/assets/{c4Diagram-7LVT6UL2-Bw2VGggd.js → c4Diagram-7LVT6UL2-C8fJzWVp.js} +1 -1
  6. package/dist/assets/channel-DO5hoFdU.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-C1WdqrLP.js → chunk-2Q5K7J3B-DlE8nLJo.js} +1 -1
  8. package/dist/assets/{chunk-5VM5RSS4-BerUZBa1.js → chunk-5VM5RSS4-CSm-2TZ4.js} +1 -1
  9. package/dist/assets/{chunk-F27PBJKO-5W7ax2gB.js → chunk-F27PBJKO-CSgnXT2c.js} +1 -1
  10. package/dist/assets/{chunk-IMKFNOWR-JgDN1h4T.js → chunk-IMKFNOWR-C6bkt6C1.js} +1 -1
  11. package/dist/assets/{chunk-JWPE2WC7-ulNHnEdQ.js → chunk-JWPE2WC7-jUR52MBO.js} +1 -1
  12. package/dist/assets/{chunk-POPQ4Y6H-D4aTDC2T.js → chunk-POPQ4Y6H-D0UAL7O7.js} +1 -1
  13. package/dist/assets/{chunk-SVP7TREG-BA5zX4XF.js → chunk-SVP7TREG-C9dtOEii.js} +1 -1
  14. package/dist/assets/{chunk-TICWLB2K-DhOULVhX.js → chunk-TICWLB2K-BEzWKT39.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-CkpCgrVg.js → chunk-XXDRQBXY-uD1oklVY.js} +1 -1
  16. package/dist/assets/classDiagram-ZZMXUADV-BsQtwznI.js +1 -0
  17. package/dist/assets/classDiagram-v2-VYDZK3BY-BsQtwznI.js +1 -0
  18. package/dist/assets/{cose-bilkent-JH36ORCC-0xUSfjfV.js → cose-bilkent-JH36ORCC-aGjlSdMe.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BE6fNu2_.js → cynefin-OW5HDTMX-D3G4OYOn.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-5FMLGOSQ-rFMnZ9b3.js → cynefinDiagram-5FMLGOSQ-9swRDUnu.js} +1 -1
  21. package/dist/assets/{dagre-GXQ25YYZ-C1p4ffd7.js → dagre-GXQ25YYZ-lvaAGdqY.js} +1 -1
  22. package/dist/assets/{diagram-S7CK7UJ4-BV_3Xj4O.js → diagram-S7CK7UJ4-BaZ6YIkR.js} +1 -1
  23. package/dist/assets/{diagram-UQ7AKVKN-Bb_Evkrm.js → diagram-UQ7AKVKN-COJ2Gb38.js} +1 -1
  24. package/dist/assets/{diagram-VSXAHHWV-BzgupjxD.js → diagram-VSXAHHWV-CYZEkbzP.js} +1 -1
  25. package/dist/assets/{diagram-VX7I27RA-D1c67z76.js → diagram-VX7I27RA-fFeQJ4ae.js} +1 -1
  26. package/dist/assets/{diagram-Z3DM3KII-DsJAJGKE.js → diagram-Z3DM3KII-B7knszDV.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-PWID7BFC-B1ge_obq.js → ebnfDiagram-PWID7BFC-Dk0l329Y.js} +1 -1
  28. package/dist/assets/{erDiagram-RLTQ6QDP-DrV-tggw.js → erDiagram-RLTQ6QDP-CtpYxui8.js} +1 -1
  29. package/dist/assets/{flowDiagram-HODETNUW-CVzDiSQ3.js → flowDiagram-HODETNUW-aVWueRJD.js} +1 -1
  30. package/dist/assets/{ganttDiagram-EL5Y4UJY-CGRdSN9D.js → ganttDiagram-EL5Y4UJY-DfrX-B7p.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-WWUBYQGX-CAfvFXzL.js → gitGraphDiagram-WWUBYQGX-BFB1ZPF3.js} +1 -1
  32. package/dist/assets/{index-DaxH81ZJ.js → index-DqRygn9E.js} +170 -169
  33. package/dist/assets/index-Ds4HaAjz.css +1 -0
  34. package/dist/assets/{infoDiagram-27XIBGKW-DTshEkGK.js → infoDiagram-27XIBGKW-CuYkzm43.js} +1 -1
  35. package/dist/assets/{ishikawaDiagram-5VMMS53U-DkbshVxp.js → ishikawaDiagram-5VMMS53U-EMiw8Akh.js} +1 -1
  36. package/dist/assets/{journeyDiagram-3NMN7TZE-v5qE8xpn.js → journeyDiagram-3NMN7TZE-DCoLV1f8.js} +1 -1
  37. package/dist/assets/{kanban-definition-UXKFOSKX-a9CNMFLt.js → kanban-definition-UXKFOSKX-CyA4Yinu.js} +1 -1
  38. package/dist/assets/{layout-DrW4R1TH.js → layout-BlaB0grA.js} +1 -1
  39. package/dist/assets/{linear-Cs3qpXT_.js → linear-DLX3dJDH.js} +1 -1
  40. package/dist/assets/{mermaid.core-C7Ti-Fty.js → mermaid.core-Cy51CT89.js} +6 -6
  41. package/dist/assets/{mindmap-definition-YA3MSWOX-B0VDdIcG.js → mindmap-definition-YA3MSWOX-OBXjfL7I.js} +1 -1
  42. package/dist/assets/{pegDiagram-XKGWAZYB-CV1L7MjP.js → pegDiagram-XKGWAZYB-C_BJjUh1.js} +1 -1
  43. package/dist/assets/{pieDiagram-E7YTZNPT-DSCuzVTV.js → pieDiagram-E7YTZNPT-jIpbr3Fi.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-AXDQQJYC-hzX-9AKo.js → quadrantDiagram-AXDQQJYC-C1A15xI5.js} +1 -1
  45. package/dist/assets/{railroadDiagram-O6MQD6OU-BbmzIqcp.js → railroadDiagram-O6MQD6OU-Rslzn1pI.js} +1 -1
  46. package/dist/assets/{requirementDiagram-BXWQKSXE-CQCRRBSL.js → requirementDiagram-BXWQKSXE-LlfURO5-.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-P5KCCOFB-kiaLcW6X.js → sankeyDiagram-P5KCCOFB-B5BPF4-O.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bgx8ST7Q.js → sequenceDiagram-WJ2MYXX4-orYZe9zX.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-BTjJ-ygN.js → sizeCapture-INFHLROL-Co-K5Cfb.js} +1 -1
  50. package/dist/assets/{stateDiagram-D77RDMKH-D48RR1FL.js → stateDiagram-D77RDMKH-COEx1aH2.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-MP3YSRHH-B-XwSR-z.js → stateDiagram-v2-MP3YSRHH-DaeTZvmy.js} +1 -1
  52. package/dist/assets/{swimlanes-42K2YHIH-BhE-QuEZ.js → swimlanes-42K2YHIH-BHQaDhfZ.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-VR7AAH4N-BHW8q5M5.js +8 -0
  54. package/dist/assets/{timeline-definition-24CTP7MA-Cm7EUWWt.js → timeline-definition-24CTP7MA-COqCRwGX.js} +1 -1
  55. package/dist/assets/{vennDiagram-4TSXK5OY-X-nC9wZ0.js → vennDiagram-4TSXK5OY-nym5XSnr.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VM6X3IG4-D33YvEQZ.js → wardleyDiagram-VM6X3IG4-DEPPey_W.js} +1 -1
  57. package/dist/assets/{xychartDiagram-S5SC5T6Z-lziwBKsM.js → xychartDiagram-S5SC5T6Z-uSJ404kL.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/sw.js +1 -1
  60. package/dist-server/server/index.js +3 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/piecemaker/index.js +7 -3
  63. package/dist-server/server/piecemaker/index.js.map +1 -1
  64. package/dist-server/server/piecemaker/knowledge/index.js +13 -6
  65. package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
  66. package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
  67. package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
  68. package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
  69. package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
  70. package/dist-server/server/piecemaker/library/index.js +14 -41
  71. package/dist-server/server/piecemaker/library/index.js.map +1 -1
  72. package/dist-server/server/piecemaker/library/store.js +11 -3
  73. package/dist-server/server/piecemaker/library/store.js.map +1 -1
  74. package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
  75. package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
  76. package/dist-server/server/piecemaker/timesheet/service.js +8 -7
  77. package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
  78. package/dist-server/server/piecemaker/timesheet/store.js +5 -3
  79. package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
  80. package/package.json +1 -1
  81. package/product.config.json +2 -0
  82. package/scripts/piecemaker/build-app-icon.py +88 -0
  83. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
  84. package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
  85. package/scripts/release/assert-piecemaker-version.mjs +25 -0
  86. package/server/index.ts +4 -1
  87. package/server/piecemaker/anonymizer/client-config.cjs +45 -162
  88. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
  89. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
  90. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
  91. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
  92. package/server/piecemaker/anonymizer/providers.cjs +14 -135
  93. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
  94. package/server/piecemaker/anonymizer/service.cjs +144 -28
  95. package/server/piecemaker/index.ts +8 -3
  96. package/server/piecemaker/knowledge/index.ts +14 -6
  97. package/server/piecemaker/knowledge/local-routes.ts +72 -0
  98. package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
  99. package/server/piecemaker/library/index.ts +14 -34
  100. package/server/piecemaker/library/store.ts +11 -3
  101. package/server/piecemaker/library/workspace-installation.ts +20 -3
  102. package/server/piecemaker/timesheet/service.ts +9 -7
  103. package/server/piecemaker/timesheet/store.ts +5 -3
  104. package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
  105. package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
  106. package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
  107. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
  108. package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
  109. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
  110. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
  111. package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
  112. package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
  113. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
  114. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
  115. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
  116. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
  117. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
  118. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
  119. package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
  120. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
  121. package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
  122. package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
  123. package/dist/assets/channel-DdzrPZ6Y.js +0 -1
  124. package/dist/assets/classDiagram-ZZMXUADV-Dim2o8E5.js +0 -1
  125. package/dist/assets/classDiagram-v2-VYDZK3BY-Dim2o8E5.js +0 -1
  126. package/dist/assets/index-D_Dc_nQv.css +0 -1
  127. package/dist/assets/swimlanesDiagram-VR7AAH4N-DR525d5V.js +0 -8
  128. package/server/piecemaker/anonymizer/proxy.cjs +0 -365
  129. package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
  130. package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
  131. package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
  132. package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
  133. package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
  134. package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
  135. package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "piecemaker-hudsucker"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ bytes = "1"
8
+ futures = "0.3"
9
+ http-body-util = "0.1"
10
+ hudsucker = { version = "0.25", default-features = false, features = ["decoder", "http2", "rcgen-ca", "rustls-client"] }
11
+ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] }
12
+ serde_json = "1"
13
+ tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
14
+ tokio-stream = "0.1"
@@ -0,0 +1,501 @@
1
+ use std::collections::{HashMap, HashSet};
2
+ use std::net::SocketAddr;
3
+ use std::sync::atomic::{AtomicU64, Ordering};
4
+ use std::sync::Arc;
5
+
6
+ use bytes::Bytes;
7
+ use futures::StreamExt;
8
+ use http_body_util::{BodyExt, BodyStream};
9
+ use hudsucker::certificate_authority::RcgenAuthority;
10
+ use hudsucker::hyper::{Request, Response, StatusCode, Uri};
11
+ use hudsucker::rcgen::{Issuer, KeyPair};
12
+ use hudsucker::rustls::crypto::aws_lc_rs;
13
+ use hudsucker::tokio_tungstenite::tungstenite::Message;
14
+ use hudsucker::{
15
+ Body, HttpContext, HttpHandler, Proxy, RequestOrResponse, WebSocketContext, WebSocketHandler,
16
+ };
17
+ use tokio::sync::mpsc;
18
+ use tokio_stream::wrappers::ReceiverStream;
19
+
20
+ const MAX_BODY: usize = 64 * 1024 * 1024;
21
+
22
+ static WS_IDS: AtomicU64 = AtomicU64::new(1);
23
+
24
+ #[derive(Clone)]
25
+ struct Handler {
26
+ rewriter: String,
27
+ hosts: Arc<HashSet<String>>,
28
+ upstream: Arc<HashMap<String, String>>,
29
+ http: reqwest::Client,
30
+ provider: String,
31
+ ws_id: Option<String>,
32
+ }
33
+
34
+ fn arg(name: &str) -> String {
35
+ let mut args = std::env::args().skip(1);
36
+ while let Some(key) = args.next() {
37
+ if key == name {
38
+ return args.next().unwrap_or_default();
39
+ }
40
+ }
41
+ String::new()
42
+ }
43
+
44
+ fn host_of(value: &str) -> String {
45
+ value
46
+ .split(':')
47
+ .next()
48
+ .unwrap_or(value)
49
+ .trim()
50
+ .to_ascii_lowercase()
51
+ }
52
+
53
+ fn request_host(req: &Request<Body>) -> String {
54
+ if let Some(host) = req.uri().host() {
55
+ return host.to_ascii_lowercase();
56
+ }
57
+ if let Some(authority) = req.uri().authority() {
58
+ return authority.host().to_ascii_lowercase();
59
+ }
60
+ req.headers()
61
+ .get("host")
62
+ .and_then(|value| value.to_str().ok())
63
+ .map(host_of)
64
+ .unwrap_or_default()
65
+ }
66
+
67
+ fn provider_for(host: &str) -> &'static str {
68
+ match host {
69
+ "chatgpt.com" => "codex",
70
+ "api.openai.com" => "opencode",
71
+ _ => "claude",
72
+ }
73
+ }
74
+
75
+ fn header_text(req: &Request<Body>, name: &str) -> String {
76
+ req.headers()
77
+ .get(name)
78
+ .and_then(|value| value.to_str().ok())
79
+ .unwrap_or("")
80
+ .to_string()
81
+ }
82
+
83
+ fn response_type(res: &Response<Body>) -> String {
84
+ res.headers()
85
+ .get("content-type")
86
+ .and_then(|value| value.to_str().ok())
87
+ .unwrap_or("")
88
+ .to_ascii_lowercase()
89
+ }
90
+
91
+ fn is_json(value: &str) -> bool {
92
+ value.contains("json")
93
+ }
94
+
95
+ fn is_sse(value: &str) -> bool {
96
+ value.contains("text/event-stream")
97
+ }
98
+
99
+ fn health_request(req: &Request<Body>) -> bool {
100
+ let host = request_host(req);
101
+ req.method() == hudsucker::hyper::Method::GET
102
+ && (req.uri().path() == "/" || req.uri().path().is_empty())
103
+ && (host.is_empty() || host == "127.0.0.1" || host == "localhost")
104
+ }
105
+
106
+ fn failure() -> Response<Body> {
107
+ Response::builder()
108
+ .status(StatusCode::BAD_GATEWAY)
109
+ .header("content-type", "application/json")
110
+ .body(Body::from(
111
+ "{\"error\":{\"type\":\"piecemaker_proxy\",\"message\":\"Relais d'anonymisation indisponible.\"}}",
112
+ ))
113
+ .unwrap()
114
+ }
115
+
116
+ fn uri_path(uri: &Uri) -> String {
117
+ match uri.path_and_query() {
118
+ Some(path) if !path.as_str().is_empty() => path.as_str().to_string(),
119
+ _ => "/".to_string(),
120
+ }
121
+ }
122
+
123
+ impl Handler {
124
+ fn allowed(&self, host: &str) -> bool {
125
+ self.hosts.contains(host)
126
+ }
127
+
128
+ fn point_upstream(&self, host: &str, req: &mut Request<Body>) {
129
+ let Some(dest) = self.upstream.get(host) else {
130
+ return;
131
+ };
132
+ let path = uri_path(req.uri());
133
+ let rewritten = format!("http://{dest}{path}");
134
+ if let Ok(uri) = rewritten.parse() {
135
+ *req.uri_mut() = uri;
136
+ }
137
+ if let Ok(value) = dest.parse() {
138
+ req.headers_mut().insert("host", value);
139
+ }
140
+ }
141
+
142
+ async fn post_bytes(&self, path: &str, provider: &str, extra: &[(&str, &str)], body: Bytes) -> Result<Bytes, ()> {
143
+ let mut request = self
144
+ .http
145
+ .post(format!("{}{path}", self.rewriter))
146
+ .header("x-piecemaker-provider", provider)
147
+ .body(body);
148
+ for (name, value) in extra {
149
+ request = request.header(*name, *value);
150
+ }
151
+ let response = request.send().await.map_err(|_| ())?;
152
+ if !response.status().is_success() {
153
+ return Err(());
154
+ }
155
+ response.bytes().await.map_err(|_| ())
156
+ }
157
+
158
+ fn spawn_sse(&self, provider: String, body: Body) -> Body {
159
+ let http = self.http.clone();
160
+ let url = format!("{}/v1/sse", self.rewriter);
161
+ let (tx, rx) = mpsc::channel::<Result<Bytes, std::io::Error>>(8);
162
+ tokio::spawn(async move {
163
+ let incoming = BodyStream::new(body).filter_map(|frame| async move {
164
+ match frame {
165
+ Ok(frame) => frame.into_data().ok().map(Ok),
166
+ Err(error) => Some(Err(std::io::Error::other(error.to_string()))),
167
+ }
168
+ });
169
+ let sent = http
170
+ .post(url)
171
+ .header("x-piecemaker-provider", provider)
172
+ .body(reqwest::Body::wrap_stream(incoming))
173
+ .send()
174
+ .await;
175
+ match sent {
176
+ Ok(response) => {
177
+ let mut stream = response.bytes_stream();
178
+ while let Some(chunk) = stream.next().await {
179
+ let mapped = chunk.map_err(|error| std::io::Error::other(error.to_string()));
180
+ if tx.send(mapped).await.is_err() {
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ Err(error) => {
186
+ let _ = tx.send(Err(std::io::Error::other(error.to_string()))).await;
187
+ }
188
+ }
189
+ });
190
+ Body::from_stream(ReceiverStream::new(rx))
191
+ }
192
+ }
193
+
194
+ impl HttpHandler for Handler {
195
+ async fn should_intercept_connect(&mut self, _ctx: &HttpContext, req: &Request<Body>) -> bool {
196
+ self.allowed(&request_host(req))
197
+ }
198
+
199
+ async fn should_intercept_tls(
200
+ &mut self,
201
+ _ctx: &HttpContext,
202
+ client_hello: hudsucker::rustls::server::ClientHello<'_>,
203
+ ) -> bool {
204
+ client_hello
205
+ .server_name()
206
+ .map(|name| self.allowed(&name.to_ascii_lowercase()))
207
+ .unwrap_or(false)
208
+ }
209
+
210
+ async fn handle_request(&mut self, _ctx: &HttpContext, req: Request<Body>) -> RequestOrResponse {
211
+ if health_request(&req) {
212
+ return Response::builder()
213
+ .status(StatusCode::OK)
214
+ .body(Body::from("ok"))
215
+ .unwrap()
216
+ .into();
217
+ }
218
+
219
+ if req.method() == hudsucker::hyper::Method::CONNECT {
220
+ return req.into();
221
+ }
222
+
223
+ let host = request_host(&req);
224
+ if !self.allowed(&host) {
225
+ return req.into();
226
+ }
227
+ self.provider = provider_for(&host).to_string();
228
+
229
+ let content_type = header_text(&req, "content-type");
230
+ let (mut parts, body) = req.into_parts();
231
+ parts.headers.remove("accept-encoding");
232
+
233
+ let body = if is_json(&content_type.to_ascii_lowercase()) {
234
+ let collected = match body.collect().await {
235
+ Ok(collected) => collected.to_bytes(),
236
+ Err(_) => return failure().into(),
237
+ };
238
+ if collected.len() > MAX_BODY {
239
+ return failure().into();
240
+ }
241
+ if collected.is_empty() {
242
+ Body::empty()
243
+ } else {
244
+ match self
245
+ .post_bytes("/v1/request", &self.provider.clone(), &[], collected)
246
+ .await
247
+ {
248
+ Ok(rewritten) => {
249
+ if let Ok(length) = rewritten.len().to_string().parse() {
250
+ parts.headers.insert("content-length", length);
251
+ }
252
+ Body::from(rewritten)
253
+ }
254
+ Err(_) => return failure().into(),
255
+ }
256
+ }
257
+ } else {
258
+ body
259
+ };
260
+
261
+ let mut req = Request::from_parts(parts, body);
262
+ self.point_upstream(&host, &mut req);
263
+ req.into()
264
+ }
265
+
266
+ async fn handle_response(&mut self, _ctx: &HttpContext, res: Response<Body>) -> Response<Body> {
267
+ let kind = response_type(&res);
268
+ let provider = self.provider.clone();
269
+ let (mut parts, body) = res.into_parts();
270
+ parts.headers.remove("content-encoding");
271
+ parts.headers.remove("content-length");
272
+ parts.headers.remove("transfer-encoding");
273
+ parts.headers.remove("connection");
274
+
275
+ if is_sse(&kind) {
276
+ parts.headers.remove("content-length");
277
+ parts.headers.insert(
278
+ "content-type",
279
+ "text/event-stream".parse().unwrap(),
280
+ );
281
+ return Response::from_parts(parts, self.spawn_sse(provider, body));
282
+ }
283
+
284
+ if is_json(&kind) {
285
+ let collected = match body.collect().await {
286
+ Ok(collected) => collected.to_bytes(),
287
+ Err(_) => return failure(),
288
+ };
289
+ if collected.len() > MAX_BODY {
290
+ return failure();
291
+ }
292
+ let rewritten = match self.post_bytes("/v1/response", &provider, &[], collected).await {
293
+ Ok(rewritten) => rewritten,
294
+ Err(_) => return failure(),
295
+ };
296
+ return Response::from_parts(parts, Body::from(rewritten));
297
+ }
298
+
299
+ Response::from_parts(parts, body)
300
+ }
301
+ }
302
+
303
+ impl WebSocketHandler for Handler {
304
+ async fn handle_message(&mut self, ctx: &WebSocketContext, message: Message) -> Option<Message> {
305
+ let (outbound, uri) = match ctx {
306
+ WebSocketContext::ClientToServer { dst, .. } => (true, dst),
307
+ WebSocketContext::ServerToClient { src, .. } => (false, src),
308
+ };
309
+ let host = uri.host().map(|value| value.to_ascii_lowercase()).unwrap_or_default();
310
+ if !self.allowed(&host) {
311
+ return Some(message);
312
+ }
313
+ let Message::Text(text) = message else {
314
+ return Some(message);
315
+ };
316
+ if self.ws_id.is_none() {
317
+ self.ws_id = Some(format!("ws-{}", WS_IDS.fetch_add(1, Ordering::Relaxed)));
318
+ }
319
+ let id = self.ws_id.clone().unwrap_or_default();
320
+ let direction = if outbound { "out" } else { "in" };
321
+ let provider = provider_for(&host);
322
+ match self
323
+ .post_bytes(
324
+ "/v1/ws",
325
+ provider,
326
+ &[
327
+ ("x-piecemaker-ws", id.as_str()),
328
+ ("x-piecemaker-direction", direction),
329
+ ],
330
+ Bytes::from(text.to_string()),
331
+ )
332
+ .await
333
+ {
334
+ Ok(rewritten) if !rewritten.is_empty() => {
335
+ Some(Message::Text(String::from_utf8_lossy(&rewritten).into_owned().into()))
336
+ }
337
+ Ok(_) => None,
338
+ Err(_) => None,
339
+ }
340
+ }
341
+
342
+ async fn handle_websocket(
343
+ mut self,
344
+ ctx: WebSocketContext,
345
+ mut stream: impl futures::Stream<Item = Result<Message, hudsucker::tokio_tungstenite::tungstenite::Error>>
346
+ + Unpin
347
+ + Send
348
+ + 'static,
349
+ mut sink: impl futures::Sink<Message, Error = hudsucker::tokio_tungstenite::tungstenite::Error>
350
+ + Unpin
351
+ + Send
352
+ + 'static,
353
+ ) {
354
+ use futures::SinkExt;
355
+ while let Some(message) = stream.next().await {
356
+ match message {
357
+ Ok(message) => {
358
+ let Some(message) = self.handle_message(&ctx, message).await else {
359
+ continue;
360
+ };
361
+ if sink.send(message).await.is_err() {
362
+ break;
363
+ }
364
+ }
365
+ Err(_) => break,
366
+ }
367
+ }
368
+ if let Some(id) = self.ws_id.clone() {
369
+ let host = match &ctx {
370
+ WebSocketContext::ClientToServer { dst, .. } => dst.host().unwrap_or_default().to_string(),
371
+ WebSocketContext::ServerToClient { src, .. } => src.host().unwrap_or_default().to_string(),
372
+ };
373
+ if let Ok(tail) = self
374
+ .post_bytes(
375
+ "/v1/ws-end",
376
+ provider_for(&host_of(&host)),
377
+ &[("x-piecemaker-ws", id.as_str())],
378
+ Bytes::new(),
379
+ )
380
+ .await
381
+ {
382
+ if let Ok(messages) = serde_json_messages(&tail) {
383
+ for message in messages {
384
+ if sink.send(Message::Text(message.into())).await.is_err() {
385
+ break;
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ fn serde_json_messages(bytes: &Bytes) -> Result<Vec<String>, ()> {
395
+ let text = std::str::from_utf8(bytes).map_err(|_| ())?;
396
+ let value: serde_json::Value = serde_json::from_str(text).map_err(|_| ())?;
397
+ Ok(value
398
+ .get("messages")
399
+ .and_then(|entry| entry.as_array())
400
+ .map(|entries| {
401
+ entries
402
+ .iter()
403
+ .filter_map(|entry| entry.as_str().map(str::to_string))
404
+ .collect()
405
+ })
406
+ .unwrap_or_default())
407
+ }
408
+
409
+ fn parse_list(value: &str) -> HashSet<String> {
410
+ value
411
+ .split(',')
412
+ .map(|entry| host_of(entry.trim()))
413
+ .filter(|entry| !entry.is_empty())
414
+ .collect()
415
+ }
416
+
417
+ fn parse_map(value: &str) -> HashMap<String, String> {
418
+ value
419
+ .split(',')
420
+ .filter_map(|entry| {
421
+ let (host, dest) = entry.split_once('=')?;
422
+ let host = host_of(host.trim());
423
+ let dest = dest.trim().to_string();
424
+ if host.is_empty() || dest.is_empty() {
425
+ None
426
+ } else {
427
+ Some((host, dest))
428
+ }
429
+ })
430
+ .collect()
431
+ }
432
+
433
+ #[tokio::main]
434
+ async fn main() {
435
+ let listen = arg("--listen");
436
+ let rewriter = arg("--rewriter");
437
+ let ca_cert = arg("--ca-cert");
438
+ let ca_key = arg("--ca-key");
439
+ let hosts = parse_list(&arg("--hosts"));
440
+ let upstream = parse_map(&arg("--upstream-map"));
441
+ if listen.is_empty() || rewriter.is_empty() || ca_cert.is_empty() || ca_key.is_empty() || hosts.is_empty() {
442
+ eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:4111 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com");
443
+ std::process::exit(2);
444
+ }
445
+
446
+ let key = std::fs::read_to_string(&ca_key).unwrap_or_else(|error| {
447
+ eprintln!("clé d'autorité illisible : {error}");
448
+ std::process::exit(1);
449
+ });
450
+ let cert = std::fs::read_to_string(&ca_cert).unwrap_or_else(|error| {
451
+ eprintln!("certificat d'autorité illisible : {error}");
452
+ std::process::exit(1);
453
+ });
454
+ let key_pair = KeyPair::from_pem(&key).unwrap_or_else(|error| {
455
+ eprintln!("clé d'autorité refusée : {error}");
456
+ std::process::exit(1);
457
+ });
458
+ let issuer = Issuer::from_ca_cert_pem(&cert, key_pair).unwrap_or_else(|error| {
459
+ eprintln!("certificat d'autorité refusé : {error}");
460
+ std::process::exit(1);
461
+ });
462
+ let authority = RcgenAuthority::new(issuer, 1_000, aws_lc_rs::default_provider());
463
+ let addr: SocketAddr = listen.parse().unwrap_or_else(|_| {
464
+ eprintln!("adresse d'écoute invalide");
465
+ std::process::exit(2);
466
+ });
467
+ let http = reqwest::Client::builder()
468
+ .no_proxy()
469
+ .build()
470
+ .unwrap_or_else(|error| {
471
+ eprintln!("client interne : {error}");
472
+ std::process::exit(1);
473
+ });
474
+ let handler = Handler {
475
+ rewriter,
476
+ hosts: Arc::new(hosts),
477
+ upstream: Arc::new(upstream),
478
+ http,
479
+ provider: "claude".to_string(),
480
+ ws_id: None,
481
+ };
482
+ println!("listening {addr}");
483
+ let proxy = match Proxy::builder()
484
+ .with_addr(addr)
485
+ .with_ca(authority)
486
+ .with_rustls_connector(aws_lc_rs::default_provider())
487
+ .with_http_handler(handler.clone())
488
+ .with_websocket_handler(handler)
489
+ .build()
490
+ {
491
+ Ok(proxy) => proxy,
492
+ Err(error) => {
493
+ eprintln!("proxy : {error}");
494
+ std::process::exit(1);
495
+ }
496
+ };
497
+ if let Err(error) = proxy.start().await {
498
+ eprintln!("proxy arrêté : {error}");
499
+ std::process::exit(1);
500
+ }
501
+ }