From 46e7c6c2c5e553233f614dbb8876af33f20a5497 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Sun, 22 Jun 2025 19:26:03 +0100 Subject: [PATCH 01/13] adding failing test --- crates/bsnext_input/src/input_test/mod.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/bsnext_input/src/input_test/mod.rs b/crates/bsnext_input/src/input_test/mod.rs index be5fa446..5e6ce923 100644 --- a/crates/bsnext_input/src/input_test/mod.rs +++ b/crates/bsnext_input/src/input_test/mod.rs @@ -258,6 +258,26 @@ servers: ) } #[test] +fn test_deserialize_server_watch_run_opts() { + let input = r#" +servers: +- bind_address: 0.0.0.0:4000 + watchers: + - dirs: ./ + - dirs: ./other + debounce: + ms: 2000 + filter: + ext: "**/*.css" + run: + - sh: echo lol + - bslive: huh +"#; + let c: Result = serde_yaml::from_str(input); + assert!(c.is_err()); + // dbg!(&c.servers.get(0).unwrap().watchers.get(1).unwrap()); +} +#[test] fn test_deserialize_server_clients_config() { let input = r#" servers: From 5fb20c25f24af01cdf7a957fc51fcd6f1c3939e9 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 4 Jul 2025 14:30:03 +0100 Subject: [PATCH 02/13] sse opts --- crates/bsnext_core/src/handlers/proxy.rs | 30 +++++++++++---- crates/bsnext_core/src/raw_loader.rs | 24 +++++++----- crates/bsnext_core/tests/every_route_kind.rs | 3 +- crates/bsnext_dto/src/lib.rs | 12 +++++- crates/bsnext_input/src/input_test/mod.rs | 40 ++++++++++---------- crates/bsnext_input/src/route.rs | 15 +++++++- examples/openai/bslive.yml | 16 +++++--- generated/dto.ts | 6 ++- generated/schema.js | 20 ++++++---- generated/schema.ts | 22 ++++++----- inject/dist/index.js | 2 +- 11 files changed, 122 insertions(+), 68 deletions(-) diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index c905a814..fea55748 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -13,6 +13,7 @@ use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::client::legacy::Client; use tower::ServiceExt; use tower_http::decompression::DecompressionLayer; +use tracing::{trace_span, Instrument}; #[derive(Debug, Clone)] pub struct ProxyConfig { @@ -47,11 +48,12 @@ pub async fn proxy_handler( req: Request, ) -> Result { let target = config.target.clone(); - - tracing::trace!(?config); let path = req.uri().path(); - tracing::trace!(req.uri = %path, config.path = config.path); + let span = trace_span!("proxy_handler"); + let _g = span.enter(); + + tracing::trace!(?config); let path_query = req .uri() @@ -79,6 +81,7 @@ pub async fn proxy_handler( // todo(alpha): Which other headers to mod here? req.headers_mut().insert("host", host_header_value); + req.headers_mut().remove("referer"); // decompress requests if needed if let Some(h) = req.extensions().get::() { @@ -89,16 +92,24 @@ pub async fn proxy_handler( .headers() .get("accept") .map(|h| h.to_str().unwrap_or("")), - "will accept request + decompress" + "will accept request + decompress?" ); if req_accepted { let sv2 = any(serve_one_proxy_req.layer(DecompressionLayer::new())); - return Ok(sv2.oneshot(req).await.into_response()); + return Ok(sv2 + .oneshot(req) + .instrument(span.clone()) + .await + .into_response()); } } let sv2 = any(serve_one_proxy_req); - Ok(sv2.oneshot(req).await.into_response()) + Ok(sv2 + .oneshot(req) + .instrument(span.clone()) + .await + .into_response()) } async fn serve_one_proxy_req(req: Request) -> Response { @@ -108,10 +119,13 @@ async fn serve_one_proxy_req(req: Request) -> Response { .get::, Body>>() .expect("must have a client, move this to an extractor?") }; + tracing::trace!(req.headers = ?req.headers()); let client_c = client.clone(); - client_c + let resp = client_c .request(req) .await .map_err(|_| StatusCode::BAD_REQUEST) - .into_response() + .into_response(); + tracing::trace!(resp.headers = ?resp.headers()); + resp } diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index d81aa458..b01eca55 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -8,7 +8,7 @@ use http::header::CONTENT_TYPE; use axum::body::Body; use axum::response::sse::Event; -use bsnext_input::route::RawRoute; +use bsnext_input::route::{RawRoute, SseOpts}; use bytes::Bytes; use http::{StatusCode, Uri}; use http_body_util::BodyExt; @@ -28,18 +28,21 @@ async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { } RawRoute::Json { json } => Json(&json.0).into_response(), RawRoute::Raw { raw } => text_asset_response(uri.path(), raw).into_response(), - RawRoute::Sse { sse } => { - let l = sse + RawRoute::Sse { + sse: SseOpts { body, throttle_ms }, + } => { + let l = body .lines() .map(|l| l.to_owned()) - .map(|l| l.strip_prefix("data:").unwrap_or(&l).to_owned()) - .filter(|l| !l.trim().is_empty()) + .map(|l| l.strip_prefix("data:").unwrap_or(&l).trim().to_owned()) + .filter(|l| !l.is_empty()) .collect::>(); tracing::trace!(lines.count = l.len(), "sending EventStream"); + let milli = throttle_ms.unwrap_or(10); let stream = tokio_stream::iter(l) - .throttle(Duration::from_millis(10)) + .throttle(Duration::from_millis(milli)) .map(|chu| Event::default().data(chu)) .map(Ok::<_, Infallible>); @@ -69,10 +72,11 @@ mod raw_test { - path: /json json: [1] - path: /sse - sse: | - a - b - c"#; + sse: + body: | + a + b + c"#; { let routes: Vec = serde_yaml::from_str(routes_input)?; diff --git a/crates/bsnext_core/tests/every_route_kind.rs b/crates/bsnext_core/tests/every_route_kind.rs index 3e77065b..c4f4431b 100644 --- a/crates/bsnext_core/tests/every_route_kind.rs +++ b/crates/bsnext_core/tests/every_route_kind.rs @@ -23,7 +23,8 @@ servers: - path: /raw raw: -----raw-content---- - path: /sse - sse: | + sse: + body: | a b c diff --git a/crates/bsnext_dto/src/lib.rs b/crates/bsnext_dto/src/lib.rs index c3ebaf77..62f6a3e9 100644 --- a/crates/bsnext_dto/src/lib.rs +++ b/crates/bsnext_dto/src/lib.rs @@ -57,11 +57,17 @@ pub enum RouteKindDTO { Html { html: String }, Json { json_str: String }, Raw { raw: String }, - Sse { sse: String }, + Sse { sse: SseDTOOpts }, Proxy { proxy: String }, Dir { dir: String, base: Option }, } +#[typeshare] +#[derive(Debug, serde::Serialize)] +pub struct SseDTOOpts { + pub body: String, +} + impl From for RouteKindDTO { fn from(value: RouteKind) -> Self { match value { @@ -71,7 +77,9 @@ impl From for RouteKindDTO { json_str: serde_json::to_string(&json).expect("unreachable"), }, RawRoute::Raw { raw } => RouteKindDTO::Raw { raw }, - RawRoute::Sse { sse } => RouteKindDTO::Sse { sse }, + RawRoute::Sse { sse: opts } => RouteKindDTO::Sse { + sse: SseDTOOpts { body: opts.body }, + }, }, RouteKind::Proxy(ProxyRoute { proxy }) => RouteKindDTO::Proxy { proxy }, RouteKind::Dir(DirRoute { dir, base }) => RouteKindDTO::Dir { diff --git a/crates/bsnext_input/src/input_test/mod.rs b/crates/bsnext_input/src/input_test/mod.rs index 5e6ce923..b2f9cb14 100644 --- a/crates/bsnext_input/src/input_test/mod.rs +++ b/crates/bsnext_input/src/input_test/mod.rs @@ -257,26 +257,26 @@ servers: ] ) } -#[test] -fn test_deserialize_server_watch_run_opts() { - let input = r#" -servers: -- bind_address: 0.0.0.0:4000 - watchers: - - dirs: ./ - - dirs: ./other - debounce: - ms: 2000 - filter: - ext: "**/*.css" - run: - - sh: echo lol - - bslive: huh -"#; - let c: Result = serde_yaml::from_str(input); - assert!(c.is_err()); - // dbg!(&c.servers.get(0).unwrap().watchers.get(1).unwrap()); -} +// #[test] +// fn test_deserialize_server_watch_run_opts() { +// let input = r#" +// servers: +// - bind_address: 0.0.0.0:4000 +// watchers: +// - dirs: ./ +// - dirs: ./other +// debounce: +// ms: 2000 +// filter: +// ext: "**/*.css" +// run: +// - sh: echo lol +// - bslive: huh +// "#; +// let c: Result = serde_yaml::from_str(input); +// assert!(c.is_err()); +// // dbg!(&c.servers.get(0).unwrap().watchers.get(1).unwrap()); +// } #[test] fn test_deserialize_server_clients_config() { let input = r#" diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index f0564e3a..757a7db2 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -136,7 +136,12 @@ impl RouteKind { RouteKind::Raw(RawRoute::Json { json }) } pub fn new_sse(raw: impl Into) -> Self { - RouteKind::Raw(RawRoute::Sse { sse: raw.into() }) + RouteKind::Raw(RawRoute::Sse { + sse: SseOpts { + body: raw.into(), + throttle_ms: None, + }, + }) } } @@ -185,7 +190,13 @@ pub enum RawRoute { Html { html: String }, Json { json: JsonWrapper }, Raw { raw: String }, - Sse { sse: String }, + Sse { sse: SseOpts }, +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +pub struct SseOpts { + pub body: String, + pub throttle_ms: Option, } #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] diff --git a/examples/openai/bslive.yml b/examples/openai/bslive.yml index e04f85f7..3088aecd 100644 --- a/examples/openai/bslive.yml +++ b/examples/openai/bslive.yml @@ -7,9 +7,13 @@ servers: proxy: https://api.openai.com - path: /openai/v1/chat/completions cors: true - sse: | - data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} - - data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"Thsis"},"logprobs":null,"finish_reason":null}]} - - data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} + sse: + body: | + data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"Thsis"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} + + data: [DONE] + diff --git a/generated/dto.ts b/generated/dto.ts index a4ba14e4..8950052c 100644 --- a/generated/dto.ts +++ b/generated/dto.ts @@ -82,7 +82,7 @@ export type RouteKindDTO = raw: string; }} | { kind: "Sse", payload: { - sse: string; + sse: SseDTOOpts; }} | { kind: "Proxy", payload: { proxy: string; @@ -126,6 +126,10 @@ export interface ServersChangedDTO { servers_resp: GetActiveServersResponseDTO; } +export interface SseDTOOpts { + body: string; +} + export interface StderrLineDTO { line: string; prefix?: string; diff --git a/generated/schema.js b/generated/schema.js index cba5f593..eab6b50f 100644 --- a/generated/schema.js +++ b/generated/schema.js @@ -88,6 +88,9 @@ var injectConfigSchema = z.object({ var inputAcceptedDTOSchema = z.object({ path: z.string() }); +var sseDTOOptsSchema = z.object({ + body: z.string() +}); var routeKindDTOSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("Html"), @@ -110,7 +113,7 @@ var routeKindDTOSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("Sse"), payload: z.object({ - sse: z.string() + sse: sseDTOOptsSchema }) }), z.object({ @@ -127,10 +130,6 @@ var routeKindDTOSchema = z.discriminatedUnion("kind", [ }) }) ]); -var routeDTOSchema = z.object({ - path: z.string(), - kind: routeKindDTOSchema -}); var serverChangeSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("Stopped"), @@ -160,9 +159,9 @@ var serverChangeSetItemSchema = z.object({ var serverChangeSetSchema = z.object({ items: z.array(serverChangeSetItemSchema) }); -var serverDescSchema = z.object({ - routes: z.array(routeDTOSchema), - id: z.string() +var routeDTOSchema = z.object({ + path: z.string(), + kind: routeKindDTOSchema }); var serversChangedDTOSchema = z.object({ servers_resp: getActiveServersResponseDTOSchema @@ -307,6 +306,10 @@ var startupEventDTOSchema = z.discriminatedUnion("kind", [ payload: z.string() }) ]); +var serverDescSchema = z.object({ + routes: z.array(routeDTOSchema), + id: z.string() +}); var externalEventsDTOSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("ServersChanged"), @@ -368,6 +371,7 @@ export { serverDescSchema, serverIdentityDTOSchema, serversChangedDTOSchema, + sseDTOOptsSchema, startupEventDTOSchema, stderrLineDTOSchema, stdoutLineDTOSchema, diff --git a/generated/schema.ts b/generated/schema.ts index 279ed8c2..c0ca99e8 100644 --- a/generated/schema.ts +++ b/generated/schema.ts @@ -80,6 +80,10 @@ export const inputAcceptedDTOSchema = z.object({ path: z.string(), }); +export const sseDTOOptsSchema = z.object({ + body: z.string(), +}); + export const routeKindDTOSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("Html"), @@ -102,7 +106,7 @@ export const routeKindDTOSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("Sse"), payload: z.object({ - sse: z.string(), + sse: sseDTOOptsSchema, }), }), z.object({ @@ -120,11 +124,6 @@ export const routeKindDTOSchema = z.discriminatedUnion("kind", [ }), ]); -export const routeDTOSchema = z.object({ - path: z.string(), - kind: routeKindDTOSchema, -}); - export const serverChangeSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("Stopped"), @@ -157,9 +156,9 @@ export const serverChangeSetSchema = z.object({ items: z.array(serverChangeSetItemSchema), }); -export const serverDescSchema = z.object({ - routes: z.array(routeDTOSchema), - id: z.string(), +export const routeDTOSchema = z.object({ + path: z.string(), + kind: routeKindDTOSchema, }); export const serversChangedDTOSchema = z.object({ @@ -318,6 +317,11 @@ export const startupEventDTOSchema = z.discriminatedUnion("kind", [ }), ]); +export const serverDescSchema = z.object({ + routes: z.array(routeDTOSchema), + id: z.string(), +}); + export const externalEventsDTOSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("ServersChanged"), diff --git a/inject/dist/index.js b/inject/dist/index.js index 7fe28094..db744fc9 100644 --- a/inject/dist/index.js +++ b/inject/dist/index.js @@ -1,3 +1,3 @@ var ln=Object.create;var er=Object.defineProperty;var dn=Object.getOwnPropertyDescriptor;var fn=Object.getOwnPropertyNames;var pn=Object.getPrototypeOf,hn=Object.prototype.hasOwnProperty;var mn=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);var vn=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of fn(e))!hn.call(r,i)&&i!==t&&er(r,i,{get:()=>e[i],enumerable:!(n=dn(e,i))||n.enumerable});return r};var yn=(r,e,t)=>(t=r!=null?ln(pn(r)):{},vn(e||!r||!r.__esModule?er(t,"default",{value:r,enumerable:!0}):t,r));var cn=mn(sn=>{"use strict";var At=class{constructor(e){this.func=e,this.running=!1,this.id=null,this._handler=()=>(this.running=!1,this.id=null,this.func())}start(e){this.running&&clearTimeout(this.id),this.id=setTimeout(this._handler,e),this.running=!0}stop(){this.running&&(clearTimeout(this.id),this.running=!1,this.id=null)}};At.start=(r,e)=>setTimeout(e,r);sn.Timer=At});var Rt=function(r,e){return Rt=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,n){t.__proto__=n}||function(t,n){for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])},Rt(r,e)};function A(r,e){if(typeof e!="function"&&e!==null)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");Rt(r,e);function t(){this.constructor=r}r.prototype=e===null?Object.create(e):(t.prototype=e.prototype,new t)}var Qe=function(){return Qe=Object.assign||function(e){for(var t,n=1,i=arguments.length;n0&&o[o.length-1])&&(l[0]===6||l[0]===2)){t=0;continue}if(l[0]===3&&(!o||l[1]>o[0]&&l[1]=r.length&&(r=void 0),{value:r&&r[n++],done:!r}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(r,e){var t=typeof Symbol=="function"&&r[Symbol.iterator];if(!t)return r;var n=t.call(r),i,o=[],a;try{for(;(e===void 0||e-- >0)&&!(i=n.next()).done;)o.push(i.value)}catch(s){a={error:s}}finally{try{i&&!i.done&&(t=n.return)&&t.call(n)}finally{if(a)throw a.error}}return o}function V(r,e,t){if(t||arguments.length===2)for(var n=0,i=e.length,o;n1||u(g,w)})},O&&(i[g]=O(i[g])))}function u(g,O){try{l(n[g](O))}catch(w){T(o[0][3],w)}}function l(g){g.value instanceof ae?Promise.resolve(g.value.v).then(d,v):T(o[0][2],g)}function d(g){u("next",g)}function v(g){u("throw",g)}function T(g,O){g(O),o.shift(),o.length&&u(o[0][0],o[0][1])}}function nr(r){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var e=r[Symbol.asyncIterator],t;return e?e.call(r):(r=typeof J=="function"?J(r):r[Symbol.iterator](),t={},n("next"),n("throw"),n("return"),t[Symbol.asyncIterator]=function(){return this},t);function n(o){t[o]=r[o]&&function(a){return new Promise(function(s,u){a=r[o](a),i(s,u,a.done,a.value)})}}function i(o,a,s,u){Promise.resolve(u).then(function(l){o({value:l,done:s})},a)}}function S(r){return typeof r=="function"}function tt(r){var e=function(n){Error.call(n),n.stack=new Error().stack},t=r(e);return t.prototype=Object.create(Error.prototype),t.prototype.constructor=t,t}var rt=tt(function(r){return function(t){r(this),this.message=t?t.length+` errors occurred during unsubscription: `+t.map(function(n,i){return i+1+") "+n.toString()}).join(` - `):"",this.name="UnsubscriptionError",this.errors=t}});function se(r,e){if(r){var t=r.indexOf(e);0<=t&&r.splice(t,1)}}var H=function(){function r(e){this.initialTeardown=e,this.closed=!1,this._parentage=null,this._finalizers=null}return r.prototype.unsubscribe=function(){var e,t,n,i,o;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=J(a),u=s.next();!u.done;u=s.next()){var l=u.value;l.remove(this)}}catch(w){e={error:w}}finally{try{u&&!u.done&&(t=s.return)&&t.call(s)}finally{if(e)throw e.error}}else a.remove(this);var d=this.initialTeardown;if(S(d))try{d()}catch(w){o=w instanceof rt?w.errors:[w]}var v=this._finalizers;if(v){this._finalizers=null;try{for(var T=J(v),g=T.next();!g.done;g=T.next()){var O=g.value;try{ir(O)}catch(w){o=o??[],w instanceof rt?o=V(V([],W(o)),W(w.errors)):o.push(w)}}}catch(w){n={error:w}}finally{try{g&&!g.done&&(i=T.return)&&i.call(T)}finally{if(n)throw n.error}}}if(o)throw new rt(o)}},r.prototype.add=function(e){var t;if(e&&e!==this)if(this.closed)ir(e);else{if(e instanceof r){if(e.closed||e._hasParent(this))return;e._addParent(this)}(this._finalizers=(t=this._finalizers)!==null&&t!==void 0?t:[]).push(e)}},r.prototype._hasParent=function(e){var t=this._parentage;return t===e||Array.isArray(t)&&t.includes(e)},r.prototype._addParent=function(e){var t=this._parentage;this._parentage=Array.isArray(t)?(t.push(e),t):t?[t,e]:e},r.prototype._removeParent=function(e){var t=this._parentage;t===e?this._parentage=null:Array.isArray(t)&&se(t,e)},r.prototype.remove=function(e){var t=this._finalizers;t&&se(t,e),e instanceof r&&e._removeParent(this)},r.EMPTY=function(){var e=new r;return e.closed=!0,e}(),r}();var Pt=H.EMPTY;function nt(r){return r instanceof H||r&&"closed"in r&&S(r.remove)&&S(r.add)&&S(r.unsubscribe)}function ir(r){S(r)?r():r.unsubscribe()}var z={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var je={setTimeout:function(r,e){for(var t=[],n=2;n0},enumerable:!1,configurable:!0}),e.prototype._trySubscribe=function(t){return this._throwIfClosed(),r.prototype._trySubscribe.call(this,t)},e.prototype._subscribe=function(t){return this._throwIfClosed(),this._checkFinalizedStatuses(t),this._innerSubscribe(t)},e.prototype._innerSubscribe=function(t){var n=this,i=this,o=i.hasError,a=i.isStopped,s=i.observers;return o||a?Pt:(this.currentObservers=null,s.push(t),new H(function(){n.currentObservers=null,se(s,t)}))},e.prototype._checkFinalizedStatuses=function(t){var n=this,i=n.hasError,o=n.thrownError,a=n.isStopped;i?t.error(o):a&&t.complete()},e.prototype.asObservable=function(){var t=new E;return t.source=this,t},e.create=function(t,n){return new at(t,n)},e}(E);var at=function(r){A(e,r);function e(t,n){var i=r.call(this)||this;return i.destination=t,i.source=n,i}return e.prototype.next=function(t){var n,i;(i=(n=this.destination)===null||n===void 0?void 0:n.next)===null||i===void 0||i.call(n,t)},e.prototype.error=function(t){var n,i;(i=(n=this.destination)===null||n===void 0?void 0:n.error)===null||i===void 0||i.call(n,t)},e.prototype.complete=function(){var t,n;(n=(t=this.destination)===null||t===void 0?void 0:t.complete)===null||n===void 0||n.call(t)},e.prototype._subscribe=function(t){var n,i;return(i=(n=this.source)===null||n===void 0?void 0:n.subscribe(t))!==null&&i!==void 0?i:Pt},e}(B);var We={now:function(){return(We.delegate||Date).now()},delegate:void 0};var st=function(r){A(e,r);function e(t,n,i){t===void 0&&(t=1/0),n===void 0&&(n=1/0),i===void 0&&(i=We);var o=r.call(this)||this;return o._bufferSize=t,o._windowTime=n,o._timestampProvider=i,o._buffer=[],o._infiniteTimeWindow=!0,o._infiniteTimeWindow=n===1/0,o._bufferSize=Math.max(1,t),o._windowTime=Math.max(1,n),o}return e.prototype.next=function(t){var n=this,i=n.isStopped,o=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,u=n._windowTime;i||(o.push(t),!a&&o.push(s.now()+u)),this._trimBuffer(),r.prototype.next.call(this,t)},e.prototype._subscribe=function(t){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(t),i=this,o=i._infiniteTimeWindow,a=i._buffer,s=a.slice(),u=0;u0&&(d=new le({next:function(Xe){return Ke.next(Xe)},error:function(Xe){w=!0,R(),v=Ut(L,i,Xe),Ke.error(Xe)},complete:function(){O=!0,R(),v=Ut(L,a),Ke.complete()}}),I(Ue).subscribe(d))})(l)}}function Ut(r,e){for(var t=[],n=2;n{let e=new URL(window.location.href),t=e.protocol==="https:"?"wss":"ws",n;if(r.host)n=new URL(r.ws_path,t+"://"+r.host);else{let o=new URL(e);o.protocol=t,o.pathname=r.ws_path,n=o}return Vt(n.toString()).pipe(Ft({delay:5e3}))}}}var Nr={debug(...r){},error(...r){},info(...r){},trace(...r){}},zt={name:"console",globalSetup:r=>{let e=new B;return[e,{debug:function(...n){e.next({level:"debug",args:n})},info:function(...n){e.next({level:"info",args:n})},trace:function(...n){e.next({level:"trace",args:n})},error:function(...n){e.next({level:"error",args:n})}}]},resetSink:(r,e,t)=>r.pipe(Be(n=>{let i=["trace","debug","info","error"],o=i.indexOf(n.level),a=i.indexOf(t.log_level);o>=a&&console.log(`[${n.level}]`,...n.args)}),ze())};var k;(function(r){r.assertEqual=i=>i;function e(i){}r.assertIs=e;function t(i){throw new Error}r.assertNever=t,r.arrayToEnum=i=>{let o={};for(let a of i)o[a]=a;return o},r.getValidEnumValues=i=>{let o=r.objectKeys(i).filter(s=>typeof i[i[s]]!="number"),a={};for(let s of o)a[s]=i[s];return r.objectValues(a)},r.objectValues=i=>r.objectKeys(i).map(function(o){return i[o]}),r.objectKeys=typeof Object.keys=="function"?i=>Object.keys(i):i=>{let o=[];for(let a in i)Object.prototype.hasOwnProperty.call(i,a)&&o.push(a);return o},r.find=(i,o)=>{for(let a of i)if(o(a))return a},r.isInteger=typeof Number.isInteger=="function"?i=>Number.isInteger(i):i=>typeof i=="number"&&isFinite(i)&&Math.floor(i)===i;function n(i,o=" | "){return i.map(a=>typeof a=="string"?`'${a}'`:a).join(o)}r.joinValues=n,r.jsonStringifyReplacer=(i,o)=>typeof o=="bigint"?o.toString():o})(k||(k={}));var qt;(function(r){r.mergeShapes=(e,t)=>({...e,...t})})(qt||(qt={}));var h=k.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),Q=r=>{switch(typeof r){case"undefined":return h.undefined;case"string":return h.string;case"number":return isNaN(r)?h.nan:h.number;case"boolean":return h.boolean;case"function":return h.function;case"bigint":return h.bigint;case"symbol":return h.symbol;case"object":return Array.isArray(r)?h.array:r===null?h.null:r.then&&typeof r.then=="function"&&r.catch&&typeof r.catch=="function"?h.promise:typeof Map<"u"&&r instanceof Map?h.map:typeof Set<"u"&&r instanceof Set?h.set:typeof Date<"u"&&r instanceof Date?h.date:h.object;default:return h.unknown}},f=k.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]),Ln=r=>JSON.stringify(r,null,2).replace(/"([^"]+)":/g,"$1:"),M=class r extends Error{get errors(){return this.issues}constructor(e){super(),this.issues=[],this.addIssue=n=>{this.issues=[...this.issues,n]},this.addIssues=(n=[])=>{this.issues=[...this.issues,...n]};let t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name="ZodError",this.issues=e}format(e){let t=e||function(o){return o.message},n={_errors:[]},i=o=>{for(let a of o.issues)if(a.code==="invalid_union")a.unionErrors.map(i);else if(a.code==="invalid_return_type")i(a.returnTypeError);else if(a.code==="invalid_arguments")i(a.argumentsError);else if(a.path.length===0)n._errors.push(t(a));else{let s=n,u=0;for(;ut.message){let t={},n=[];for(let i of this.issues)i.path.length>0?(t[i.path[0]]=t[i.path[0]]||[],t[i.path[0]].push(e(i))):n.push(e(i));return{formErrors:n,fieldErrors:t}}get formErrors(){return this.flatten()}};M.create=r=>new M(r);var Ne=(r,e)=>{let t;switch(r.code){case f.invalid_type:r.received===h.undefined?t="Required":t=`Expected ${r.expected}, received ${r.received}`;break;case f.invalid_literal:t=`Invalid literal value, expected ${JSON.stringify(r.expected,k.jsonStringifyReplacer)}`;break;case f.unrecognized_keys:t=`Unrecognized key(s) in object: ${k.joinValues(r.keys,", ")}`;break;case f.invalid_union:t="Invalid input";break;case f.invalid_union_discriminator:t=`Invalid discriminator value. Expected ${k.joinValues(r.options)}`;break;case f.invalid_enum_value:t=`Invalid enum value. Expected ${k.joinValues(r.options)}, received '${r.received}'`;break;case f.invalid_arguments:t="Invalid function arguments";break;case f.invalid_return_type:t="Invalid function return type";break;case f.invalid_date:t="Invalid date";break;case f.invalid_string:typeof r.validation=="object"?"includes"in r.validation?(t=`Invalid input: must include "${r.validation.includes}"`,typeof r.validation.position=="number"&&(t=`${t} at one or more positions greater than or equal to ${r.validation.position}`)):"startsWith"in r.validation?t=`Invalid input: must start with "${r.validation.startsWith}"`:"endsWith"in r.validation?t=`Invalid input: must end with "${r.validation.endsWith}"`:k.assertNever(r.validation):r.validation!=="regex"?t=`Invalid ${r.validation}`:t="Invalid";break;case f.too_small:r.type==="array"?t=`Array must contain ${r.exact?"exactly":r.inclusive?"at least":"more than"} ${r.minimum} element(s)`:r.type==="string"?t=`String must contain ${r.exact?"exactly":r.inclusive?"at least":"over"} ${r.minimum} character(s)`:r.type==="number"?t=`Number must be ${r.exact?"exactly equal to ":r.inclusive?"greater than or equal to ":"greater than "}${r.minimum}`:r.type==="date"?t=`Date must be ${r.exact?"exactly equal to ":r.inclusive?"greater than or equal to ":"greater than "}${new Date(Number(r.minimum))}`:t="Invalid input";break;case f.too_big:r.type==="array"?t=`Array must contain ${r.exact?"exactly":r.inclusive?"at most":"less than"} ${r.maximum} element(s)`:r.type==="string"?t=`String must contain ${r.exact?"exactly":r.inclusive?"at most":"under"} ${r.maximum} character(s)`:r.type==="number"?t=`Number must be ${r.exact?"exactly":r.inclusive?"less than or equal to":"less than"} ${r.maximum}`:r.type==="bigint"?t=`BigInt must be ${r.exact?"exactly":r.inclusive?"less than or equal to":"less than"} ${r.maximum}`:r.type==="date"?t=`Date must be ${r.exact?"exactly":r.inclusive?"smaller than or equal to":"smaller than"} ${new Date(Number(r.maximum))}`:t="Invalid input";break;case f.custom:t="Invalid input";break;case f.invalid_intersection_types:t="Intersection results could not be merged";break;case f.not_multiple_of:t=`Number must be a multiple of ${r.multipleOf}`;break;case f.not_finite:t="Number must be finite";break;default:t=e.defaultError,k.assertNever(r)}return{message:t}},Zr=Ne;function Mn(r){Zr=r}function wt(){return Zr}var St=r=>{let{data:e,path:t,errorMaps:n,issueData:i}=r,o=[...t,...i.path||[]],a={...i,path:o};if(i.message!==void 0)return{...i,path:o,message:i.message};let s="",u=n.filter(l=>!!l).slice().reverse();for(let l of u)s=l(a,{data:e,defaultError:s}).message;return{...i,path:o,message:s}},Zn=[];function p(r,e){let t=wt(),n=St({issueData:e,data:r.data,path:r.path,errorMaps:[r.common.contextualErrorMap,r.schemaErrorMap,t,t===Ne?void 0:Ne].filter(i=>!!i)});r.common.issues.push(n)}var P=class r{constructor(){this.value="valid"}dirty(){this.value==="valid"&&(this.value="dirty")}abort(){this.value!=="aborted"&&(this.value="aborted")}static mergeArray(e,t){let n=[];for(let i of t){if(i.status==="aborted")return _;i.status==="dirty"&&e.dirty(),n.push(i.value)}return{status:e.value,value:n}}static async mergeObjectAsync(e,t){let n=[];for(let i of t){let o=await i.key,a=await i.value;n.push({key:o,value:a})}return r.mergeObjectSync(e,n)}static mergeObjectSync(e,t){let n={};for(let i of t){let{key:o,value:a}=i;if(o.status==="aborted"||a.status==="aborted")return _;o.status==="dirty"&&e.dirty(),a.status==="dirty"&&e.dirty(),o.value!=="__proto__"&&(typeof a.value<"u"||i.alwaysSet)&&(n[o.value]=a.value)}return{status:e.value,value:n}}},_=Object.freeze({status:"aborted"}),Pe=r=>({status:"dirty",value:r}),N=r=>({status:"valid",value:r}),Ht=r=>r.status==="aborted",Gt=r=>r.status==="dirty",fe=r=>r.status==="valid",Ge=r=>typeof Promise<"u"&&r instanceof Promise;function kt(r,e,t,n){if(t==="a"&&!n)throw new TypeError("Private accessor was defined without a getter");if(typeof e=="function"?r!==e||!n:!e.has(r))throw new TypeError("Cannot read private member from an object whose class did not declare it");return t==="m"?n:t==="a"?n.call(r):n?n.value:e.get(r)}function Fr(r,e,t,n,i){if(n==="m")throw new TypeError("Private method is not writable");if(n==="a"&&!i)throw new TypeError("Private accessor was defined without a setter");if(typeof e=="function"?r!==e||!i:!e.has(r))throw new TypeError("Cannot write private member to an object whose class did not declare it");return n==="a"?i.call(r,t):i?i.value=t:e.set(r,t),t}var m;(function(r){r.errToObj=e=>typeof e=="string"?{message:e}:e||{},r.toString=e=>typeof e=="string"?e:e?.message})(m||(m={}));var qe,He,$=class{constructor(e,t,n,i){this._cachedPath=[],this.parent=e,this.data=t,this._path=n,this._key=i}get path(){return this._cachedPath.length||(this._key instanceof Array?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}},Dr=(r,e)=>{if(fe(e))return{success:!0,data:e.value};if(!r.common.issues.length)throw new Error("Validation failed but no issues detected.");return{success:!1,get error(){if(this._error)return this._error;let t=new M(r.common.issues);return this._error=t,this._error}}};function b(r){if(!r)return{};let{errorMap:e,invalid_type_error:t,required_error:n,description:i}=r;if(e&&(t||n))throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);return e?{errorMap:e,description:i}:{errorMap:(a,s)=>{var u,l;let{message:d}=r;return a.code==="invalid_enum_value"?{message:d??s.defaultError}:typeof s.data>"u"?{message:(u=d??n)!==null&&u!==void 0?u:s.defaultError}:a.code!=="invalid_type"?{message:s.defaultError}:{message:(l=d??t)!==null&&l!==void 0?l:s.defaultError}},description:i}}var x=class{get description(){return this._def.description}_getType(e){return Q(e.data)}_getOrReturnCtx(e,t){return t||{common:e.parent.common,data:e.data,parsedType:Q(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}_processInputParams(e){return{status:new P,ctx:{common:e.parent.common,data:e.data,parsedType:Q(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}}_parseSync(e){let t=this._parse(e);if(Ge(t))throw new Error("Synchronous parse encountered promise.");return t}_parseAsync(e){let t=this._parse(e);return Promise.resolve(t)}parse(e,t){let n=this.safeParse(e,t);if(n.success)return n.data;throw n.error}safeParse(e,t){var n;let i={common:{issues:[],async:(n=t?.async)!==null&&n!==void 0?n:!1,contextualErrorMap:t?.errorMap},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Q(e)},o=this._parseSync({data:e,path:i.path,parent:i});return Dr(i,o)}"~validate"(e){var t,n;let i={common:{issues:[],async:!!this["~standard"].async},path:[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Q(e)};if(!this["~standard"].async)try{let o=this._parseSync({data:e,path:[],parent:i});return fe(o)?{value:o.value}:{issues:i.common.issues}}catch(o){!((n=(t=o?.message)===null||t===void 0?void 0:t.toLowerCase())===null||n===void 0)&&n.includes("encountered")&&(this["~standard"].async=!0),i.common={issues:[],async:!0}}return this._parseAsync({data:e,path:[],parent:i}).then(o=>fe(o)?{value:o.value}:{issues:i.common.issues})}async parseAsync(e,t){let n=await this.safeParseAsync(e,t);if(n.success)return n.data;throw n.error}async safeParseAsync(e,t){let n={common:{issues:[],contextualErrorMap:t?.errorMap,async:!0},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Q(e)},i=this._parse({data:e,path:n.path,parent:n}),o=await(Ge(i)?i:Promise.resolve(i));return Dr(n,o)}refine(e,t){let n=i=>typeof t=="string"||typeof t>"u"?{message:t}:typeof t=="function"?t(i):t;return this._refinement((i,o)=>{let a=e(i),s=()=>o.addIssue({code:f.custom,...n(i)});return typeof Promise<"u"&&a instanceof Promise?a.then(u=>u?!0:(s(),!1)):a?!0:(s(),!1)})}refinement(e,t){return this._refinement((n,i)=>e(n)?!0:(i.addIssue(typeof t=="function"?t(n,i):t),!1))}_refinement(e){return new Z({schema:this,typeName:y.ZodEffects,effect:{type:"refinement",refinement:e}})}superRefine(e){return this._refinement(e)}constructor(e){this.spa=this.safeParseAsync,this._def=e,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.readonly=this.readonly.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this),this["~standard"]={version:1,vendor:"zod",validate:t=>this["~validate"](t)}}optional(){return U.create(this,this._def)}nullable(){return Y.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return te.create(this)}promise(){return oe.create(this,this._def)}or(e){return _e.create([this,e],this._def)}and(e){return be.create(this,e,this._def)}transform(e){return new Z({...b(this._def),schema:this,typeName:y.ZodEffects,effect:{type:"transform",transform:e}})}default(e){let t=typeof e=="function"?e:()=>e;return new Te({...b(this._def),innerType:this,defaultValue:t,typeName:y.ZodDefault})}brand(){return new Ye({typeName:y.ZodBranded,type:this,...b(this._def)})}catch(e){let t=typeof e=="function"?e:()=>e;return new Oe({...b(this._def),innerType:this,catchValue:t,typeName:y.ZodCatch})}describe(e){let t=this.constructor;return new t({...this._def,description:e})}pipe(e){return Je.create(this,e)}readonly(){return Ee.create(this)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}},Fn=/^c[^\s-]{8,}$/i,Un=/^[0-9a-z]+$/,$n=/^[0-9A-HJKMNP-TV-Z]{26}$/i,Wn=/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i,Vn=/^[a-z0-9_-]{21}$/i,zn=/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/,Bn=/^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/,qn=/^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i,Hn="^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$",Bt,Gn=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,Yn=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/,Jn=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/,Kn=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,Xn=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,Qn=/^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/,Ur="((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))",ei=new RegExp(`^${Ur}$`);function $r(r){let e="([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d";return r.precision?e=`${e}\\.\\d{${r.precision}}`:r.precision==null&&(e=`${e}(\\.\\d+)?`),e}function ti(r){return new RegExp(`^${$r(r)}$`)}function Wr(r){let e=`${Ur}T${$r(r)}`,t=[];return t.push(r.local?"Z?":"Z"),r.offset&&t.push("([+-]\\d{2}:?\\d{2})"),e=`${e}(${t.join("|")})`,new RegExp(`^${e}$`)}function ri(r,e){return!!((e==="v4"||!e)&&Gn.test(r)||(e==="v6"||!e)&&Jn.test(r))}function ni(r,e){if(!zn.test(r))return!1;try{let[t]=r.split("."),n=t.replace(/-/g,"+").replace(/_/g,"/").padEnd(t.length+(4-t.length%4)%4,"="),i=JSON.parse(atob(n));return!(typeof i!="object"||i===null||!i.typ||!i.alg||e&&i.alg!==e)}catch{return!1}}function ii(r,e){return!!((e==="v4"||!e)&&Yn.test(r)||(e==="v6"||!e)&&Kn.test(r))}var ne=class r extends x{_parse(e){if(this._def.coerce&&(e.data=String(e.data)),this._getType(e)!==h.string){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_type,expected:h.string,received:o.parsedType}),_}let n=new P,i;for(let o of this._def.checks)if(o.kind==="min")e.data.lengtho.value&&(i=this._getOrReturnCtx(e,i),p(i,{code:f.too_big,maximum:o.value,type:"string",inclusive:!0,exact:!1,message:o.message}),n.dirty());else if(o.kind==="length"){let a=e.data.length>o.value,s=e.data.lengthe.test(i),{validation:t,code:f.invalid_string,...m.errToObj(n)})}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}email(e){return this._addCheck({kind:"email",...m.errToObj(e)})}url(e){return this._addCheck({kind:"url",...m.errToObj(e)})}emoji(e){return this._addCheck({kind:"emoji",...m.errToObj(e)})}uuid(e){return this._addCheck({kind:"uuid",...m.errToObj(e)})}nanoid(e){return this._addCheck({kind:"nanoid",...m.errToObj(e)})}cuid(e){return this._addCheck({kind:"cuid",...m.errToObj(e)})}cuid2(e){return this._addCheck({kind:"cuid2",...m.errToObj(e)})}ulid(e){return this._addCheck({kind:"ulid",...m.errToObj(e)})}base64(e){return this._addCheck({kind:"base64",...m.errToObj(e)})}base64url(e){return this._addCheck({kind:"base64url",...m.errToObj(e)})}jwt(e){return this._addCheck({kind:"jwt",...m.errToObj(e)})}ip(e){return this._addCheck({kind:"ip",...m.errToObj(e)})}cidr(e){return this._addCheck({kind:"cidr",...m.errToObj(e)})}datetime(e){var t,n;return typeof e=="string"?this._addCheck({kind:"datetime",precision:null,offset:!1,local:!1,message:e}):this._addCheck({kind:"datetime",precision:typeof e?.precision>"u"?null:e?.precision,offset:(t=e?.offset)!==null&&t!==void 0?t:!1,local:(n=e?.local)!==null&&n!==void 0?n:!1,...m.errToObj(e?.message)})}date(e){return this._addCheck({kind:"date",message:e})}time(e){return typeof e=="string"?this._addCheck({kind:"time",precision:null,message:e}):this._addCheck({kind:"time",precision:typeof e?.precision>"u"?null:e?.precision,...m.errToObj(e?.message)})}duration(e){return this._addCheck({kind:"duration",...m.errToObj(e)})}regex(e,t){return this._addCheck({kind:"regex",regex:e,...m.errToObj(t)})}includes(e,t){return this._addCheck({kind:"includes",value:e,position:t?.position,...m.errToObj(t?.message)})}startsWith(e,t){return this._addCheck({kind:"startsWith",value:e,...m.errToObj(t)})}endsWith(e,t){return this._addCheck({kind:"endsWith",value:e,...m.errToObj(t)})}min(e,t){return this._addCheck({kind:"min",value:e,...m.errToObj(t)})}max(e,t){return this._addCheck({kind:"max",value:e,...m.errToObj(t)})}length(e,t){return this._addCheck({kind:"length",value:e,...m.errToObj(t)})}nonempty(e){return this.min(1,m.errToObj(e))}trim(){return new r({...this._def,checks:[...this._def.checks,{kind:"trim"}]})}toLowerCase(){return new r({...this._def,checks:[...this._def.checks,{kind:"toLowerCase"}]})}toUpperCase(){return new r({...this._def,checks:[...this._def.checks,{kind:"toUpperCase"}]})}get isDatetime(){return!!this._def.checks.find(e=>e.kind==="datetime")}get isDate(){return!!this._def.checks.find(e=>e.kind==="date")}get isTime(){return!!this._def.checks.find(e=>e.kind==="time")}get isDuration(){return!!this._def.checks.find(e=>e.kind==="duration")}get isEmail(){return!!this._def.checks.find(e=>e.kind==="email")}get isURL(){return!!this._def.checks.find(e=>e.kind==="url")}get isEmoji(){return!!this._def.checks.find(e=>e.kind==="emoji")}get isUUID(){return!!this._def.checks.find(e=>e.kind==="uuid")}get isNANOID(){return!!this._def.checks.find(e=>e.kind==="nanoid")}get isCUID(){return!!this._def.checks.find(e=>e.kind==="cuid")}get isCUID2(){return!!this._def.checks.find(e=>e.kind==="cuid2")}get isULID(){return!!this._def.checks.find(e=>e.kind==="ulid")}get isIP(){return!!this._def.checks.find(e=>e.kind==="ip")}get isCIDR(){return!!this._def.checks.find(e=>e.kind==="cidr")}get isBase64(){return!!this._def.checks.find(e=>e.kind==="base64")}get isBase64url(){return!!this._def.checks.find(e=>e.kind==="base64url")}get minLength(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxLength(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.value{var e;return new ne({checks:[],typeName:y.ZodString,coerce:(e=r?.coerce)!==null&&e!==void 0?e:!1,...b(r)})};function oi(r,e){let t=(r.toString().split(".")[1]||"").length,n=(e.toString().split(".")[1]||"").length,i=t>n?t:n,o=parseInt(r.toFixed(i).replace(".","")),a=parseInt(e.toFixed(i).replace(".",""));return o%a/Math.pow(10,i)}var pe=class r extends x{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte,this.step=this.multipleOf}_parse(e){if(this._def.coerce&&(e.data=Number(e.data)),this._getType(e)!==h.number){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_type,expected:h.number,received:o.parsedType}),_}let n,i=new P;for(let o of this._def.checks)o.kind==="int"?k.isInteger(e.data)||(n=this._getOrReturnCtx(e,n),p(n,{code:f.invalid_type,expected:"integer",received:"float",message:o.message}),i.dirty()):o.kind==="min"?(o.inclusive?e.datao.value:e.data>=o.value)&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.too_big,maximum:o.value,type:"number",inclusive:o.inclusive,exact:!1,message:o.message}),i.dirty()):o.kind==="multipleOf"?oi(e.data,o.value)!==0&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.not_multiple_of,multipleOf:o.value,message:o.message}),i.dirty()):o.kind==="finite"?Number.isFinite(e.data)||(n=this._getOrReturnCtx(e,n),p(n,{code:f.not_finite,message:o.message}),i.dirty()):k.assertNever(o);return{status:i.value,value:e.data}}gte(e,t){return this.setLimit("min",e,!0,m.toString(t))}gt(e,t){return this.setLimit("min",e,!1,m.toString(t))}lte(e,t){return this.setLimit("max",e,!0,m.toString(t))}lt(e,t){return this.setLimit("max",e,!1,m.toString(t))}setLimit(e,t,n,i){return new r({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:n,message:m.toString(i)}]})}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}int(e){return this._addCheck({kind:"int",message:m.toString(e)})}positive(e){return this._addCheck({kind:"min",value:0,inclusive:!1,message:m.toString(e)})}negative(e){return this._addCheck({kind:"max",value:0,inclusive:!1,message:m.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:0,inclusive:!0,message:m.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:0,inclusive:!0,message:m.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:m.toString(t)})}finite(e){return this._addCheck({kind:"finite",message:m.toString(e)})}safe(e){return this._addCheck({kind:"min",inclusive:!0,value:Number.MIN_SAFE_INTEGER,message:m.toString(e)})._addCheck({kind:"max",inclusive:!0,value:Number.MAX_SAFE_INTEGER,message:m.toString(e)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuee.kind==="int"||e.kind==="multipleOf"&&k.isInteger(e.value))}get isFinite(){let e=null,t=null;for(let n of this._def.checks){if(n.kind==="finite"||n.kind==="int"||n.kind==="multipleOf")return!0;n.kind==="min"?(t===null||n.value>t)&&(t=n.value):n.kind==="max"&&(e===null||n.valuenew pe({checks:[],typeName:y.ZodNumber,coerce:r?.coerce||!1,...b(r)});var he=class r extends x{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte}_parse(e){if(this._def.coerce)try{e.data=BigInt(e.data)}catch{return this._getInvalidInput(e)}if(this._getType(e)!==h.bigint)return this._getInvalidInput(e);let n,i=new P;for(let o of this._def.checks)o.kind==="min"?(o.inclusive?e.datao.value:e.data>=o.value)&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.too_big,type:"bigint",maximum:o.value,inclusive:o.inclusive,message:o.message}),i.dirty()):o.kind==="multipleOf"?e.data%o.value!==BigInt(0)&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.not_multiple_of,multipleOf:o.value,message:o.message}),i.dirty()):k.assertNever(o);return{status:i.value,value:e.data}}_getInvalidInput(e){let t=this._getOrReturnCtx(e);return p(t,{code:f.invalid_type,expected:h.bigint,received:t.parsedType}),_}gte(e,t){return this.setLimit("min",e,!0,m.toString(t))}gt(e,t){return this.setLimit("min",e,!1,m.toString(t))}lte(e,t){return this.setLimit("max",e,!0,m.toString(t))}lt(e,t){return this.setLimit("max",e,!1,m.toString(t))}setLimit(e,t,n,i){return new r({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:n,message:m.toString(i)}]})}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}positive(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!1,message:m.toString(e)})}negative(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!1,message:m.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!0,message:m.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!0,message:m.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:m.toString(t)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.value{var e;return new he({checks:[],typeName:y.ZodBigInt,coerce:(e=r?.coerce)!==null&&e!==void 0?e:!1,...b(r)})};var me=class extends x{_parse(e){if(this._def.coerce&&(e.data=!!e.data),this._getType(e)!==h.boolean){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.boolean,received:n.parsedType}),_}return N(e.data)}};me.create=r=>new me({typeName:y.ZodBoolean,coerce:r?.coerce||!1,...b(r)});var ve=class r extends x{_parse(e){if(this._def.coerce&&(e.data=new Date(e.data)),this._getType(e)!==h.date){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_type,expected:h.date,received:o.parsedType}),_}if(isNaN(e.data.getTime())){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_date}),_}let n=new P,i;for(let o of this._def.checks)o.kind==="min"?e.data.getTime()o.value&&(i=this._getOrReturnCtx(e,i),p(i,{code:f.too_big,message:o.message,inclusive:!0,exact:!1,maximum:o.value,type:"date"}),n.dirty()):k.assertNever(o);return{status:n.value,value:new Date(e.data.getTime())}}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}min(e,t){return this._addCheck({kind:"min",value:e.getTime(),message:m.toString(t)})}max(e,t){return this._addCheck({kind:"max",value:e.getTime(),message:m.toString(t)})}get minDate(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e!=null?new Date(e):null}get maxDate(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuenew ve({checks:[],coerce:r?.coerce||!1,typeName:y.ZodDate,...b(r)});var De=class extends x{_parse(e){if(this._getType(e)!==h.symbol){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.symbol,received:n.parsedType}),_}return N(e.data)}};De.create=r=>new De({typeName:y.ZodSymbol,...b(r)});var ye=class extends x{_parse(e){if(this._getType(e)!==h.undefined){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.undefined,received:n.parsedType}),_}return N(e.data)}};ye.create=r=>new ye({typeName:y.ZodUndefined,...b(r)});var ge=class extends x{_parse(e){if(this._getType(e)!==h.null){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.null,received:n.parsedType}),_}return N(e.data)}};ge.create=r=>new ge({typeName:y.ZodNull,...b(r)});var ie=class extends x{constructor(){super(...arguments),this._any=!0}_parse(e){return N(e.data)}};ie.create=r=>new ie({typeName:y.ZodAny,...b(r)});var ee=class extends x{constructor(){super(...arguments),this._unknown=!0}_parse(e){return N(e.data)}};ee.create=r=>new ee({typeName:y.ZodUnknown,...b(r)});var q=class extends x{_parse(e){let t=this._getOrReturnCtx(e);return p(t,{code:f.invalid_type,expected:h.never,received:t.parsedType}),_}};q.create=r=>new q({typeName:y.ZodNever,...b(r)});var Le=class extends x{_parse(e){if(this._getType(e)!==h.undefined){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.void,received:n.parsedType}),_}return N(e.data)}};Le.create=r=>new Le({typeName:y.ZodVoid,...b(r)});var te=class r extends x{_parse(e){let{ctx:t,status:n}=this._processInputParams(e),i=this._def;if(t.parsedType!==h.array)return p(t,{code:f.invalid_type,expected:h.array,received:t.parsedType}),_;if(i.exactLength!==null){let a=t.data.length>i.exactLength.value,s=t.data.lengthi.maxLength.value&&(p(t,{code:f.too_big,maximum:i.maxLength.value,type:"array",inclusive:!0,exact:!1,message:i.maxLength.message}),n.dirty()),t.common.async)return Promise.all([...t.data].map((a,s)=>i.type._parseAsync(new $(t,a,t.path,s)))).then(a=>P.mergeArray(n,a));let o=[...t.data].map((a,s)=>i.type._parseSync(new $(t,a,t.path,s)));return P.mergeArray(n,o)}get element(){return this._def.type}min(e,t){return new r({...this._def,minLength:{value:e,message:m.toString(t)}})}max(e,t){return new r({...this._def,maxLength:{value:e,message:m.toString(t)}})}length(e,t){return new r({...this._def,exactLength:{value:e,message:m.toString(t)}})}nonempty(e){return this.min(1,e)}};te.create=(r,e)=>new te({type:r,minLength:null,maxLength:null,exactLength:null,typeName:y.ZodArray,...b(e)});function Re(r){if(r instanceof D){let e={};for(let t in r.shape){let n=r.shape[t];e[t]=U.create(Re(n))}return new D({...r._def,shape:()=>e})}else return r instanceof te?new te({...r._def,type:Re(r.element)}):r instanceof U?U.create(Re(r.unwrap())):r instanceof Y?Y.create(Re(r.unwrap())):r instanceof G?G.create(r.items.map(e=>Re(e))):r}var D=class r extends x{constructor(){super(...arguments),this._cached=null,this.nonstrict=this.passthrough,this.augment=this.extend}_getCached(){if(this._cached!==null)return this._cached;let e=this._def.shape(),t=k.objectKeys(e);return this._cached={shape:e,keys:t}}_parse(e){if(this._getType(e)!==h.object){let l=this._getOrReturnCtx(e);return p(l,{code:f.invalid_type,expected:h.object,received:l.parsedType}),_}let{status:n,ctx:i}=this._processInputParams(e),{shape:o,keys:a}=this._getCached(),s=[];if(!(this._def.catchall instanceof q&&this._def.unknownKeys==="strip"))for(let l in i.data)a.includes(l)||s.push(l);let u=[];for(let l of a){let d=o[l],v=i.data[l];u.push({key:{status:"valid",value:l},value:d._parse(new $(i,v,i.path,l)),alwaysSet:l in i.data})}if(this._def.catchall instanceof q){let l=this._def.unknownKeys;if(l==="passthrough")for(let d of s)u.push({key:{status:"valid",value:d},value:{status:"valid",value:i.data[d]}});else if(l==="strict")s.length>0&&(p(i,{code:f.unrecognized_keys,keys:s}),n.dirty());else if(l!=="strip")throw new Error("Internal ZodObject error: invalid unknownKeys value.")}else{let l=this._def.catchall;for(let d of s){let v=i.data[d];u.push({key:{status:"valid",value:d},value:l._parse(new $(i,v,i.path,d)),alwaysSet:d in i.data})}}return i.common.async?Promise.resolve().then(async()=>{let l=[];for(let d of u){let v=await d.key,T=await d.value;l.push({key:v,value:T,alwaysSet:d.alwaysSet})}return l}).then(l=>P.mergeObjectSync(n,l)):P.mergeObjectSync(n,u)}get shape(){return this._def.shape()}strict(e){return m.errToObj,new r({...this._def,unknownKeys:"strict",...e!==void 0?{errorMap:(t,n)=>{var i,o,a,s;let u=(a=(o=(i=this._def).errorMap)===null||o===void 0?void 0:o.call(i,t,n).message)!==null&&a!==void 0?a:n.defaultError;return t.code==="unrecognized_keys"?{message:(s=m.errToObj(e).message)!==null&&s!==void 0?s:u}:{message:u}}}:{}})}strip(){return new r({...this._def,unknownKeys:"strip"})}passthrough(){return new r({...this._def,unknownKeys:"passthrough"})}extend(e){return new r({...this._def,shape:()=>({...this._def.shape(),...e})})}merge(e){return new r({unknownKeys:e._def.unknownKeys,catchall:e._def.catchall,shape:()=>({...this._def.shape(),...e._def.shape()}),typeName:y.ZodObject})}setKey(e,t){return this.augment({[e]:t})}catchall(e){return new r({...this._def,catchall:e})}pick(e){let t={};return k.objectKeys(e).forEach(n=>{e[n]&&this.shape[n]&&(t[n]=this.shape[n])}),new r({...this._def,shape:()=>t})}omit(e){let t={};return k.objectKeys(this.shape).forEach(n=>{e[n]||(t[n]=this.shape[n])}),new r({...this._def,shape:()=>t})}deepPartial(){return Re(this)}partial(e){let t={};return k.objectKeys(this.shape).forEach(n=>{let i=this.shape[n];e&&!e[n]?t[n]=i:t[n]=i.optional()}),new r({...this._def,shape:()=>t})}required(e){let t={};return k.objectKeys(this.shape).forEach(n=>{if(e&&!e[n])t[n]=this.shape[n];else{let o=this.shape[n];for(;o instanceof U;)o=o._def.innerType;t[n]=o}}),new r({...this._def,shape:()=>t})}keyof(){return Vr(k.objectKeys(this.shape))}};D.create=(r,e)=>new D({shape:()=>r,unknownKeys:"strip",catchall:q.create(),typeName:y.ZodObject,...b(e)});D.strictCreate=(r,e)=>new D({shape:()=>r,unknownKeys:"strict",catchall:q.create(),typeName:y.ZodObject,...b(e)});D.lazycreate=(r,e)=>new D({shape:r,unknownKeys:"strip",catchall:q.create(),typeName:y.ZodObject,...b(e)});var _e=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n=this._def.options;function i(o){for(let s of o)if(s.result.status==="valid")return s.result;for(let s of o)if(s.result.status==="dirty")return t.common.issues.push(...s.ctx.common.issues),s.result;let a=o.map(s=>new M(s.ctx.common.issues));return p(t,{code:f.invalid_union,unionErrors:a}),_}if(t.common.async)return Promise.all(n.map(async o=>{let a={...t,common:{...t.common,issues:[]},parent:null};return{result:await o._parseAsync({data:t.data,path:t.path,parent:a}),ctx:a}})).then(i);{let o,a=[];for(let u of n){let l={...t,common:{...t.common,issues:[]},parent:null},d=u._parseSync({data:t.data,path:t.path,parent:l});if(d.status==="valid")return d;d.status==="dirty"&&!o&&(o={result:d,ctx:l}),l.common.issues.length&&a.push(l.common.issues)}if(o)return t.common.issues.push(...o.ctx.common.issues),o.result;let s=a.map(u=>new M(u));return p(t,{code:f.invalid_union,unionErrors:s}),_}}get options(){return this._def.options}};_e.create=(r,e)=>new _e({options:r,typeName:y.ZodUnion,...b(e)});var X=r=>r instanceof xe?X(r.schema):r instanceof Z?X(r.innerType()):r instanceof we?[r.value]:r instanceof Se?r.options:r instanceof ke?k.objectValues(r.enum):r instanceof Te?X(r._def.innerType):r instanceof ye?[void 0]:r instanceof ge?[null]:r instanceof U?[void 0,...X(r.unwrap())]:r instanceof Y?[null,...X(r.unwrap())]:r instanceof Ye||r instanceof Ee?X(r.unwrap()):r instanceof Oe?X(r._def.innerType):[],Tt=class r extends x{_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==h.object)return p(t,{code:f.invalid_type,expected:h.object,received:t.parsedType}),_;let n=this.discriminator,i=t.data[n],o=this.optionsMap.get(i);return o?t.common.async?o._parseAsync({data:t.data,path:t.path,parent:t}):o._parseSync({data:t.data,path:t.path,parent:t}):(p(t,{code:f.invalid_union_discriminator,options:Array.from(this.optionsMap.keys()),path:[n]}),_)}get discriminator(){return this._def.discriminator}get options(){return this._def.options}get optionsMap(){return this._def.optionsMap}static create(e,t,n){let i=new Map;for(let o of t){let a=X(o.shape[e]);if(!a.length)throw new Error(`A discriminator value for key \`${e}\` could not be extracted from all schema options`);for(let s of a){if(i.has(s))throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(s)}`);i.set(s,o)}}return new r({typeName:y.ZodDiscriminatedUnion,discriminator:e,options:t,optionsMap:i,...b(n)})}};function Yt(r,e){let t=Q(r),n=Q(e);if(r===e)return{valid:!0,data:r};if(t===h.object&&n===h.object){let i=k.objectKeys(e),o=k.objectKeys(r).filter(s=>i.indexOf(s)!==-1),a={...r,...e};for(let s of o){let u=Yt(r[s],e[s]);if(!u.valid)return{valid:!1};a[s]=u.data}return{valid:!0,data:a}}else if(t===h.array&&n===h.array){if(r.length!==e.length)return{valid:!1};let i=[];for(let o=0;o{if(Ht(o)||Ht(a))return _;let s=Yt(o.value,a.value);return s.valid?((Gt(o)||Gt(a))&&t.dirty(),{status:t.value,value:s.data}):(p(n,{code:f.invalid_intersection_types}),_)};return n.common.async?Promise.all([this._def.left._parseAsync({data:n.data,path:n.path,parent:n}),this._def.right._parseAsync({data:n.data,path:n.path,parent:n})]).then(([o,a])=>i(o,a)):i(this._def.left._parseSync({data:n.data,path:n.path,parent:n}),this._def.right._parseSync({data:n.data,path:n.path,parent:n}))}};be.create=(r,e,t)=>new be({left:r,right:e,typeName:y.ZodIntersection,...b(t)});var G=class r extends x{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.array)return p(n,{code:f.invalid_type,expected:h.array,received:n.parsedType}),_;if(n.data.lengththis._def.items.length&&(p(n,{code:f.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:"array"}),t.dirty());let o=[...n.data].map((a,s)=>{let u=this._def.items[s]||this._def.rest;return u?u._parse(new $(n,a,n.path,s)):null}).filter(a=>!!a);return n.common.async?Promise.all(o).then(a=>P.mergeArray(t,a)):P.mergeArray(t,o)}get items(){return this._def.items}rest(e){return new r({...this._def,rest:e})}};G.create=(r,e)=>{if(!Array.isArray(r))throw new Error("You must pass an array of schemas to z.tuple([ ... ])");return new G({items:r,typeName:y.ZodTuple,rest:null,...b(e)})};var Ot=class r extends x{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.object)return p(n,{code:f.invalid_type,expected:h.object,received:n.parsedType}),_;let i=[],o=this._def.keyType,a=this._def.valueType;for(let s in n.data)i.push({key:o._parse(new $(n,s,n.path,s)),value:a._parse(new $(n,n.data[s],n.path,s)),alwaysSet:s in n.data});return n.common.async?P.mergeObjectAsync(t,i):P.mergeObjectSync(t,i)}get element(){return this._def.valueType}static create(e,t,n){return t instanceof x?new r({keyType:e,valueType:t,typeName:y.ZodRecord,...b(n)}):new r({keyType:ne.create(),valueType:e,typeName:y.ZodRecord,...b(t)})}},Me=class extends x{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.map)return p(n,{code:f.invalid_type,expected:h.map,received:n.parsedType}),_;let i=this._def.keyType,o=this._def.valueType,a=[...n.data.entries()].map(([s,u],l)=>({key:i._parse(new $(n,s,n.path,[l,"key"])),value:o._parse(new $(n,u,n.path,[l,"value"]))}));if(n.common.async){let s=new Map;return Promise.resolve().then(async()=>{for(let u of a){let l=await u.key,d=await u.value;if(l.status==="aborted"||d.status==="aborted")return _;(l.status==="dirty"||d.status==="dirty")&&t.dirty(),s.set(l.value,d.value)}return{status:t.value,value:s}})}else{let s=new Map;for(let u of a){let l=u.key,d=u.value;if(l.status==="aborted"||d.status==="aborted")return _;(l.status==="dirty"||d.status==="dirty")&&t.dirty(),s.set(l.value,d.value)}return{status:t.value,value:s}}}};Me.create=(r,e,t)=>new Me({valueType:e,keyType:r,typeName:y.ZodMap,...b(t)});var Ze=class r extends x{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.set)return p(n,{code:f.invalid_type,expected:h.set,received:n.parsedType}),_;let i=this._def;i.minSize!==null&&n.data.sizei.maxSize.value&&(p(n,{code:f.too_big,maximum:i.maxSize.value,type:"set",inclusive:!0,exact:!1,message:i.maxSize.message}),t.dirty());let o=this._def.valueType;function a(u){let l=new Set;for(let d of u){if(d.status==="aborted")return _;d.status==="dirty"&&t.dirty(),l.add(d.value)}return{status:t.value,value:l}}let s=[...n.data.values()].map((u,l)=>o._parse(new $(n,u,n.path,l)));return n.common.async?Promise.all(s).then(u=>a(u)):a(s)}min(e,t){return new r({...this._def,minSize:{value:e,message:m.toString(t)}})}max(e,t){return new r({...this._def,maxSize:{value:e,message:m.toString(t)}})}size(e,t){return this.min(e,t).max(e,t)}nonempty(e){return this.min(1,e)}};Ze.create=(r,e)=>new Ze({valueType:r,minSize:null,maxSize:null,typeName:y.ZodSet,...b(e)});var Et=class r extends x{constructor(){super(...arguments),this.validate=this.implement}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==h.function)return p(t,{code:f.invalid_type,expected:h.function,received:t.parsedType}),_;function n(s,u){return St({data:s,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,wt(),Ne].filter(l=>!!l),issueData:{code:f.invalid_arguments,argumentsError:u}})}function i(s,u){return St({data:s,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,wt(),Ne].filter(l=>!!l),issueData:{code:f.invalid_return_type,returnTypeError:u}})}let o={errorMap:t.common.contextualErrorMap},a=t.data;if(this._def.returns instanceof oe){let s=this;return N(async function(...u){let l=new M([]),d=await s._def.args.parseAsync(u,o).catch(g=>{throw l.addIssue(n(u,g)),l}),v=await Reflect.apply(a,this,d);return await s._def.returns._def.type.parseAsync(v,o).catch(g=>{throw l.addIssue(i(v,g)),l})})}else{let s=this;return N(function(...u){let l=s._def.args.safeParse(u,o);if(!l.success)throw new M([n(u,l.error)]);let d=Reflect.apply(a,this,l.data),v=s._def.returns.safeParse(d,o);if(!v.success)throw new M([i(d,v.error)]);return v.data})}}parameters(){return this._def.args}returnType(){return this._def.returns}args(...e){return new r({...this._def,args:G.create(e).rest(ee.create())})}returns(e){return new r({...this._def,returns:e})}implement(e){return this.parse(e)}strictImplement(e){return this.parse(e)}static create(e,t,n){return new r({args:e||G.create([]).rest(ee.create()),returns:t||ee.create(),typeName:y.ZodFunction,...b(n)})}},xe=class extends x{get schema(){return this._def.getter()}_parse(e){let{ctx:t}=this._processInputParams(e);return this._def.getter()._parse({data:t.data,path:t.path,parent:t})}};xe.create=(r,e)=>new xe({getter:r,typeName:y.ZodLazy,...b(e)});var we=class extends x{_parse(e){if(e.data!==this._def.value){let t=this._getOrReturnCtx(e);return p(t,{received:t.data,code:f.invalid_literal,expected:this._def.value}),_}return{status:"valid",value:e.data}}get value(){return this._def.value}};we.create=(r,e)=>new we({value:r,typeName:y.ZodLiteral,...b(e)});function Vr(r,e){return new Se({values:r,typeName:y.ZodEnum,...b(e)})}var Se=class r extends x{constructor(){super(...arguments),qe.set(this,void 0)}_parse(e){if(typeof e.data!="string"){let t=this._getOrReturnCtx(e),n=this._def.values;return p(t,{expected:k.joinValues(n),received:t.parsedType,code:f.invalid_type}),_}if(kt(this,qe,"f")||Fr(this,qe,new Set(this._def.values),"f"),!kt(this,qe,"f").has(e.data)){let t=this._getOrReturnCtx(e),n=this._def.values;return p(t,{received:t.data,code:f.invalid_enum_value,options:n}),_}return N(e.data)}get options(){return this._def.values}get enum(){let e={};for(let t of this._def.values)e[t]=t;return e}get Values(){let e={};for(let t of this._def.values)e[t]=t;return e}get Enum(){let e={};for(let t of this._def.values)e[t]=t;return e}extract(e,t=this._def){return r.create(e,{...this._def,...t})}exclude(e,t=this._def){return r.create(this.options.filter(n=>!e.includes(n)),{...this._def,...t})}};qe=new WeakMap;Se.create=Vr;var ke=class extends x{constructor(){super(...arguments),He.set(this,void 0)}_parse(e){let t=k.getValidEnumValues(this._def.values),n=this._getOrReturnCtx(e);if(n.parsedType!==h.string&&n.parsedType!==h.number){let i=k.objectValues(t);return p(n,{expected:k.joinValues(i),received:n.parsedType,code:f.invalid_type}),_}if(kt(this,He,"f")||Fr(this,He,new Set(k.getValidEnumValues(this._def.values)),"f"),!kt(this,He,"f").has(e.data)){let i=k.objectValues(t);return p(n,{received:n.data,code:f.invalid_enum_value,options:i}),_}return N(e.data)}get enum(){return this._def.values}};He=new WeakMap;ke.create=(r,e)=>new ke({values:r,typeName:y.ZodNativeEnum,...b(e)});var oe=class extends x{unwrap(){return this._def.type}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==h.promise&&t.common.async===!1)return p(t,{code:f.invalid_type,expected:h.promise,received:t.parsedType}),_;let n=t.parsedType===h.promise?t.data:Promise.resolve(t.data);return N(n.then(i=>this._def.type.parseAsync(i,{path:t.path,errorMap:t.common.contextualErrorMap})))}};oe.create=(r,e)=>new oe({type:r,typeName:y.ZodPromise,...b(e)});var Z=class extends x{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName===y.ZodEffects?this._def.schema.sourceType():this._def.schema}_parse(e){let{status:t,ctx:n}=this._processInputParams(e),i=this._def.effect||null,o={addIssue:a=>{p(n,a),a.fatal?t.abort():t.dirty()},get path(){return n.path}};if(o.addIssue=o.addIssue.bind(o),i.type==="preprocess"){let a=i.transform(n.data,o);if(n.common.async)return Promise.resolve(a).then(async s=>{if(t.value==="aborted")return _;let u=await this._def.schema._parseAsync({data:s,path:n.path,parent:n});return u.status==="aborted"?_:u.status==="dirty"||t.value==="dirty"?Pe(u.value):u});{if(t.value==="aborted")return _;let s=this._def.schema._parseSync({data:a,path:n.path,parent:n});return s.status==="aborted"?_:s.status==="dirty"||t.value==="dirty"?Pe(s.value):s}}if(i.type==="refinement"){let a=s=>{let u=i.refinement(s,o);if(n.common.async)return Promise.resolve(u);if(u instanceof Promise)throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");return s};if(n.common.async===!1){let s=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});return s.status==="aborted"?_:(s.status==="dirty"&&t.dirty(),a(s.value),{status:t.value,value:s.value})}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(s=>s.status==="aborted"?_:(s.status==="dirty"&&t.dirty(),a(s.value).then(()=>({status:t.value,value:s.value}))))}if(i.type==="transform")if(n.common.async===!1){let a=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});if(!fe(a))return a;let s=i.transform(a.value,o);if(s instanceof Promise)throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.");return{status:t.value,value:s}}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(a=>fe(a)?Promise.resolve(i.transform(a.value,o)).then(s=>({status:t.value,value:s})):a);k.assertNever(i)}};Z.create=(r,e,t)=>new Z({schema:r,typeName:y.ZodEffects,effect:e,...b(t)});Z.createWithPreprocess=(r,e,t)=>new Z({schema:e,effect:{type:"preprocess",transform:r},typeName:y.ZodEffects,...b(t)});var U=class extends x{_parse(e){return this._getType(e)===h.undefined?N(void 0):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};U.create=(r,e)=>new U({innerType:r,typeName:y.ZodOptional,...b(e)});var Y=class extends x{_parse(e){return this._getType(e)===h.null?N(null):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};Y.create=(r,e)=>new Y({innerType:r,typeName:y.ZodNullable,...b(e)});var Te=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return t.parsedType===h.undefined&&(n=this._def.defaultValue()),this._def.innerType._parse({data:n,path:t.path,parent:t})}removeDefault(){return this._def.innerType}};Te.create=(r,e)=>new Te({innerType:r,typeName:y.ZodDefault,defaultValue:typeof e.default=="function"?e.default:()=>e.default,...b(e)});var Oe=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n={...t,common:{...t.common,issues:[]}},i=this._def.innerType._parse({data:n.data,path:n.path,parent:{...n}});return Ge(i)?i.then(o=>({status:"valid",value:o.status==="valid"?o.value:this._def.catchValue({get error(){return new M(n.common.issues)},input:n.data})})):{status:"valid",value:i.status==="valid"?i.value:this._def.catchValue({get error(){return new M(n.common.issues)},input:n.data})}}removeCatch(){return this._def.innerType}};Oe.create=(r,e)=>new Oe({innerType:r,typeName:y.ZodCatch,catchValue:typeof e.catch=="function"?e.catch:()=>e.catch,...b(e)});var Fe=class extends x{_parse(e){if(this._getType(e)!==h.nan){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.nan,received:n.parsedType}),_}return{status:"valid",value:e.data}}};Fe.create=r=>new Fe({typeName:y.ZodNaN,...b(r)});var ai=Symbol("zod_brand"),Ye=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return this._def.type._parse({data:n,path:t.path,parent:t})}unwrap(){return this._def.type}},Je=class r extends x{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.common.async)return(async()=>{let o=await this._def.in._parseAsync({data:n.data,path:n.path,parent:n});return o.status==="aborted"?_:o.status==="dirty"?(t.dirty(),Pe(o.value)):this._def.out._parseAsync({data:o.value,path:n.path,parent:n})})();{let i=this._def.in._parseSync({data:n.data,path:n.path,parent:n});return i.status==="aborted"?_:i.status==="dirty"?(t.dirty(),{status:"dirty",value:i.value}):this._def.out._parseSync({data:i.value,path:n.path,parent:n})}}static create(e,t){return new r({in:e,out:t,typeName:y.ZodPipeline})}},Ee=class extends x{_parse(e){let t=this._def.innerType._parse(e),n=i=>(fe(i)&&(i.value=Object.freeze(i.value)),i);return Ge(t)?t.then(i=>n(i)):n(t)}unwrap(){return this._def.innerType}};Ee.create=(r,e)=>new Ee({innerType:r,typeName:y.ZodReadonly,...b(e)});function Lr(r,e){let t=typeof r=="function"?r(e):typeof r=="string"?{message:r}:r;return typeof t=="string"?{message:t}:t}function zr(r,e={},t){return r?ie.create().superRefine((n,i)=>{var o,a;let s=r(n);if(s instanceof Promise)return s.then(u=>{var l,d;if(!u){let v=Lr(e,n),T=(d=(l=v.fatal)!==null&&l!==void 0?l:t)!==null&&d!==void 0?d:!0;i.addIssue({code:"custom",...v,fatal:T})}});if(!s){let u=Lr(e,n),l=(a=(o=u.fatal)!==null&&o!==void 0?o:t)!==null&&a!==void 0?a:!0;i.addIssue({code:"custom",...u,fatal:l})}}):ie.create()}var si={object:D.lazycreate},y;(function(r){r.ZodString="ZodString",r.ZodNumber="ZodNumber",r.ZodNaN="ZodNaN",r.ZodBigInt="ZodBigInt",r.ZodBoolean="ZodBoolean",r.ZodDate="ZodDate",r.ZodSymbol="ZodSymbol",r.ZodUndefined="ZodUndefined",r.ZodNull="ZodNull",r.ZodAny="ZodAny",r.ZodUnknown="ZodUnknown",r.ZodNever="ZodNever",r.ZodVoid="ZodVoid",r.ZodArray="ZodArray",r.ZodObject="ZodObject",r.ZodUnion="ZodUnion",r.ZodDiscriminatedUnion="ZodDiscriminatedUnion",r.ZodIntersection="ZodIntersection",r.ZodTuple="ZodTuple",r.ZodRecord="ZodRecord",r.ZodMap="ZodMap",r.ZodSet="ZodSet",r.ZodFunction="ZodFunction",r.ZodLazy="ZodLazy",r.ZodLiteral="ZodLiteral",r.ZodEnum="ZodEnum",r.ZodEffects="ZodEffects",r.ZodNativeEnum="ZodNativeEnum",r.ZodOptional="ZodOptional",r.ZodNullable="ZodNullable",r.ZodDefault="ZodDefault",r.ZodCatch="ZodCatch",r.ZodPromise="ZodPromise",r.ZodBranded="ZodBranded",r.ZodPipeline="ZodPipeline",r.ZodReadonly="ZodReadonly"})(y||(y={}));var ci=(r,e={message:`Input not instance of ${r.name}`})=>zr(t=>t instanceof r,e),Br=ne.create,qr=pe.create,ui=Fe.create,li=he.create,Hr=me.create,di=ve.create,fi=De.create,pi=ye.create,hi=ge.create,mi=ie.create,vi=ee.create,yi=q.create,gi=Le.create,_i=te.create,bi=D.create,xi=D.strictCreate,wi=_e.create,Si=Tt.create,ki=be.create,Ti=G.create,Oi=Ot.create,Ei=Me.create,Ci=Ze.create,ji=Et.create,Ii=xe.create,Ai=we.create,Ri=Se.create,Pi=ke.create,Ni=oe.create,Mr=Z.create,Di=U.create,Li=Y.create,Mi=Z.createWithPreprocess,Zi=Je.create,Fi=()=>Br().optional(),Ui=()=>qr().optional(),$i=()=>Hr().optional(),Wi={string:r=>ne.create({...r,coerce:!0}),number:r=>pe.create({...r,coerce:!0}),boolean:r=>me.create({...r,coerce:!0}),bigint:r=>he.create({...r,coerce:!0}),date:r=>ve.create({...r,coerce:!0})},Vi=_,c=Object.freeze({__proto__:null,defaultErrorMap:Ne,setErrorMap:Mn,getErrorMap:wt,makeIssue:St,EMPTY_PATH:Zn,addIssueToContext:p,ParseStatus:P,INVALID:_,DIRTY:Pe,OK:N,isAborted:Ht,isDirty:Gt,isValid:fe,isAsync:Ge,get util(){return k},get objectUtil(){return qt},ZodParsedType:h,getParsedType:Q,ZodType:x,datetimeRegex:Wr,ZodString:ne,ZodNumber:pe,ZodBigInt:he,ZodBoolean:me,ZodDate:ve,ZodSymbol:De,ZodUndefined:ye,ZodNull:ge,ZodAny:ie,ZodUnknown:ee,ZodNever:q,ZodVoid:Le,ZodArray:te,ZodObject:D,ZodUnion:_e,ZodDiscriminatedUnion:Tt,ZodIntersection:be,ZodTuple:G,ZodRecord:Ot,ZodMap:Me,ZodSet:Ze,ZodFunction:Et,ZodLazy:xe,ZodLiteral:we,ZodEnum:Se,ZodNativeEnum:ke,ZodPromise:oe,ZodEffects:Z,ZodTransformer:Z,ZodOptional:U,ZodNullable:Y,ZodDefault:Te,ZodCatch:Oe,ZodNaN:Fe,BRAND:ai,ZodBranded:Ye,ZodPipeline:Je,ZodReadonly:Ee,custom:zr,Schema:x,ZodSchema:x,late:si,get ZodFirstPartyTypeKind(){return y},coerce:Wi,any:mi,array:_i,bigint:li,boolean:Hr,date:di,discriminatedUnion:Si,effect:Mr,enum:Ri,function:ji,instanceof:ci,intersection:ki,lazy:Ii,literal:Ai,map:Ei,nan:ui,nativeEnum:Pi,never:yi,null:hi,nullable:Li,number:qr,object:bi,oboolean:$i,onumber:Ui,optional:Di,ostring:Fi,pipeline:Zi,preprocess:Mi,promise:Ni,record:Oi,set:Ci,strictObject:xi,string:Br,symbol:fi,transformer:Mr,tuple:Ti,undefined:pi,union:wi,unknown:vi,void:gi,NEVER:Vi,ZodIssueCode:f,quotelessJson:Ln,ZodError:M});var Jr=(r=>(r.Info="info",r.Debug="debug",r.Trace="trace",r.Error="error",r))(Jr||{}),Kr=(r=>(r.Changed="Changed",r.Added="Added",r.Removed="Removed",r))(Kr||{}),Xr=(r=>(r.External="BSLIVE_EXTERNAL",r))(Xr||{}),zi=c.nativeEnum(Jr),Gr=c.object({log_level:zi}),Bi=c.object({ws_path:c.string(),host:c.string().optional()}),qi=c.object({kind:c.string(),ms:c.string()}),Yr=c.object({path:c.string()}),Hi=c.object({paths:c.array(c.string())}),Qr=c.discriminatedUnion("kind",[c.object({kind:c.literal("Both"),payload:c.object({name:c.string(),bind_address:c.string()})}),c.object({kind:c.literal("Address"),payload:c.object({bind_address:c.string()})}),c.object({kind:c.literal("Named"),payload:c.object({name:c.string()})}),c.object({kind:c.literal("Port"),payload:c.object({port:c.number()})}),c.object({kind:c.literal("PortNamed"),payload:c.object({port:c.number(),name:c.string()})})]),Gi=c.object({id:c.string(),identity:Qr,socket_addr:c.string()}),en=c.object({servers:c.array(Gi)}),tn=c.object({connect:Bi,ctx_message:c.string()}),Yi=c.object({path:c.string()}),Ji=c.discriminatedUnion("kind",[c.object({kind:c.literal("Html"),payload:c.object({html:c.string()})}),c.object({kind:c.literal("Json"),payload:c.object({json_str:c.string()})}),c.object({kind:c.literal("Raw"),payload:c.object({raw:c.string()})}),c.object({kind:c.literal("Sse"),payload:c.object({sse:c.string()})}),c.object({kind:c.literal("Proxy"),payload:c.object({proxy:c.string()})}),c.object({kind:c.literal("Dir"),payload:c.object({dir:c.string(),base:c.string().optional()})})]),Ki=c.object({path:c.string(),kind:Ji}),Xi=c.discriminatedUnion("kind",[c.object({kind:c.literal("Stopped"),payload:c.object({bind_address:c.string()})}),c.object({kind:c.literal("Started"),payload:c.undefined().optional()}),c.object({kind:c.literal("Patched"),payload:c.undefined().optional()}),c.object({kind:c.literal("Errored"),payload:c.object({error:c.string()})})]),Qi=c.object({identity:Qr,change:Xi}),Hu=c.object({items:c.array(Qi)}),Gu=c.object({routes:c.array(Ki),id:c.string()}),eo=c.object({servers_resp:en}),to=c.object({line:c.string(),prefix:c.string().optional()}),ro=c.object({line:c.string(),prefix:c.string().optional()}),no=c.object({paths:c.array(c.string())}),io=c.object({paths:c.array(c.string()),debounce:qi}),oo=c.nativeEnum(Kr),Ct=c.lazy(()=>c.discriminatedUnion("kind",[c.object({kind:c.literal("Fs"),payload:c.object({path:c.string(),change_kind:oo})}),c.object({kind:c.literal("FsMany"),payload:c.array(Ct)})])),Yu=c.discriminatedUnion("kind",[c.object({kind:c.literal("Change"),payload:Ct}),c.object({kind:c.literal("WsConnection"),payload:Gr}),c.object({kind:c.literal("Config"),payload:Gr})]),Ju=c.nativeEnum(Xr),ao=c.discriminatedUnion("kind",[c.object({kind:c.literal("Stdout"),payload:ro}),c.object({kind:c.literal("Stderr"),payload:to})]),Ku=c.discriminatedUnion("kind",[c.object({kind:c.literal("MissingInputs"),payload:c.string()}),c.object({kind:c.literal("InvalidInput"),payload:c.string()}),c.object({kind:c.literal("NotFound"),payload:c.string()}),c.object({kind:c.literal("InputWriteError"),payload:c.string()}),c.object({kind:c.literal("PathError"),payload:c.string()}),c.object({kind:c.literal("PortError"),payload:c.string()}),c.object({kind:c.literal("DirError"),payload:c.string()}),c.object({kind:c.literal("YamlError"),payload:c.string()}),c.object({kind:c.literal("MarkdownError"),payload:c.string()}),c.object({kind:c.literal("HtmlError"),payload:c.string()}),c.object({kind:c.literal("Io"),payload:c.string()}),c.object({kind:c.literal("UnsupportedExtension"),payload:c.string()}),c.object({kind:c.literal("MissingExtension"),payload:c.string()}),c.object({kind:c.literal("EmptyInput"),payload:c.string()}),c.object({kind:c.literal("BsLiveRules"),payload:c.string()})]),Xu=c.discriminatedUnion("kind",[c.object({kind:c.literal("ServersChanged"),payload:en}),c.object({kind:c.literal("TaskReport"),payload:c.object({id:c.string()})})]),Qu=c.discriminatedUnion("kind",[c.object({kind:c.literal("Started"),payload:c.undefined().optional()}),c.object({kind:c.literal("FailedStartup"),payload:c.string()})]),el=c.discriminatedUnion("kind",[c.object({kind:c.literal("ServersChanged"),payload:eo}),c.object({kind:c.literal("Watching"),payload:io}),c.object({kind:c.literal("WatchingStopped"),payload:no}),c.object({kind:c.literal("FileChanged"),payload:Yr}),c.object({kind:c.literal("FilesChanged"),payload:Hi}),c.object({kind:c.literal("InputFileChanged"),payload:Yr}),c.object({kind:c.literal("InputAccepted"),payload:Yi}),c.object({kind:c.literal("OutputLine"),payload:ao})]);var rn=[{selector:"background",styleNames:["backgroundImage"]},{selector:"border",styleNames:["borderImage","webkitBorderImage","MozBorderImage"]}],jt={stylesheetReloadTimeout:15e3},so=/\.(jpe?g|png|gif|svg)$/i,It=class{constructor(e,t,n){this.window=e,this.console=t,this.Timer=n,this.document=this.window.document,this.importCacheWaitPeriod=200,this.plugins=[]}addPlugin(e){return this.plugins.push(e)}analyze(e){}reload(e,t={}){if(this.options={...jt,...t},!(t.liveCSS&&e.match(/\.css(?:\.map)?$/i)&&this.reloadStylesheet(e))){if(t.liveImg&&e.match(so)){this.reloadImages(e);return}if(t.isChromeExtension){this.reloadChromeExtension();return}return this.reloadPage()}}reloadPage(){return this.window.document.location.reload()}reloadChromeExtension(){return this.window.chrome.runtime.reload()}reloadImages(e){let t,n=this.generateUniqueString();for(t of Array.from(this.document.images))nn(e,Jt(t.src))&&(t.src=this.generateCacheBustUrl(t.src,n));if(this.document.querySelectorAll)for(let{selector:i,styleNames:o}of rn)for(t of Array.from(this.document.querySelectorAll(`[style*=${i}]`)))this.reloadStyleImages(t.style,o,e,n);if(this.document.styleSheets)return Array.from(this.document.styleSheets).map(i=>this.reloadStylesheetImages(i,e,n))}reloadStylesheetImages(e,t,n){let i;try{i=(e||{}).cssRules}catch{}if(i)for(let o of Array.from(i))switch(o.type){case CSSRule.IMPORT_RULE:this.reloadStylesheetImages(o.styleSheet,t,n);break;case CSSRule.STYLE_RULE:for(let{styleNames:a}of rn)this.reloadStyleImages(o.style,a,t,n);break;case CSSRule.MEDIA_RULE:this.reloadStylesheetImages(o,t,n);break}}reloadStyleImages(e,t,n,i){for(let o of t){let a=e[o];if(typeof a=="string"){let s=a.replace(new RegExp("\\burl\\s*\\(([^)]*)\\)"),(u,l)=>nn(n,Jt(l))?`url(${this.generateCacheBustUrl(l,i)})`:u);s!==a&&(e[o]=s)}}}reloadStylesheet(e){let t=this.options||jt,n,i,o=(()=>{let u=[];for(i of Array.from(this.document.getElementsByTagName("link")))i.rel.match(/^stylesheet$/i)&&!i.__LiveReload_pendingRemoval&&u.push(i);return u})(),a=[];for(n of Array.from(this.document.getElementsByTagName("style")))n.sheet&&this.collectImportedStylesheets(n,n.sheet,a);for(i of Array.from(o))this.collectImportedStylesheets(i,i.sheet,a);if(this.window.StyleFix&&this.document.querySelectorAll)for(n of Array.from(this.document.querySelectorAll("style[data-href]")))o.push(n);this.console.debug(`found ${o.length} LINKed stylesheets, ${a.length} @imported stylesheets`);let s=co(e,o.concat(a),u=>Jt(this.linkHref(u)));if(s)s.object.rule?(this.console.debug(`is reloading imported stylesheet: ${s.object.href}`),this.reattachImportedRule(s.object)):(this.console.debug(`is reloading stylesheet: ${this.linkHref(s.object)}`),this.reattachStylesheetLink(s.object));else if(t.reloadMissingCSS){this.console.debug(`will reload all stylesheets because path '${e}' did not match any specific one. To disable this behavior, set 'options.reloadMissingCSS' to 'false'.`);for(i of Array.from(o))this.reattachStylesheetLink(i)}else this.console.debug(`will not reload path '${e}' because the stylesheet was not found on the page and 'options.reloadMissingCSS' was set to 'false'.`);return!0}collectImportedStylesheets(e,t,n){let i;try{i=(t||{}).cssRules}catch{}if(i&&i.length)for(let o=0;o{if(!i)return i=!0,t()};if(e.onload=()=>(this.console.debug("the new stylesheet has finished loading"),this.knownToSupportCssOnLoad=!0,o()),!this.knownToSupportCssOnLoad){let a;(a=()=>e.sheet?(this.console.debug("is polling until the new CSS finishes loading..."),o()):this.Timer.start(50,a))()}return this.Timer.start(n.stylesheetReloadTimeout,o)}linkHref(e){return e.href||e.getAttribute&&e.getAttribute("data-href")}reattachStylesheetLink(e){let t;if(e.__LiveReload_pendingRemoval)return;e.__LiveReload_pendingRemoval=!0,e.tagName==="STYLE"?(t=this.document.createElement("link"),t.rel="stylesheet",t.media=e.media,t.disabled=e.disabled):t=e.cloneNode(!1),t.href=this.generateCacheBustUrl(this.linkHref(e));let n=e.parentNode;return n.lastChild===e?n.appendChild(t):n.insertBefore(t,e.nextSibling),this.waitUntilCssLoads(t,()=>{let i;return/AppleWebKit/.test(this.window.navigator.userAgent)?i=5:i=200,this.Timer.start(i,()=>{if(e.parentNode)return e.parentNode.removeChild(e),t.onreadystatechange=null,this.window.StyleFix?this.window.StyleFix.link(t):void 0})})}reattachImportedRule({rule:e,index:t,link:n}){let i=e.parentStyleSheet,o=this.generateCacheBustUrl(e.href),a=e.media.length?[].join.call(e.media,", "):"",s=`@import url("${o}") ${a};`;e.__LiveReload_newHref=o;let u=this.document.createElement("link");return u.rel="stylesheet",u.href=o,u.__LiveReload_pendingRemoval=!0,n.parentNode&&n.parentNode.insertBefore(u,n),this.Timer.start(this.importCacheWaitPeriod,()=>{if(u.parentNode&&u.parentNode.removeChild(u),e.__LiveReload_newHref===o)return i.insertRule(s,t),i.deleteRule(t+1),e=i.cssRules[t],e.__LiveReload_newHref=o,this.Timer.start(this.importCacheWaitPeriod,()=>{if(e.__LiveReload_newHref===o)return i.insertRule(s,t),i.deleteRule(t+1)})})}generateUniqueString(){return`livereload=${Date.now()}`}generateCacheBustUrl(e,t){let n=this.options||jt,i,o;if(t||(t=this.generateUniqueString()),{url:e,hash:i,params:o}=on(e),n.overrideURL&&e.indexOf(n.serverURL)<0){let s=e;e=n.serverURL+n.overrideURL+"?url="+encodeURIComponent(e),this.console.debug(`is overriding source URL ${s} with ${e}`)}let a=o.replace(/(\?|&)livereload=(\d+)/,(s,u)=>`${u}${t}`);return a===o&&(o.length===0?a=`?${t}`:a=`${o}&${t}`),e+a+i}};function on(r){let e="",t="",n=r.indexOf("#");n>=0&&(e=r.slice(n),r=r.slice(0,n));let i=r.indexOf("??");return i>=0?i+1!==r.lastIndexOf("?")&&(n=r.lastIndexOf("?")):n=r.indexOf("?"),n>=0&&(t=r.slice(n),r=r.slice(0,n)),{url:r,params:t,hash:e}}function Jt(r){if(!r)return"";let e;return{url:r}=on(r),r.indexOf("file://")===0?e=r.replace(new RegExp("^file://(localhost)?"),""):e=r.replace(new RegExp("^([^:]+:)?//([^:/]+)(:\\d*)?/"),"/"),decodeURIComponent(e)}function an(r,e){if(r=r.replace(/^\/+/,"").toLowerCase(),e=e.replace(/^\/+/,"").toLowerCase(),r===e)return 1e4;let t=r.split(/\/|\\/).reverse(),n=e.split(/\/|\\/).reverse(),i=Math.min(t.length,n.length),o=0;for(;on){let n,i={score:0};for(let o of e)n=an(r,t(o)),n>i.score&&(i={object:o,score:n});return i.score===0?null:i}function nn(r,e){return an(r,e)>0}var un=yn(cn());var uo=/\.(jpe?g|png|gif|svg)$/i;function Kt(r,e,t){switch(r.kind){case"FsMany":{if(r.payload.some(i=>{switch(i.kind){case"Fs":return!(i.payload.path.match(/\.css(?:\.map)?$/i)||i.payload.path.match(uo));case"FsMany":throw new Error("unreachable")}}))return window.__playwright?.record?window.__playwright?.record({kind:"reloadPage"}):t.reloadPage();for(let i of r.payload)Kt(i,e,t);break}case"Fs":{let n=r.payload.path,i={liveCSS:!0,liveImg:!0,reloadMissingCSS:!0,originalPath:"",overrideURL:"",serverURL:""};window.__playwright?.record?window.__playwright?.record({kind:"reload",args:{path:n,opts:i}}):(e.trace("will reload a file with path ",n),t.reload(n,i))}}}var Xt={name:"dom plugin",globalSetup:(r,e)=>{let t=new It(window,e,un.Timer);return[r,[e,t]]},resetSink(r,e,t){let[n,i]=e;return r.pipe(de(o=>o.kind==="Change"),K(o=>o.payload),Be(o=>{n.trace("incoming message",JSON.stringify({change:o,config:t},null,2));let a=Ct.parse(o);Kt(a,n,i)}),ze())}};(r=>{tn.parse(r);let t=Pr().create(r.connect),[n,i]=zt.globalSetup(t,Nr),[o,a]=Xt.globalSetup(t,i),s=t.pipe(de(d=>d.kind==="WsConnection"),K(d=>d.payload),$t()),u=t.pipe(de(d=>d.kind==="Config"),K(d=>d.payload)),l=t.pipe(de(d=>d.kind==="Change"),K(d=>d.payload));xt(u,s).pipe(Wt(d=>{let v=[Xt.resetSink(o,a,d),zt.resetSink(n,i,d)];return xt(...v)})).subscribe(),s.subscribe(d=>{i.info("\u{1F7E2} Browsersync Live connected",{config:d})})})(window.$BSLIVE_INJECT_CONFIG$); + `):"",this.name="UnsubscriptionError",this.errors=t}});function se(r,e){if(r){var t=r.indexOf(e);0<=t&&r.splice(t,1)}}var H=function(){function r(e){this.initialTeardown=e,this.closed=!1,this._parentage=null,this._finalizers=null}return r.prototype.unsubscribe=function(){var e,t,n,i,o;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=J(a),u=s.next();!u.done;u=s.next()){var l=u.value;l.remove(this)}}catch(w){e={error:w}}finally{try{u&&!u.done&&(t=s.return)&&t.call(s)}finally{if(e)throw e.error}}else a.remove(this);var d=this.initialTeardown;if(S(d))try{d()}catch(w){o=w instanceof rt?w.errors:[w]}var v=this._finalizers;if(v){this._finalizers=null;try{for(var T=J(v),g=T.next();!g.done;g=T.next()){var O=g.value;try{ir(O)}catch(w){o=o??[],w instanceof rt?o=V(V([],W(o)),W(w.errors)):o.push(w)}}}catch(w){n={error:w}}finally{try{g&&!g.done&&(i=T.return)&&i.call(T)}finally{if(n)throw n.error}}}if(o)throw new rt(o)}},r.prototype.add=function(e){var t;if(e&&e!==this)if(this.closed)ir(e);else{if(e instanceof r){if(e.closed||e._hasParent(this))return;e._addParent(this)}(this._finalizers=(t=this._finalizers)!==null&&t!==void 0?t:[]).push(e)}},r.prototype._hasParent=function(e){var t=this._parentage;return t===e||Array.isArray(t)&&t.includes(e)},r.prototype._addParent=function(e){var t=this._parentage;this._parentage=Array.isArray(t)?(t.push(e),t):t?[t,e]:e},r.prototype._removeParent=function(e){var t=this._parentage;t===e?this._parentage=null:Array.isArray(t)&&se(t,e)},r.prototype.remove=function(e){var t=this._finalizers;t&&se(t,e),e instanceof r&&e._removeParent(this)},r.EMPTY=function(){var e=new r;return e.closed=!0,e}(),r}();var Pt=H.EMPTY;function nt(r){return r instanceof H||r&&"closed"in r&&S(r.remove)&&S(r.add)&&S(r.unsubscribe)}function ir(r){S(r)?r():r.unsubscribe()}var z={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var je={setTimeout:function(r,e){for(var t=[],n=2;n0},enumerable:!1,configurable:!0}),e.prototype._trySubscribe=function(t){return this._throwIfClosed(),r.prototype._trySubscribe.call(this,t)},e.prototype._subscribe=function(t){return this._throwIfClosed(),this._checkFinalizedStatuses(t),this._innerSubscribe(t)},e.prototype._innerSubscribe=function(t){var n=this,i=this,o=i.hasError,a=i.isStopped,s=i.observers;return o||a?Pt:(this.currentObservers=null,s.push(t),new H(function(){n.currentObservers=null,se(s,t)}))},e.prototype._checkFinalizedStatuses=function(t){var n=this,i=n.hasError,o=n.thrownError,a=n.isStopped;i?t.error(o):a&&t.complete()},e.prototype.asObservable=function(){var t=new E;return t.source=this,t},e.create=function(t,n){return new at(t,n)},e}(E);var at=function(r){A(e,r);function e(t,n){var i=r.call(this)||this;return i.destination=t,i.source=n,i}return e.prototype.next=function(t){var n,i;(i=(n=this.destination)===null||n===void 0?void 0:n.next)===null||i===void 0||i.call(n,t)},e.prototype.error=function(t){var n,i;(i=(n=this.destination)===null||n===void 0?void 0:n.error)===null||i===void 0||i.call(n,t)},e.prototype.complete=function(){var t,n;(n=(t=this.destination)===null||t===void 0?void 0:t.complete)===null||n===void 0||n.call(t)},e.prototype._subscribe=function(t){var n,i;return(i=(n=this.source)===null||n===void 0?void 0:n.subscribe(t))!==null&&i!==void 0?i:Pt},e}(B);var We={now:function(){return(We.delegate||Date).now()},delegate:void 0};var st=function(r){A(e,r);function e(t,n,i){t===void 0&&(t=1/0),n===void 0&&(n=1/0),i===void 0&&(i=We);var o=r.call(this)||this;return o._bufferSize=t,o._windowTime=n,o._timestampProvider=i,o._buffer=[],o._infiniteTimeWindow=!0,o._infiniteTimeWindow=n===1/0,o._bufferSize=Math.max(1,t),o._windowTime=Math.max(1,n),o}return e.prototype.next=function(t){var n=this,i=n.isStopped,o=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,u=n._windowTime;i||(o.push(t),!a&&o.push(s.now()+u)),this._trimBuffer(),r.prototype.next.call(this,t)},e.prototype._subscribe=function(t){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(t),i=this,o=i._infiniteTimeWindow,a=i._buffer,s=a.slice(),u=0;u0&&(d=new le({next:function(Xe){return Ke.next(Xe)},error:function(Xe){w=!0,R(),v=Ut(L,i,Xe),Ke.error(Xe)},complete:function(){O=!0,R(),v=Ut(L,a),Ke.complete()}}),I(Ue).subscribe(d))})(l)}}function Ut(r,e){for(var t=[],n=2;n{let e=new URL(window.location.href),t=e.protocol==="https:"?"wss":"ws",n;if(r.host)n=new URL(r.ws_path,t+"://"+r.host);else{let o=new URL(e);o.protocol=t,o.pathname=r.ws_path,n=o}return Vt(n.toString()).pipe(Ft({delay:5e3}))}}}var Nr={debug(...r){},error(...r){},info(...r){},trace(...r){}},zt={name:"console",globalSetup:r=>{let e=new B;return[e,{debug:function(...n){e.next({level:"debug",args:n})},info:function(...n){e.next({level:"info",args:n})},trace:function(...n){e.next({level:"trace",args:n})},error:function(...n){e.next({level:"error",args:n})}}]},resetSink:(r,e,t)=>r.pipe(Be(n=>{let i=["trace","debug","info","error"],o=i.indexOf(n.level),a=i.indexOf(t.log_level);o>=a&&console.log(`[${n.level}]`,...n.args)}),ze())};var k;(function(r){r.assertEqual=i=>i;function e(i){}r.assertIs=e;function t(i){throw new Error}r.assertNever=t,r.arrayToEnum=i=>{let o={};for(let a of i)o[a]=a;return o},r.getValidEnumValues=i=>{let o=r.objectKeys(i).filter(s=>typeof i[i[s]]!="number"),a={};for(let s of o)a[s]=i[s];return r.objectValues(a)},r.objectValues=i=>r.objectKeys(i).map(function(o){return i[o]}),r.objectKeys=typeof Object.keys=="function"?i=>Object.keys(i):i=>{let o=[];for(let a in i)Object.prototype.hasOwnProperty.call(i,a)&&o.push(a);return o},r.find=(i,o)=>{for(let a of i)if(o(a))return a},r.isInteger=typeof Number.isInteger=="function"?i=>Number.isInteger(i):i=>typeof i=="number"&&isFinite(i)&&Math.floor(i)===i;function n(i,o=" | "){return i.map(a=>typeof a=="string"?`'${a}'`:a).join(o)}r.joinValues=n,r.jsonStringifyReplacer=(i,o)=>typeof o=="bigint"?o.toString():o})(k||(k={}));var qt;(function(r){r.mergeShapes=(e,t)=>({...e,...t})})(qt||(qt={}));var h=k.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),Q=r=>{switch(typeof r){case"undefined":return h.undefined;case"string":return h.string;case"number":return isNaN(r)?h.nan:h.number;case"boolean":return h.boolean;case"function":return h.function;case"bigint":return h.bigint;case"symbol":return h.symbol;case"object":return Array.isArray(r)?h.array:r===null?h.null:r.then&&typeof r.then=="function"&&r.catch&&typeof r.catch=="function"?h.promise:typeof Map<"u"&&r instanceof Map?h.map:typeof Set<"u"&&r instanceof Set?h.set:typeof Date<"u"&&r instanceof Date?h.date:h.object;default:return h.unknown}},f=k.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]),Ln=r=>JSON.stringify(r,null,2).replace(/"([^"]+)":/g,"$1:"),M=class r extends Error{get errors(){return this.issues}constructor(e){super(),this.issues=[],this.addIssue=n=>{this.issues=[...this.issues,n]},this.addIssues=(n=[])=>{this.issues=[...this.issues,...n]};let t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name="ZodError",this.issues=e}format(e){let t=e||function(o){return o.message},n={_errors:[]},i=o=>{for(let a of o.issues)if(a.code==="invalid_union")a.unionErrors.map(i);else if(a.code==="invalid_return_type")i(a.returnTypeError);else if(a.code==="invalid_arguments")i(a.argumentsError);else if(a.path.length===0)n._errors.push(t(a));else{let s=n,u=0;for(;ut.message){let t={},n=[];for(let i of this.issues)i.path.length>0?(t[i.path[0]]=t[i.path[0]]||[],t[i.path[0]].push(e(i))):n.push(e(i));return{formErrors:n,fieldErrors:t}}get formErrors(){return this.flatten()}};M.create=r=>new M(r);var Ne=(r,e)=>{let t;switch(r.code){case f.invalid_type:r.received===h.undefined?t="Required":t=`Expected ${r.expected}, received ${r.received}`;break;case f.invalid_literal:t=`Invalid literal value, expected ${JSON.stringify(r.expected,k.jsonStringifyReplacer)}`;break;case f.unrecognized_keys:t=`Unrecognized key(s) in object: ${k.joinValues(r.keys,", ")}`;break;case f.invalid_union:t="Invalid input";break;case f.invalid_union_discriminator:t=`Invalid discriminator value. Expected ${k.joinValues(r.options)}`;break;case f.invalid_enum_value:t=`Invalid enum value. Expected ${k.joinValues(r.options)}, received '${r.received}'`;break;case f.invalid_arguments:t="Invalid function arguments";break;case f.invalid_return_type:t="Invalid function return type";break;case f.invalid_date:t="Invalid date";break;case f.invalid_string:typeof r.validation=="object"?"includes"in r.validation?(t=`Invalid input: must include "${r.validation.includes}"`,typeof r.validation.position=="number"&&(t=`${t} at one or more positions greater than or equal to ${r.validation.position}`)):"startsWith"in r.validation?t=`Invalid input: must start with "${r.validation.startsWith}"`:"endsWith"in r.validation?t=`Invalid input: must end with "${r.validation.endsWith}"`:k.assertNever(r.validation):r.validation!=="regex"?t=`Invalid ${r.validation}`:t="Invalid";break;case f.too_small:r.type==="array"?t=`Array must contain ${r.exact?"exactly":r.inclusive?"at least":"more than"} ${r.minimum} element(s)`:r.type==="string"?t=`String must contain ${r.exact?"exactly":r.inclusive?"at least":"over"} ${r.minimum} character(s)`:r.type==="number"?t=`Number must be ${r.exact?"exactly equal to ":r.inclusive?"greater than or equal to ":"greater than "}${r.minimum}`:r.type==="date"?t=`Date must be ${r.exact?"exactly equal to ":r.inclusive?"greater than or equal to ":"greater than "}${new Date(Number(r.minimum))}`:t="Invalid input";break;case f.too_big:r.type==="array"?t=`Array must contain ${r.exact?"exactly":r.inclusive?"at most":"less than"} ${r.maximum} element(s)`:r.type==="string"?t=`String must contain ${r.exact?"exactly":r.inclusive?"at most":"under"} ${r.maximum} character(s)`:r.type==="number"?t=`Number must be ${r.exact?"exactly":r.inclusive?"less than or equal to":"less than"} ${r.maximum}`:r.type==="bigint"?t=`BigInt must be ${r.exact?"exactly":r.inclusive?"less than or equal to":"less than"} ${r.maximum}`:r.type==="date"?t=`Date must be ${r.exact?"exactly":r.inclusive?"smaller than or equal to":"smaller than"} ${new Date(Number(r.maximum))}`:t="Invalid input";break;case f.custom:t="Invalid input";break;case f.invalid_intersection_types:t="Intersection results could not be merged";break;case f.not_multiple_of:t=`Number must be a multiple of ${r.multipleOf}`;break;case f.not_finite:t="Number must be finite";break;default:t=e.defaultError,k.assertNever(r)}return{message:t}},Zr=Ne;function Mn(r){Zr=r}function wt(){return Zr}var St=r=>{let{data:e,path:t,errorMaps:n,issueData:i}=r,o=[...t,...i.path||[]],a={...i,path:o};if(i.message!==void 0)return{...i,path:o,message:i.message};let s="",u=n.filter(l=>!!l).slice().reverse();for(let l of u)s=l(a,{data:e,defaultError:s}).message;return{...i,path:o,message:s}},Zn=[];function p(r,e){let t=wt(),n=St({issueData:e,data:r.data,path:r.path,errorMaps:[r.common.contextualErrorMap,r.schemaErrorMap,t,t===Ne?void 0:Ne].filter(i=>!!i)});r.common.issues.push(n)}var P=class r{constructor(){this.value="valid"}dirty(){this.value==="valid"&&(this.value="dirty")}abort(){this.value!=="aborted"&&(this.value="aborted")}static mergeArray(e,t){let n=[];for(let i of t){if(i.status==="aborted")return _;i.status==="dirty"&&e.dirty(),n.push(i.value)}return{status:e.value,value:n}}static async mergeObjectAsync(e,t){let n=[];for(let i of t){let o=await i.key,a=await i.value;n.push({key:o,value:a})}return r.mergeObjectSync(e,n)}static mergeObjectSync(e,t){let n={};for(let i of t){let{key:o,value:a}=i;if(o.status==="aborted"||a.status==="aborted")return _;o.status==="dirty"&&e.dirty(),a.status==="dirty"&&e.dirty(),o.value!=="__proto__"&&(typeof a.value<"u"||i.alwaysSet)&&(n[o.value]=a.value)}return{status:e.value,value:n}}},_=Object.freeze({status:"aborted"}),Pe=r=>({status:"dirty",value:r}),N=r=>({status:"valid",value:r}),Ht=r=>r.status==="aborted",Gt=r=>r.status==="dirty",fe=r=>r.status==="valid",Ge=r=>typeof Promise<"u"&&r instanceof Promise;function kt(r,e,t,n){if(t==="a"&&!n)throw new TypeError("Private accessor was defined without a getter");if(typeof e=="function"?r!==e||!n:!e.has(r))throw new TypeError("Cannot read private member from an object whose class did not declare it");return t==="m"?n:t==="a"?n.call(r):n?n.value:e.get(r)}function Fr(r,e,t,n,i){if(n==="m")throw new TypeError("Private method is not writable");if(n==="a"&&!i)throw new TypeError("Private accessor was defined without a setter");if(typeof e=="function"?r!==e||!i:!e.has(r))throw new TypeError("Cannot write private member to an object whose class did not declare it");return n==="a"?i.call(r,t):i?i.value=t:e.set(r,t),t}var m;(function(r){r.errToObj=e=>typeof e=="string"?{message:e}:e||{},r.toString=e=>typeof e=="string"?e:e?.message})(m||(m={}));var qe,He,$=class{constructor(e,t,n,i){this._cachedPath=[],this.parent=e,this.data=t,this._path=n,this._key=i}get path(){return this._cachedPath.length||(this._key instanceof Array?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}},Dr=(r,e)=>{if(fe(e))return{success:!0,data:e.value};if(!r.common.issues.length)throw new Error("Validation failed but no issues detected.");return{success:!1,get error(){if(this._error)return this._error;let t=new M(r.common.issues);return this._error=t,this._error}}};function b(r){if(!r)return{};let{errorMap:e,invalid_type_error:t,required_error:n,description:i}=r;if(e&&(t||n))throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);return e?{errorMap:e,description:i}:{errorMap:(a,s)=>{var u,l;let{message:d}=r;return a.code==="invalid_enum_value"?{message:d??s.defaultError}:typeof s.data>"u"?{message:(u=d??n)!==null&&u!==void 0?u:s.defaultError}:a.code!=="invalid_type"?{message:s.defaultError}:{message:(l=d??t)!==null&&l!==void 0?l:s.defaultError}},description:i}}var x=class{get description(){return this._def.description}_getType(e){return Q(e.data)}_getOrReturnCtx(e,t){return t||{common:e.parent.common,data:e.data,parsedType:Q(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}_processInputParams(e){return{status:new P,ctx:{common:e.parent.common,data:e.data,parsedType:Q(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}}_parseSync(e){let t=this._parse(e);if(Ge(t))throw new Error("Synchronous parse encountered promise.");return t}_parseAsync(e){let t=this._parse(e);return Promise.resolve(t)}parse(e,t){let n=this.safeParse(e,t);if(n.success)return n.data;throw n.error}safeParse(e,t){var n;let i={common:{issues:[],async:(n=t?.async)!==null&&n!==void 0?n:!1,contextualErrorMap:t?.errorMap},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Q(e)},o=this._parseSync({data:e,path:i.path,parent:i});return Dr(i,o)}"~validate"(e){var t,n;let i={common:{issues:[],async:!!this["~standard"].async},path:[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Q(e)};if(!this["~standard"].async)try{let o=this._parseSync({data:e,path:[],parent:i});return fe(o)?{value:o.value}:{issues:i.common.issues}}catch(o){!((n=(t=o?.message)===null||t===void 0?void 0:t.toLowerCase())===null||n===void 0)&&n.includes("encountered")&&(this["~standard"].async=!0),i.common={issues:[],async:!0}}return this._parseAsync({data:e,path:[],parent:i}).then(o=>fe(o)?{value:o.value}:{issues:i.common.issues})}async parseAsync(e,t){let n=await this.safeParseAsync(e,t);if(n.success)return n.data;throw n.error}async safeParseAsync(e,t){let n={common:{issues:[],contextualErrorMap:t?.errorMap,async:!0},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Q(e)},i=this._parse({data:e,path:n.path,parent:n}),o=await(Ge(i)?i:Promise.resolve(i));return Dr(n,o)}refine(e,t){let n=i=>typeof t=="string"||typeof t>"u"?{message:t}:typeof t=="function"?t(i):t;return this._refinement((i,o)=>{let a=e(i),s=()=>o.addIssue({code:f.custom,...n(i)});return typeof Promise<"u"&&a instanceof Promise?a.then(u=>u?!0:(s(),!1)):a?!0:(s(),!1)})}refinement(e,t){return this._refinement((n,i)=>e(n)?!0:(i.addIssue(typeof t=="function"?t(n,i):t),!1))}_refinement(e){return new Z({schema:this,typeName:y.ZodEffects,effect:{type:"refinement",refinement:e}})}superRefine(e){return this._refinement(e)}constructor(e){this.spa=this.safeParseAsync,this._def=e,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.readonly=this.readonly.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this),this["~standard"]={version:1,vendor:"zod",validate:t=>this["~validate"](t)}}optional(){return U.create(this,this._def)}nullable(){return Y.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return te.create(this)}promise(){return oe.create(this,this._def)}or(e){return _e.create([this,e],this._def)}and(e){return be.create(this,e,this._def)}transform(e){return new Z({...b(this._def),schema:this,typeName:y.ZodEffects,effect:{type:"transform",transform:e}})}default(e){let t=typeof e=="function"?e:()=>e;return new Te({...b(this._def),innerType:this,defaultValue:t,typeName:y.ZodDefault})}brand(){return new Ye({typeName:y.ZodBranded,type:this,...b(this._def)})}catch(e){let t=typeof e=="function"?e:()=>e;return new Oe({...b(this._def),innerType:this,catchValue:t,typeName:y.ZodCatch})}describe(e){let t=this.constructor;return new t({...this._def,description:e})}pipe(e){return Je.create(this,e)}readonly(){return Ee.create(this)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}},Fn=/^c[^\s-]{8,}$/i,Un=/^[0-9a-z]+$/,$n=/^[0-9A-HJKMNP-TV-Z]{26}$/i,Wn=/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i,Vn=/^[a-z0-9_-]{21}$/i,zn=/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/,Bn=/^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/,qn=/^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i,Hn="^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$",Bt,Gn=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,Yn=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/,Jn=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/,Kn=/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,Xn=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,Qn=/^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/,Ur="((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))",ei=new RegExp(`^${Ur}$`);function $r(r){let e="([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d";return r.precision?e=`${e}\\.\\d{${r.precision}}`:r.precision==null&&(e=`${e}(\\.\\d+)?`),e}function ti(r){return new RegExp(`^${$r(r)}$`)}function Wr(r){let e=`${Ur}T${$r(r)}`,t=[];return t.push(r.local?"Z?":"Z"),r.offset&&t.push("([+-]\\d{2}:?\\d{2})"),e=`${e}(${t.join("|")})`,new RegExp(`^${e}$`)}function ri(r,e){return!!((e==="v4"||!e)&&Gn.test(r)||(e==="v6"||!e)&&Jn.test(r))}function ni(r,e){if(!zn.test(r))return!1;try{let[t]=r.split("."),n=t.replace(/-/g,"+").replace(/_/g,"/").padEnd(t.length+(4-t.length%4)%4,"="),i=JSON.parse(atob(n));return!(typeof i!="object"||i===null||!i.typ||!i.alg||e&&i.alg!==e)}catch{return!1}}function ii(r,e){return!!((e==="v4"||!e)&&Yn.test(r)||(e==="v6"||!e)&&Kn.test(r))}var ne=class r extends x{_parse(e){if(this._def.coerce&&(e.data=String(e.data)),this._getType(e)!==h.string){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_type,expected:h.string,received:o.parsedType}),_}let n=new P,i;for(let o of this._def.checks)if(o.kind==="min")e.data.lengtho.value&&(i=this._getOrReturnCtx(e,i),p(i,{code:f.too_big,maximum:o.value,type:"string",inclusive:!0,exact:!1,message:o.message}),n.dirty());else if(o.kind==="length"){let a=e.data.length>o.value,s=e.data.lengthe.test(i),{validation:t,code:f.invalid_string,...m.errToObj(n)})}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}email(e){return this._addCheck({kind:"email",...m.errToObj(e)})}url(e){return this._addCheck({kind:"url",...m.errToObj(e)})}emoji(e){return this._addCheck({kind:"emoji",...m.errToObj(e)})}uuid(e){return this._addCheck({kind:"uuid",...m.errToObj(e)})}nanoid(e){return this._addCheck({kind:"nanoid",...m.errToObj(e)})}cuid(e){return this._addCheck({kind:"cuid",...m.errToObj(e)})}cuid2(e){return this._addCheck({kind:"cuid2",...m.errToObj(e)})}ulid(e){return this._addCheck({kind:"ulid",...m.errToObj(e)})}base64(e){return this._addCheck({kind:"base64",...m.errToObj(e)})}base64url(e){return this._addCheck({kind:"base64url",...m.errToObj(e)})}jwt(e){return this._addCheck({kind:"jwt",...m.errToObj(e)})}ip(e){return this._addCheck({kind:"ip",...m.errToObj(e)})}cidr(e){return this._addCheck({kind:"cidr",...m.errToObj(e)})}datetime(e){var t,n;return typeof e=="string"?this._addCheck({kind:"datetime",precision:null,offset:!1,local:!1,message:e}):this._addCheck({kind:"datetime",precision:typeof e?.precision>"u"?null:e?.precision,offset:(t=e?.offset)!==null&&t!==void 0?t:!1,local:(n=e?.local)!==null&&n!==void 0?n:!1,...m.errToObj(e?.message)})}date(e){return this._addCheck({kind:"date",message:e})}time(e){return typeof e=="string"?this._addCheck({kind:"time",precision:null,message:e}):this._addCheck({kind:"time",precision:typeof e?.precision>"u"?null:e?.precision,...m.errToObj(e?.message)})}duration(e){return this._addCheck({kind:"duration",...m.errToObj(e)})}regex(e,t){return this._addCheck({kind:"regex",regex:e,...m.errToObj(t)})}includes(e,t){return this._addCheck({kind:"includes",value:e,position:t?.position,...m.errToObj(t?.message)})}startsWith(e,t){return this._addCheck({kind:"startsWith",value:e,...m.errToObj(t)})}endsWith(e,t){return this._addCheck({kind:"endsWith",value:e,...m.errToObj(t)})}min(e,t){return this._addCheck({kind:"min",value:e,...m.errToObj(t)})}max(e,t){return this._addCheck({kind:"max",value:e,...m.errToObj(t)})}length(e,t){return this._addCheck({kind:"length",value:e,...m.errToObj(t)})}nonempty(e){return this.min(1,m.errToObj(e))}trim(){return new r({...this._def,checks:[...this._def.checks,{kind:"trim"}]})}toLowerCase(){return new r({...this._def,checks:[...this._def.checks,{kind:"toLowerCase"}]})}toUpperCase(){return new r({...this._def,checks:[...this._def.checks,{kind:"toUpperCase"}]})}get isDatetime(){return!!this._def.checks.find(e=>e.kind==="datetime")}get isDate(){return!!this._def.checks.find(e=>e.kind==="date")}get isTime(){return!!this._def.checks.find(e=>e.kind==="time")}get isDuration(){return!!this._def.checks.find(e=>e.kind==="duration")}get isEmail(){return!!this._def.checks.find(e=>e.kind==="email")}get isURL(){return!!this._def.checks.find(e=>e.kind==="url")}get isEmoji(){return!!this._def.checks.find(e=>e.kind==="emoji")}get isUUID(){return!!this._def.checks.find(e=>e.kind==="uuid")}get isNANOID(){return!!this._def.checks.find(e=>e.kind==="nanoid")}get isCUID(){return!!this._def.checks.find(e=>e.kind==="cuid")}get isCUID2(){return!!this._def.checks.find(e=>e.kind==="cuid2")}get isULID(){return!!this._def.checks.find(e=>e.kind==="ulid")}get isIP(){return!!this._def.checks.find(e=>e.kind==="ip")}get isCIDR(){return!!this._def.checks.find(e=>e.kind==="cidr")}get isBase64(){return!!this._def.checks.find(e=>e.kind==="base64")}get isBase64url(){return!!this._def.checks.find(e=>e.kind==="base64url")}get minLength(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxLength(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.value{var e;return new ne({checks:[],typeName:y.ZodString,coerce:(e=r?.coerce)!==null&&e!==void 0?e:!1,...b(r)})};function oi(r,e){let t=(r.toString().split(".")[1]||"").length,n=(e.toString().split(".")[1]||"").length,i=t>n?t:n,o=parseInt(r.toFixed(i).replace(".","")),a=parseInt(e.toFixed(i).replace(".",""));return o%a/Math.pow(10,i)}var pe=class r extends x{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte,this.step=this.multipleOf}_parse(e){if(this._def.coerce&&(e.data=Number(e.data)),this._getType(e)!==h.number){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_type,expected:h.number,received:o.parsedType}),_}let n,i=new P;for(let o of this._def.checks)o.kind==="int"?k.isInteger(e.data)||(n=this._getOrReturnCtx(e,n),p(n,{code:f.invalid_type,expected:"integer",received:"float",message:o.message}),i.dirty()):o.kind==="min"?(o.inclusive?e.datao.value:e.data>=o.value)&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.too_big,maximum:o.value,type:"number",inclusive:o.inclusive,exact:!1,message:o.message}),i.dirty()):o.kind==="multipleOf"?oi(e.data,o.value)!==0&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.not_multiple_of,multipleOf:o.value,message:o.message}),i.dirty()):o.kind==="finite"?Number.isFinite(e.data)||(n=this._getOrReturnCtx(e,n),p(n,{code:f.not_finite,message:o.message}),i.dirty()):k.assertNever(o);return{status:i.value,value:e.data}}gte(e,t){return this.setLimit("min",e,!0,m.toString(t))}gt(e,t){return this.setLimit("min",e,!1,m.toString(t))}lte(e,t){return this.setLimit("max",e,!0,m.toString(t))}lt(e,t){return this.setLimit("max",e,!1,m.toString(t))}setLimit(e,t,n,i){return new r({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:n,message:m.toString(i)}]})}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}int(e){return this._addCheck({kind:"int",message:m.toString(e)})}positive(e){return this._addCheck({kind:"min",value:0,inclusive:!1,message:m.toString(e)})}negative(e){return this._addCheck({kind:"max",value:0,inclusive:!1,message:m.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:0,inclusive:!0,message:m.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:0,inclusive:!0,message:m.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:m.toString(t)})}finite(e){return this._addCheck({kind:"finite",message:m.toString(e)})}safe(e){return this._addCheck({kind:"min",inclusive:!0,value:Number.MIN_SAFE_INTEGER,message:m.toString(e)})._addCheck({kind:"max",inclusive:!0,value:Number.MAX_SAFE_INTEGER,message:m.toString(e)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuee.kind==="int"||e.kind==="multipleOf"&&k.isInteger(e.value))}get isFinite(){let e=null,t=null;for(let n of this._def.checks){if(n.kind==="finite"||n.kind==="int"||n.kind==="multipleOf")return!0;n.kind==="min"?(t===null||n.value>t)&&(t=n.value):n.kind==="max"&&(e===null||n.valuenew pe({checks:[],typeName:y.ZodNumber,coerce:r?.coerce||!1,...b(r)});var he=class r extends x{constructor(){super(...arguments),this.min=this.gte,this.max=this.lte}_parse(e){if(this._def.coerce)try{e.data=BigInt(e.data)}catch{return this._getInvalidInput(e)}if(this._getType(e)!==h.bigint)return this._getInvalidInput(e);let n,i=new P;for(let o of this._def.checks)o.kind==="min"?(o.inclusive?e.datao.value:e.data>=o.value)&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.too_big,type:"bigint",maximum:o.value,inclusive:o.inclusive,message:o.message}),i.dirty()):o.kind==="multipleOf"?e.data%o.value!==BigInt(0)&&(n=this._getOrReturnCtx(e,n),p(n,{code:f.not_multiple_of,multipleOf:o.value,message:o.message}),i.dirty()):k.assertNever(o);return{status:i.value,value:e.data}}_getInvalidInput(e){let t=this._getOrReturnCtx(e);return p(t,{code:f.invalid_type,expected:h.bigint,received:t.parsedType}),_}gte(e,t){return this.setLimit("min",e,!0,m.toString(t))}gt(e,t){return this.setLimit("min",e,!1,m.toString(t))}lte(e,t){return this.setLimit("max",e,!0,m.toString(t))}lt(e,t){return this.setLimit("max",e,!1,m.toString(t))}setLimit(e,t,n,i){return new r({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:n,message:m.toString(i)}]})}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}positive(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!1,message:m.toString(e)})}negative(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!1,message:m.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!0,message:m.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!0,message:m.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:m.toString(t)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.value{var e;return new he({checks:[],typeName:y.ZodBigInt,coerce:(e=r?.coerce)!==null&&e!==void 0?e:!1,...b(r)})};var me=class extends x{_parse(e){if(this._def.coerce&&(e.data=!!e.data),this._getType(e)!==h.boolean){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.boolean,received:n.parsedType}),_}return N(e.data)}};me.create=r=>new me({typeName:y.ZodBoolean,coerce:r?.coerce||!1,...b(r)});var ve=class r extends x{_parse(e){if(this._def.coerce&&(e.data=new Date(e.data)),this._getType(e)!==h.date){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_type,expected:h.date,received:o.parsedType}),_}if(isNaN(e.data.getTime())){let o=this._getOrReturnCtx(e);return p(o,{code:f.invalid_date}),_}let n=new P,i;for(let o of this._def.checks)o.kind==="min"?e.data.getTime()o.value&&(i=this._getOrReturnCtx(e,i),p(i,{code:f.too_big,message:o.message,inclusive:!0,exact:!1,maximum:o.value,type:"date"}),n.dirty()):k.assertNever(o);return{status:n.value,value:new Date(e.data.getTime())}}_addCheck(e){return new r({...this._def,checks:[...this._def.checks,e]})}min(e,t){return this._addCheck({kind:"min",value:e.getTime(),message:m.toString(t)})}max(e,t){return this._addCheck({kind:"max",value:e.getTime(),message:m.toString(t)})}get minDate(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e!=null?new Date(e):null}get maxDate(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuenew ve({checks:[],coerce:r?.coerce||!1,typeName:y.ZodDate,...b(r)});var De=class extends x{_parse(e){if(this._getType(e)!==h.symbol){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.symbol,received:n.parsedType}),_}return N(e.data)}};De.create=r=>new De({typeName:y.ZodSymbol,...b(r)});var ye=class extends x{_parse(e){if(this._getType(e)!==h.undefined){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.undefined,received:n.parsedType}),_}return N(e.data)}};ye.create=r=>new ye({typeName:y.ZodUndefined,...b(r)});var ge=class extends x{_parse(e){if(this._getType(e)!==h.null){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.null,received:n.parsedType}),_}return N(e.data)}};ge.create=r=>new ge({typeName:y.ZodNull,...b(r)});var ie=class extends x{constructor(){super(...arguments),this._any=!0}_parse(e){return N(e.data)}};ie.create=r=>new ie({typeName:y.ZodAny,...b(r)});var ee=class extends x{constructor(){super(...arguments),this._unknown=!0}_parse(e){return N(e.data)}};ee.create=r=>new ee({typeName:y.ZodUnknown,...b(r)});var q=class extends x{_parse(e){let t=this._getOrReturnCtx(e);return p(t,{code:f.invalid_type,expected:h.never,received:t.parsedType}),_}};q.create=r=>new q({typeName:y.ZodNever,...b(r)});var Le=class extends x{_parse(e){if(this._getType(e)!==h.undefined){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.void,received:n.parsedType}),_}return N(e.data)}};Le.create=r=>new Le({typeName:y.ZodVoid,...b(r)});var te=class r extends x{_parse(e){let{ctx:t,status:n}=this._processInputParams(e),i=this._def;if(t.parsedType!==h.array)return p(t,{code:f.invalid_type,expected:h.array,received:t.parsedType}),_;if(i.exactLength!==null){let a=t.data.length>i.exactLength.value,s=t.data.lengthi.maxLength.value&&(p(t,{code:f.too_big,maximum:i.maxLength.value,type:"array",inclusive:!0,exact:!1,message:i.maxLength.message}),n.dirty()),t.common.async)return Promise.all([...t.data].map((a,s)=>i.type._parseAsync(new $(t,a,t.path,s)))).then(a=>P.mergeArray(n,a));let o=[...t.data].map((a,s)=>i.type._parseSync(new $(t,a,t.path,s)));return P.mergeArray(n,o)}get element(){return this._def.type}min(e,t){return new r({...this._def,minLength:{value:e,message:m.toString(t)}})}max(e,t){return new r({...this._def,maxLength:{value:e,message:m.toString(t)}})}length(e,t){return new r({...this._def,exactLength:{value:e,message:m.toString(t)}})}nonempty(e){return this.min(1,e)}};te.create=(r,e)=>new te({type:r,minLength:null,maxLength:null,exactLength:null,typeName:y.ZodArray,...b(e)});function Re(r){if(r instanceof D){let e={};for(let t in r.shape){let n=r.shape[t];e[t]=U.create(Re(n))}return new D({...r._def,shape:()=>e})}else return r instanceof te?new te({...r._def,type:Re(r.element)}):r instanceof U?U.create(Re(r.unwrap())):r instanceof Y?Y.create(Re(r.unwrap())):r instanceof G?G.create(r.items.map(e=>Re(e))):r}var D=class r extends x{constructor(){super(...arguments),this._cached=null,this.nonstrict=this.passthrough,this.augment=this.extend}_getCached(){if(this._cached!==null)return this._cached;let e=this._def.shape(),t=k.objectKeys(e);return this._cached={shape:e,keys:t}}_parse(e){if(this._getType(e)!==h.object){let l=this._getOrReturnCtx(e);return p(l,{code:f.invalid_type,expected:h.object,received:l.parsedType}),_}let{status:n,ctx:i}=this._processInputParams(e),{shape:o,keys:a}=this._getCached(),s=[];if(!(this._def.catchall instanceof q&&this._def.unknownKeys==="strip"))for(let l in i.data)a.includes(l)||s.push(l);let u=[];for(let l of a){let d=o[l],v=i.data[l];u.push({key:{status:"valid",value:l},value:d._parse(new $(i,v,i.path,l)),alwaysSet:l in i.data})}if(this._def.catchall instanceof q){let l=this._def.unknownKeys;if(l==="passthrough")for(let d of s)u.push({key:{status:"valid",value:d},value:{status:"valid",value:i.data[d]}});else if(l==="strict")s.length>0&&(p(i,{code:f.unrecognized_keys,keys:s}),n.dirty());else if(l!=="strip")throw new Error("Internal ZodObject error: invalid unknownKeys value.")}else{let l=this._def.catchall;for(let d of s){let v=i.data[d];u.push({key:{status:"valid",value:d},value:l._parse(new $(i,v,i.path,d)),alwaysSet:d in i.data})}}return i.common.async?Promise.resolve().then(async()=>{let l=[];for(let d of u){let v=await d.key,T=await d.value;l.push({key:v,value:T,alwaysSet:d.alwaysSet})}return l}).then(l=>P.mergeObjectSync(n,l)):P.mergeObjectSync(n,u)}get shape(){return this._def.shape()}strict(e){return m.errToObj,new r({...this._def,unknownKeys:"strict",...e!==void 0?{errorMap:(t,n)=>{var i,o,a,s;let u=(a=(o=(i=this._def).errorMap)===null||o===void 0?void 0:o.call(i,t,n).message)!==null&&a!==void 0?a:n.defaultError;return t.code==="unrecognized_keys"?{message:(s=m.errToObj(e).message)!==null&&s!==void 0?s:u}:{message:u}}}:{}})}strip(){return new r({...this._def,unknownKeys:"strip"})}passthrough(){return new r({...this._def,unknownKeys:"passthrough"})}extend(e){return new r({...this._def,shape:()=>({...this._def.shape(),...e})})}merge(e){return new r({unknownKeys:e._def.unknownKeys,catchall:e._def.catchall,shape:()=>({...this._def.shape(),...e._def.shape()}),typeName:y.ZodObject})}setKey(e,t){return this.augment({[e]:t})}catchall(e){return new r({...this._def,catchall:e})}pick(e){let t={};return k.objectKeys(e).forEach(n=>{e[n]&&this.shape[n]&&(t[n]=this.shape[n])}),new r({...this._def,shape:()=>t})}omit(e){let t={};return k.objectKeys(this.shape).forEach(n=>{e[n]||(t[n]=this.shape[n])}),new r({...this._def,shape:()=>t})}deepPartial(){return Re(this)}partial(e){let t={};return k.objectKeys(this.shape).forEach(n=>{let i=this.shape[n];e&&!e[n]?t[n]=i:t[n]=i.optional()}),new r({...this._def,shape:()=>t})}required(e){let t={};return k.objectKeys(this.shape).forEach(n=>{if(e&&!e[n])t[n]=this.shape[n];else{let o=this.shape[n];for(;o instanceof U;)o=o._def.innerType;t[n]=o}}),new r({...this._def,shape:()=>t})}keyof(){return Vr(k.objectKeys(this.shape))}};D.create=(r,e)=>new D({shape:()=>r,unknownKeys:"strip",catchall:q.create(),typeName:y.ZodObject,...b(e)});D.strictCreate=(r,e)=>new D({shape:()=>r,unknownKeys:"strict",catchall:q.create(),typeName:y.ZodObject,...b(e)});D.lazycreate=(r,e)=>new D({shape:r,unknownKeys:"strip",catchall:q.create(),typeName:y.ZodObject,...b(e)});var _e=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n=this._def.options;function i(o){for(let s of o)if(s.result.status==="valid")return s.result;for(let s of o)if(s.result.status==="dirty")return t.common.issues.push(...s.ctx.common.issues),s.result;let a=o.map(s=>new M(s.ctx.common.issues));return p(t,{code:f.invalid_union,unionErrors:a}),_}if(t.common.async)return Promise.all(n.map(async o=>{let a={...t,common:{...t.common,issues:[]},parent:null};return{result:await o._parseAsync({data:t.data,path:t.path,parent:a}),ctx:a}})).then(i);{let o,a=[];for(let u of n){let l={...t,common:{...t.common,issues:[]},parent:null},d=u._parseSync({data:t.data,path:t.path,parent:l});if(d.status==="valid")return d;d.status==="dirty"&&!o&&(o={result:d,ctx:l}),l.common.issues.length&&a.push(l.common.issues)}if(o)return t.common.issues.push(...o.ctx.common.issues),o.result;let s=a.map(u=>new M(u));return p(t,{code:f.invalid_union,unionErrors:s}),_}}get options(){return this._def.options}};_e.create=(r,e)=>new _e({options:r,typeName:y.ZodUnion,...b(e)});var X=r=>r instanceof xe?X(r.schema):r instanceof Z?X(r.innerType()):r instanceof we?[r.value]:r instanceof Se?r.options:r instanceof ke?k.objectValues(r.enum):r instanceof Te?X(r._def.innerType):r instanceof ye?[void 0]:r instanceof ge?[null]:r instanceof U?[void 0,...X(r.unwrap())]:r instanceof Y?[null,...X(r.unwrap())]:r instanceof Ye||r instanceof Ee?X(r.unwrap()):r instanceof Oe?X(r._def.innerType):[],Tt=class r extends x{_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==h.object)return p(t,{code:f.invalid_type,expected:h.object,received:t.parsedType}),_;let n=this.discriminator,i=t.data[n],o=this.optionsMap.get(i);return o?t.common.async?o._parseAsync({data:t.data,path:t.path,parent:t}):o._parseSync({data:t.data,path:t.path,parent:t}):(p(t,{code:f.invalid_union_discriminator,options:Array.from(this.optionsMap.keys()),path:[n]}),_)}get discriminator(){return this._def.discriminator}get options(){return this._def.options}get optionsMap(){return this._def.optionsMap}static create(e,t,n){let i=new Map;for(let o of t){let a=X(o.shape[e]);if(!a.length)throw new Error(`A discriminator value for key \`${e}\` could not be extracted from all schema options`);for(let s of a){if(i.has(s))throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(s)}`);i.set(s,o)}}return new r({typeName:y.ZodDiscriminatedUnion,discriminator:e,options:t,optionsMap:i,...b(n)})}};function Yt(r,e){let t=Q(r),n=Q(e);if(r===e)return{valid:!0,data:r};if(t===h.object&&n===h.object){let i=k.objectKeys(e),o=k.objectKeys(r).filter(s=>i.indexOf(s)!==-1),a={...r,...e};for(let s of o){let u=Yt(r[s],e[s]);if(!u.valid)return{valid:!1};a[s]=u.data}return{valid:!0,data:a}}else if(t===h.array&&n===h.array){if(r.length!==e.length)return{valid:!1};let i=[];for(let o=0;o{if(Ht(o)||Ht(a))return _;let s=Yt(o.value,a.value);return s.valid?((Gt(o)||Gt(a))&&t.dirty(),{status:t.value,value:s.data}):(p(n,{code:f.invalid_intersection_types}),_)};return n.common.async?Promise.all([this._def.left._parseAsync({data:n.data,path:n.path,parent:n}),this._def.right._parseAsync({data:n.data,path:n.path,parent:n})]).then(([o,a])=>i(o,a)):i(this._def.left._parseSync({data:n.data,path:n.path,parent:n}),this._def.right._parseSync({data:n.data,path:n.path,parent:n}))}};be.create=(r,e,t)=>new be({left:r,right:e,typeName:y.ZodIntersection,...b(t)});var G=class r extends x{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.array)return p(n,{code:f.invalid_type,expected:h.array,received:n.parsedType}),_;if(n.data.lengththis._def.items.length&&(p(n,{code:f.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:"array"}),t.dirty());let o=[...n.data].map((a,s)=>{let u=this._def.items[s]||this._def.rest;return u?u._parse(new $(n,a,n.path,s)):null}).filter(a=>!!a);return n.common.async?Promise.all(o).then(a=>P.mergeArray(t,a)):P.mergeArray(t,o)}get items(){return this._def.items}rest(e){return new r({...this._def,rest:e})}};G.create=(r,e)=>{if(!Array.isArray(r))throw new Error("You must pass an array of schemas to z.tuple([ ... ])");return new G({items:r,typeName:y.ZodTuple,rest:null,...b(e)})};var Ot=class r extends x{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.object)return p(n,{code:f.invalid_type,expected:h.object,received:n.parsedType}),_;let i=[],o=this._def.keyType,a=this._def.valueType;for(let s in n.data)i.push({key:o._parse(new $(n,s,n.path,s)),value:a._parse(new $(n,n.data[s],n.path,s)),alwaysSet:s in n.data});return n.common.async?P.mergeObjectAsync(t,i):P.mergeObjectSync(t,i)}get element(){return this._def.valueType}static create(e,t,n){return t instanceof x?new r({keyType:e,valueType:t,typeName:y.ZodRecord,...b(n)}):new r({keyType:ne.create(),valueType:e,typeName:y.ZodRecord,...b(t)})}},Me=class extends x{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.map)return p(n,{code:f.invalid_type,expected:h.map,received:n.parsedType}),_;let i=this._def.keyType,o=this._def.valueType,a=[...n.data.entries()].map(([s,u],l)=>({key:i._parse(new $(n,s,n.path,[l,"key"])),value:o._parse(new $(n,u,n.path,[l,"value"]))}));if(n.common.async){let s=new Map;return Promise.resolve().then(async()=>{for(let u of a){let l=await u.key,d=await u.value;if(l.status==="aborted"||d.status==="aborted")return _;(l.status==="dirty"||d.status==="dirty")&&t.dirty(),s.set(l.value,d.value)}return{status:t.value,value:s}})}else{let s=new Map;for(let u of a){let l=u.key,d=u.value;if(l.status==="aborted"||d.status==="aborted")return _;(l.status==="dirty"||d.status==="dirty")&&t.dirty(),s.set(l.value,d.value)}return{status:t.value,value:s}}}};Me.create=(r,e,t)=>new Me({valueType:e,keyType:r,typeName:y.ZodMap,...b(t)});var Ze=class r extends x{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.parsedType!==h.set)return p(n,{code:f.invalid_type,expected:h.set,received:n.parsedType}),_;let i=this._def;i.minSize!==null&&n.data.sizei.maxSize.value&&(p(n,{code:f.too_big,maximum:i.maxSize.value,type:"set",inclusive:!0,exact:!1,message:i.maxSize.message}),t.dirty());let o=this._def.valueType;function a(u){let l=new Set;for(let d of u){if(d.status==="aborted")return _;d.status==="dirty"&&t.dirty(),l.add(d.value)}return{status:t.value,value:l}}let s=[...n.data.values()].map((u,l)=>o._parse(new $(n,u,n.path,l)));return n.common.async?Promise.all(s).then(u=>a(u)):a(s)}min(e,t){return new r({...this._def,minSize:{value:e,message:m.toString(t)}})}max(e,t){return new r({...this._def,maxSize:{value:e,message:m.toString(t)}})}size(e,t){return this.min(e,t).max(e,t)}nonempty(e){return this.min(1,e)}};Ze.create=(r,e)=>new Ze({valueType:r,minSize:null,maxSize:null,typeName:y.ZodSet,...b(e)});var Et=class r extends x{constructor(){super(...arguments),this.validate=this.implement}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==h.function)return p(t,{code:f.invalid_type,expected:h.function,received:t.parsedType}),_;function n(s,u){return St({data:s,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,wt(),Ne].filter(l=>!!l),issueData:{code:f.invalid_arguments,argumentsError:u}})}function i(s,u){return St({data:s,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,wt(),Ne].filter(l=>!!l),issueData:{code:f.invalid_return_type,returnTypeError:u}})}let o={errorMap:t.common.contextualErrorMap},a=t.data;if(this._def.returns instanceof oe){let s=this;return N(async function(...u){let l=new M([]),d=await s._def.args.parseAsync(u,o).catch(g=>{throw l.addIssue(n(u,g)),l}),v=await Reflect.apply(a,this,d);return await s._def.returns._def.type.parseAsync(v,o).catch(g=>{throw l.addIssue(i(v,g)),l})})}else{let s=this;return N(function(...u){let l=s._def.args.safeParse(u,o);if(!l.success)throw new M([n(u,l.error)]);let d=Reflect.apply(a,this,l.data),v=s._def.returns.safeParse(d,o);if(!v.success)throw new M([i(d,v.error)]);return v.data})}}parameters(){return this._def.args}returnType(){return this._def.returns}args(...e){return new r({...this._def,args:G.create(e).rest(ee.create())})}returns(e){return new r({...this._def,returns:e})}implement(e){return this.parse(e)}strictImplement(e){return this.parse(e)}static create(e,t,n){return new r({args:e||G.create([]).rest(ee.create()),returns:t||ee.create(),typeName:y.ZodFunction,...b(n)})}},xe=class extends x{get schema(){return this._def.getter()}_parse(e){let{ctx:t}=this._processInputParams(e);return this._def.getter()._parse({data:t.data,path:t.path,parent:t})}};xe.create=(r,e)=>new xe({getter:r,typeName:y.ZodLazy,...b(e)});var we=class extends x{_parse(e){if(e.data!==this._def.value){let t=this._getOrReturnCtx(e);return p(t,{received:t.data,code:f.invalid_literal,expected:this._def.value}),_}return{status:"valid",value:e.data}}get value(){return this._def.value}};we.create=(r,e)=>new we({value:r,typeName:y.ZodLiteral,...b(e)});function Vr(r,e){return new Se({values:r,typeName:y.ZodEnum,...b(e)})}var Se=class r extends x{constructor(){super(...arguments),qe.set(this,void 0)}_parse(e){if(typeof e.data!="string"){let t=this._getOrReturnCtx(e),n=this._def.values;return p(t,{expected:k.joinValues(n),received:t.parsedType,code:f.invalid_type}),_}if(kt(this,qe,"f")||Fr(this,qe,new Set(this._def.values),"f"),!kt(this,qe,"f").has(e.data)){let t=this._getOrReturnCtx(e),n=this._def.values;return p(t,{received:t.data,code:f.invalid_enum_value,options:n}),_}return N(e.data)}get options(){return this._def.values}get enum(){let e={};for(let t of this._def.values)e[t]=t;return e}get Values(){let e={};for(let t of this._def.values)e[t]=t;return e}get Enum(){let e={};for(let t of this._def.values)e[t]=t;return e}extract(e,t=this._def){return r.create(e,{...this._def,...t})}exclude(e,t=this._def){return r.create(this.options.filter(n=>!e.includes(n)),{...this._def,...t})}};qe=new WeakMap;Se.create=Vr;var ke=class extends x{constructor(){super(...arguments),He.set(this,void 0)}_parse(e){let t=k.getValidEnumValues(this._def.values),n=this._getOrReturnCtx(e);if(n.parsedType!==h.string&&n.parsedType!==h.number){let i=k.objectValues(t);return p(n,{expected:k.joinValues(i),received:n.parsedType,code:f.invalid_type}),_}if(kt(this,He,"f")||Fr(this,He,new Set(k.getValidEnumValues(this._def.values)),"f"),!kt(this,He,"f").has(e.data)){let i=k.objectValues(t);return p(n,{received:n.data,code:f.invalid_enum_value,options:i}),_}return N(e.data)}get enum(){return this._def.values}};He=new WeakMap;ke.create=(r,e)=>new ke({values:r,typeName:y.ZodNativeEnum,...b(e)});var oe=class extends x{unwrap(){return this._def.type}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==h.promise&&t.common.async===!1)return p(t,{code:f.invalid_type,expected:h.promise,received:t.parsedType}),_;let n=t.parsedType===h.promise?t.data:Promise.resolve(t.data);return N(n.then(i=>this._def.type.parseAsync(i,{path:t.path,errorMap:t.common.contextualErrorMap})))}};oe.create=(r,e)=>new oe({type:r,typeName:y.ZodPromise,...b(e)});var Z=class extends x{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName===y.ZodEffects?this._def.schema.sourceType():this._def.schema}_parse(e){let{status:t,ctx:n}=this._processInputParams(e),i=this._def.effect||null,o={addIssue:a=>{p(n,a),a.fatal?t.abort():t.dirty()},get path(){return n.path}};if(o.addIssue=o.addIssue.bind(o),i.type==="preprocess"){let a=i.transform(n.data,o);if(n.common.async)return Promise.resolve(a).then(async s=>{if(t.value==="aborted")return _;let u=await this._def.schema._parseAsync({data:s,path:n.path,parent:n});return u.status==="aborted"?_:u.status==="dirty"||t.value==="dirty"?Pe(u.value):u});{if(t.value==="aborted")return _;let s=this._def.schema._parseSync({data:a,path:n.path,parent:n});return s.status==="aborted"?_:s.status==="dirty"||t.value==="dirty"?Pe(s.value):s}}if(i.type==="refinement"){let a=s=>{let u=i.refinement(s,o);if(n.common.async)return Promise.resolve(u);if(u instanceof Promise)throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");return s};if(n.common.async===!1){let s=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});return s.status==="aborted"?_:(s.status==="dirty"&&t.dirty(),a(s.value),{status:t.value,value:s.value})}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(s=>s.status==="aborted"?_:(s.status==="dirty"&&t.dirty(),a(s.value).then(()=>({status:t.value,value:s.value}))))}if(i.type==="transform")if(n.common.async===!1){let a=this._def.schema._parseSync({data:n.data,path:n.path,parent:n});if(!fe(a))return a;let s=i.transform(a.value,o);if(s instanceof Promise)throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.");return{status:t.value,value:s}}else return this._def.schema._parseAsync({data:n.data,path:n.path,parent:n}).then(a=>fe(a)?Promise.resolve(i.transform(a.value,o)).then(s=>({status:t.value,value:s})):a);k.assertNever(i)}};Z.create=(r,e,t)=>new Z({schema:r,typeName:y.ZodEffects,effect:e,...b(t)});Z.createWithPreprocess=(r,e,t)=>new Z({schema:e,effect:{type:"preprocess",transform:r},typeName:y.ZodEffects,...b(t)});var U=class extends x{_parse(e){return this._getType(e)===h.undefined?N(void 0):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};U.create=(r,e)=>new U({innerType:r,typeName:y.ZodOptional,...b(e)});var Y=class extends x{_parse(e){return this._getType(e)===h.null?N(null):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}};Y.create=(r,e)=>new Y({innerType:r,typeName:y.ZodNullable,...b(e)});var Te=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return t.parsedType===h.undefined&&(n=this._def.defaultValue()),this._def.innerType._parse({data:n,path:t.path,parent:t})}removeDefault(){return this._def.innerType}};Te.create=(r,e)=>new Te({innerType:r,typeName:y.ZodDefault,defaultValue:typeof e.default=="function"?e.default:()=>e.default,...b(e)});var Oe=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n={...t,common:{...t.common,issues:[]}},i=this._def.innerType._parse({data:n.data,path:n.path,parent:{...n}});return Ge(i)?i.then(o=>({status:"valid",value:o.status==="valid"?o.value:this._def.catchValue({get error(){return new M(n.common.issues)},input:n.data})})):{status:"valid",value:i.status==="valid"?i.value:this._def.catchValue({get error(){return new M(n.common.issues)},input:n.data})}}removeCatch(){return this._def.innerType}};Oe.create=(r,e)=>new Oe({innerType:r,typeName:y.ZodCatch,catchValue:typeof e.catch=="function"?e.catch:()=>e.catch,...b(e)});var Fe=class extends x{_parse(e){if(this._getType(e)!==h.nan){let n=this._getOrReturnCtx(e);return p(n,{code:f.invalid_type,expected:h.nan,received:n.parsedType}),_}return{status:"valid",value:e.data}}};Fe.create=r=>new Fe({typeName:y.ZodNaN,...b(r)});var ai=Symbol("zod_brand"),Ye=class extends x{_parse(e){let{ctx:t}=this._processInputParams(e),n=t.data;return this._def.type._parse({data:n,path:t.path,parent:t})}unwrap(){return this._def.type}},Je=class r extends x{_parse(e){let{status:t,ctx:n}=this._processInputParams(e);if(n.common.async)return(async()=>{let o=await this._def.in._parseAsync({data:n.data,path:n.path,parent:n});return o.status==="aborted"?_:o.status==="dirty"?(t.dirty(),Pe(o.value)):this._def.out._parseAsync({data:o.value,path:n.path,parent:n})})();{let i=this._def.in._parseSync({data:n.data,path:n.path,parent:n});return i.status==="aborted"?_:i.status==="dirty"?(t.dirty(),{status:"dirty",value:i.value}):this._def.out._parseSync({data:i.value,path:n.path,parent:n})}}static create(e,t){return new r({in:e,out:t,typeName:y.ZodPipeline})}},Ee=class extends x{_parse(e){let t=this._def.innerType._parse(e),n=i=>(fe(i)&&(i.value=Object.freeze(i.value)),i);return Ge(t)?t.then(i=>n(i)):n(t)}unwrap(){return this._def.innerType}};Ee.create=(r,e)=>new Ee({innerType:r,typeName:y.ZodReadonly,...b(e)});function Lr(r,e){let t=typeof r=="function"?r(e):typeof r=="string"?{message:r}:r;return typeof t=="string"?{message:t}:t}function zr(r,e={},t){return r?ie.create().superRefine((n,i)=>{var o,a;let s=r(n);if(s instanceof Promise)return s.then(u=>{var l,d;if(!u){let v=Lr(e,n),T=(d=(l=v.fatal)!==null&&l!==void 0?l:t)!==null&&d!==void 0?d:!0;i.addIssue({code:"custom",...v,fatal:T})}});if(!s){let u=Lr(e,n),l=(a=(o=u.fatal)!==null&&o!==void 0?o:t)!==null&&a!==void 0?a:!0;i.addIssue({code:"custom",...u,fatal:l})}}):ie.create()}var si={object:D.lazycreate},y;(function(r){r.ZodString="ZodString",r.ZodNumber="ZodNumber",r.ZodNaN="ZodNaN",r.ZodBigInt="ZodBigInt",r.ZodBoolean="ZodBoolean",r.ZodDate="ZodDate",r.ZodSymbol="ZodSymbol",r.ZodUndefined="ZodUndefined",r.ZodNull="ZodNull",r.ZodAny="ZodAny",r.ZodUnknown="ZodUnknown",r.ZodNever="ZodNever",r.ZodVoid="ZodVoid",r.ZodArray="ZodArray",r.ZodObject="ZodObject",r.ZodUnion="ZodUnion",r.ZodDiscriminatedUnion="ZodDiscriminatedUnion",r.ZodIntersection="ZodIntersection",r.ZodTuple="ZodTuple",r.ZodRecord="ZodRecord",r.ZodMap="ZodMap",r.ZodSet="ZodSet",r.ZodFunction="ZodFunction",r.ZodLazy="ZodLazy",r.ZodLiteral="ZodLiteral",r.ZodEnum="ZodEnum",r.ZodEffects="ZodEffects",r.ZodNativeEnum="ZodNativeEnum",r.ZodOptional="ZodOptional",r.ZodNullable="ZodNullable",r.ZodDefault="ZodDefault",r.ZodCatch="ZodCatch",r.ZodPromise="ZodPromise",r.ZodBranded="ZodBranded",r.ZodPipeline="ZodPipeline",r.ZodReadonly="ZodReadonly"})(y||(y={}));var ci=(r,e={message:`Input not instance of ${r.name}`})=>zr(t=>t instanceof r,e),Br=ne.create,qr=pe.create,ui=Fe.create,li=he.create,Hr=me.create,di=ve.create,fi=De.create,pi=ye.create,hi=ge.create,mi=ie.create,vi=ee.create,yi=q.create,gi=Le.create,_i=te.create,bi=D.create,xi=D.strictCreate,wi=_e.create,Si=Tt.create,ki=be.create,Ti=G.create,Oi=Ot.create,Ei=Me.create,Ci=Ze.create,ji=Et.create,Ii=xe.create,Ai=we.create,Ri=Se.create,Pi=ke.create,Ni=oe.create,Mr=Z.create,Di=U.create,Li=Y.create,Mi=Z.createWithPreprocess,Zi=Je.create,Fi=()=>Br().optional(),Ui=()=>qr().optional(),$i=()=>Hr().optional(),Wi={string:r=>ne.create({...r,coerce:!0}),number:r=>pe.create({...r,coerce:!0}),boolean:r=>me.create({...r,coerce:!0}),bigint:r=>he.create({...r,coerce:!0}),date:r=>ve.create({...r,coerce:!0})},Vi=_,c=Object.freeze({__proto__:null,defaultErrorMap:Ne,setErrorMap:Mn,getErrorMap:wt,makeIssue:St,EMPTY_PATH:Zn,addIssueToContext:p,ParseStatus:P,INVALID:_,DIRTY:Pe,OK:N,isAborted:Ht,isDirty:Gt,isValid:fe,isAsync:Ge,get util(){return k},get objectUtil(){return qt},ZodParsedType:h,getParsedType:Q,ZodType:x,datetimeRegex:Wr,ZodString:ne,ZodNumber:pe,ZodBigInt:he,ZodBoolean:me,ZodDate:ve,ZodSymbol:De,ZodUndefined:ye,ZodNull:ge,ZodAny:ie,ZodUnknown:ee,ZodNever:q,ZodVoid:Le,ZodArray:te,ZodObject:D,ZodUnion:_e,ZodDiscriminatedUnion:Tt,ZodIntersection:be,ZodTuple:G,ZodRecord:Ot,ZodMap:Me,ZodSet:Ze,ZodFunction:Et,ZodLazy:xe,ZodLiteral:we,ZodEnum:Se,ZodNativeEnum:ke,ZodPromise:oe,ZodEffects:Z,ZodTransformer:Z,ZodOptional:U,ZodNullable:Y,ZodDefault:Te,ZodCatch:Oe,ZodNaN:Fe,BRAND:ai,ZodBranded:Ye,ZodPipeline:Je,ZodReadonly:Ee,custom:zr,Schema:x,ZodSchema:x,late:si,get ZodFirstPartyTypeKind(){return y},coerce:Wi,any:mi,array:_i,bigint:li,boolean:Hr,date:di,discriminatedUnion:Si,effect:Mr,enum:Ri,function:ji,instanceof:ci,intersection:ki,lazy:Ii,literal:Ai,map:Ei,nan:ui,nativeEnum:Pi,never:yi,null:hi,nullable:Li,number:qr,object:bi,oboolean:$i,onumber:Ui,optional:Di,ostring:Fi,pipeline:Zi,preprocess:Mi,promise:Ni,record:Oi,set:Ci,strictObject:xi,string:Br,symbol:fi,transformer:Mr,tuple:Ti,undefined:pi,union:wi,unknown:vi,void:gi,NEVER:Vi,ZodIssueCode:f,quotelessJson:Ln,ZodError:M});var Jr=(r=>(r.Info="info",r.Debug="debug",r.Trace="trace",r.Error="error",r))(Jr||{}),Kr=(r=>(r.Changed="Changed",r.Added="Added",r.Removed="Removed",r))(Kr||{}),Xr=(r=>(r.External="BSLIVE_EXTERNAL",r))(Xr||{}),zi=c.nativeEnum(Jr),Gr=c.object({log_level:zi}),Bi=c.object({ws_path:c.string(),host:c.string().optional()}),qi=c.object({kind:c.string(),ms:c.string()}),Yr=c.object({path:c.string()}),Hi=c.object({paths:c.array(c.string())}),Qr=c.discriminatedUnion("kind",[c.object({kind:c.literal("Both"),payload:c.object({name:c.string(),bind_address:c.string()})}),c.object({kind:c.literal("Address"),payload:c.object({bind_address:c.string()})}),c.object({kind:c.literal("Named"),payload:c.object({name:c.string()})}),c.object({kind:c.literal("Port"),payload:c.object({port:c.number()})}),c.object({kind:c.literal("PortNamed"),payload:c.object({port:c.number(),name:c.string()})})]),Gi=c.object({id:c.string(),identity:Qr,socket_addr:c.string()}),en=c.object({servers:c.array(Gi)}),tn=c.object({connect:Bi,ctx_message:c.string()}),Yi=c.object({path:c.string()}),Ji=c.object({body:c.string()}),Ki=c.discriminatedUnion("kind",[c.object({kind:c.literal("Html"),payload:c.object({html:c.string()})}),c.object({kind:c.literal("Json"),payload:c.object({json_str:c.string()})}),c.object({kind:c.literal("Raw"),payload:c.object({raw:c.string()})}),c.object({kind:c.literal("Sse"),payload:c.object({sse:Ji})}),c.object({kind:c.literal("Proxy"),payload:c.object({proxy:c.string()})}),c.object({kind:c.literal("Dir"),payload:c.object({dir:c.string(),base:c.string().optional()})})]),Xi=c.discriminatedUnion("kind",[c.object({kind:c.literal("Stopped"),payload:c.object({bind_address:c.string()})}),c.object({kind:c.literal("Started"),payload:c.undefined().optional()}),c.object({kind:c.literal("Patched"),payload:c.undefined().optional()}),c.object({kind:c.literal("Errored"),payload:c.object({error:c.string()})})]),Qi=c.object({identity:Qr,change:Xi}),Gu=c.object({items:c.array(Qi)}),eo=c.object({path:c.string(),kind:Ki}),to=c.object({servers_resp:en}),ro=c.object({line:c.string(),prefix:c.string().optional()}),no=c.object({line:c.string(),prefix:c.string().optional()}),io=c.object({paths:c.array(c.string())}),oo=c.object({paths:c.array(c.string()),debounce:qi}),ao=c.nativeEnum(Kr),Ct=c.lazy(()=>c.discriminatedUnion("kind",[c.object({kind:c.literal("Fs"),payload:c.object({path:c.string(),change_kind:ao})}),c.object({kind:c.literal("FsMany"),payload:c.array(Ct)})])),Yu=c.discriminatedUnion("kind",[c.object({kind:c.literal("Change"),payload:Ct}),c.object({kind:c.literal("WsConnection"),payload:Gr}),c.object({kind:c.literal("Config"),payload:Gr})]),Ju=c.nativeEnum(Xr),so=c.discriminatedUnion("kind",[c.object({kind:c.literal("Stdout"),payload:no}),c.object({kind:c.literal("Stderr"),payload:ro})]),Ku=c.discriminatedUnion("kind",[c.object({kind:c.literal("MissingInputs"),payload:c.string()}),c.object({kind:c.literal("InvalidInput"),payload:c.string()}),c.object({kind:c.literal("NotFound"),payload:c.string()}),c.object({kind:c.literal("InputWriteError"),payload:c.string()}),c.object({kind:c.literal("PathError"),payload:c.string()}),c.object({kind:c.literal("PortError"),payload:c.string()}),c.object({kind:c.literal("DirError"),payload:c.string()}),c.object({kind:c.literal("YamlError"),payload:c.string()}),c.object({kind:c.literal("MarkdownError"),payload:c.string()}),c.object({kind:c.literal("HtmlError"),payload:c.string()}),c.object({kind:c.literal("Io"),payload:c.string()}),c.object({kind:c.literal("UnsupportedExtension"),payload:c.string()}),c.object({kind:c.literal("MissingExtension"),payload:c.string()}),c.object({kind:c.literal("EmptyInput"),payload:c.string()}),c.object({kind:c.literal("BsLiveRules"),payload:c.string()})]),Xu=c.discriminatedUnion("kind",[c.object({kind:c.literal("ServersChanged"),payload:en}),c.object({kind:c.literal("TaskReport"),payload:c.object({id:c.string()})})]),Qu=c.discriminatedUnion("kind",[c.object({kind:c.literal("Started"),payload:c.undefined().optional()}),c.object({kind:c.literal("FailedStartup"),payload:c.string()})]),el=c.object({routes:c.array(eo),id:c.string()}),tl=c.discriminatedUnion("kind",[c.object({kind:c.literal("ServersChanged"),payload:to}),c.object({kind:c.literal("Watching"),payload:oo}),c.object({kind:c.literal("WatchingStopped"),payload:io}),c.object({kind:c.literal("FileChanged"),payload:Yr}),c.object({kind:c.literal("FilesChanged"),payload:Hi}),c.object({kind:c.literal("InputFileChanged"),payload:Yr}),c.object({kind:c.literal("InputAccepted"),payload:Yi}),c.object({kind:c.literal("OutputLine"),payload:so})]);var rn=[{selector:"background",styleNames:["backgroundImage"]},{selector:"border",styleNames:["borderImage","webkitBorderImage","MozBorderImage"]}],jt={stylesheetReloadTimeout:15e3},co=/\.(jpe?g|png|gif|svg)$/i,It=class{constructor(e,t,n){this.window=e,this.console=t,this.Timer=n,this.document=this.window.document,this.importCacheWaitPeriod=200,this.plugins=[]}addPlugin(e){return this.plugins.push(e)}analyze(e){}reload(e,t={}){if(this.options={...jt,...t},!(t.liveCSS&&e.match(/\.css(?:\.map)?$/i)&&this.reloadStylesheet(e))){if(t.liveImg&&e.match(co)){this.reloadImages(e);return}if(t.isChromeExtension){this.reloadChromeExtension();return}return this.reloadPage()}}reloadPage(){return this.window.document.location.reload()}reloadChromeExtension(){return this.window.chrome.runtime.reload()}reloadImages(e){let t,n=this.generateUniqueString();for(t of Array.from(this.document.images))nn(e,Jt(t.src))&&(t.src=this.generateCacheBustUrl(t.src,n));if(this.document.querySelectorAll)for(let{selector:i,styleNames:o}of rn)for(t of Array.from(this.document.querySelectorAll(`[style*=${i}]`)))this.reloadStyleImages(t.style,o,e,n);if(this.document.styleSheets)return Array.from(this.document.styleSheets).map(i=>this.reloadStylesheetImages(i,e,n))}reloadStylesheetImages(e,t,n){let i;try{i=(e||{}).cssRules}catch{}if(i)for(let o of Array.from(i))switch(o.type){case CSSRule.IMPORT_RULE:this.reloadStylesheetImages(o.styleSheet,t,n);break;case CSSRule.STYLE_RULE:for(let{styleNames:a}of rn)this.reloadStyleImages(o.style,a,t,n);break;case CSSRule.MEDIA_RULE:this.reloadStylesheetImages(o,t,n);break}}reloadStyleImages(e,t,n,i){for(let o of t){let a=e[o];if(typeof a=="string"){let s=a.replace(new RegExp("\\burl\\s*\\(([^)]*)\\)"),(u,l)=>nn(n,Jt(l))?`url(${this.generateCacheBustUrl(l,i)})`:u);s!==a&&(e[o]=s)}}}reloadStylesheet(e){let t=this.options||jt,n,i,o=(()=>{let u=[];for(i of Array.from(this.document.getElementsByTagName("link")))i.rel.match(/^stylesheet$/i)&&!i.__LiveReload_pendingRemoval&&u.push(i);return u})(),a=[];for(n of Array.from(this.document.getElementsByTagName("style")))n.sheet&&this.collectImportedStylesheets(n,n.sheet,a);for(i of Array.from(o))this.collectImportedStylesheets(i,i.sheet,a);if(this.window.StyleFix&&this.document.querySelectorAll)for(n of Array.from(this.document.querySelectorAll("style[data-href]")))o.push(n);this.console.debug(`found ${o.length} LINKed stylesheets, ${a.length} @imported stylesheets`);let s=uo(e,o.concat(a),u=>Jt(this.linkHref(u)));if(s)s.object.rule?(this.console.debug(`is reloading imported stylesheet: ${s.object.href}`),this.reattachImportedRule(s.object)):(this.console.debug(`is reloading stylesheet: ${this.linkHref(s.object)}`),this.reattachStylesheetLink(s.object));else if(t.reloadMissingCSS){this.console.debug(`will reload all stylesheets because path '${e}' did not match any specific one. To disable this behavior, set 'options.reloadMissingCSS' to 'false'.`);for(i of Array.from(o))this.reattachStylesheetLink(i)}else this.console.debug(`will not reload path '${e}' because the stylesheet was not found on the page and 'options.reloadMissingCSS' was set to 'false'.`);return!0}collectImportedStylesheets(e,t,n){let i;try{i=(t||{}).cssRules}catch{}if(i&&i.length)for(let o=0;o{if(!i)return i=!0,t()};if(e.onload=()=>(this.console.debug("the new stylesheet has finished loading"),this.knownToSupportCssOnLoad=!0,o()),!this.knownToSupportCssOnLoad){let a;(a=()=>e.sheet?(this.console.debug("is polling until the new CSS finishes loading..."),o()):this.Timer.start(50,a))()}return this.Timer.start(n.stylesheetReloadTimeout,o)}linkHref(e){return e.href||e.getAttribute&&e.getAttribute("data-href")}reattachStylesheetLink(e){let t;if(e.__LiveReload_pendingRemoval)return;e.__LiveReload_pendingRemoval=!0,e.tagName==="STYLE"?(t=this.document.createElement("link"),t.rel="stylesheet",t.media=e.media,t.disabled=e.disabled):t=e.cloneNode(!1),t.href=this.generateCacheBustUrl(this.linkHref(e));let n=e.parentNode;return n.lastChild===e?n.appendChild(t):n.insertBefore(t,e.nextSibling),this.waitUntilCssLoads(t,()=>{let i;return/AppleWebKit/.test(this.window.navigator.userAgent)?i=5:i=200,this.Timer.start(i,()=>{if(e.parentNode)return e.parentNode.removeChild(e),t.onreadystatechange=null,this.window.StyleFix?this.window.StyleFix.link(t):void 0})})}reattachImportedRule({rule:e,index:t,link:n}){let i=e.parentStyleSheet,o=this.generateCacheBustUrl(e.href),a=e.media.length?[].join.call(e.media,", "):"",s=`@import url("${o}") ${a};`;e.__LiveReload_newHref=o;let u=this.document.createElement("link");return u.rel="stylesheet",u.href=o,u.__LiveReload_pendingRemoval=!0,n.parentNode&&n.parentNode.insertBefore(u,n),this.Timer.start(this.importCacheWaitPeriod,()=>{if(u.parentNode&&u.parentNode.removeChild(u),e.__LiveReload_newHref===o)return i.insertRule(s,t),i.deleteRule(t+1),e=i.cssRules[t],e.__LiveReload_newHref=o,this.Timer.start(this.importCacheWaitPeriod,()=>{if(e.__LiveReload_newHref===o)return i.insertRule(s,t),i.deleteRule(t+1)})})}generateUniqueString(){return`livereload=${Date.now()}`}generateCacheBustUrl(e,t){let n=this.options||jt,i,o;if(t||(t=this.generateUniqueString()),{url:e,hash:i,params:o}=on(e),n.overrideURL&&e.indexOf(n.serverURL)<0){let s=e;e=n.serverURL+n.overrideURL+"?url="+encodeURIComponent(e),this.console.debug(`is overriding source URL ${s} with ${e}`)}let a=o.replace(/(\?|&)livereload=(\d+)/,(s,u)=>`${u}${t}`);return a===o&&(o.length===0?a=`?${t}`:a=`${o}&${t}`),e+a+i}};function on(r){let e="",t="",n=r.indexOf("#");n>=0&&(e=r.slice(n),r=r.slice(0,n));let i=r.indexOf("??");return i>=0?i+1!==r.lastIndexOf("?")&&(n=r.lastIndexOf("?")):n=r.indexOf("?"),n>=0&&(t=r.slice(n),r=r.slice(0,n)),{url:r,params:t,hash:e}}function Jt(r){if(!r)return"";let e;return{url:r}=on(r),r.indexOf("file://")===0?e=r.replace(new RegExp("^file://(localhost)?"),""):e=r.replace(new RegExp("^([^:]+:)?//([^:/]+)(:\\d*)?/"),"/"),decodeURIComponent(e)}function an(r,e){if(r=r.replace(/^\/+/,"").toLowerCase(),e=e.replace(/^\/+/,"").toLowerCase(),r===e)return 1e4;let t=r.split(/\/|\\/).reverse(),n=e.split(/\/|\\/).reverse(),i=Math.min(t.length,n.length),o=0;for(;on){let n,i={score:0};for(let o of e)n=an(r,t(o)),n>i.score&&(i={object:o,score:n});return i.score===0?null:i}function nn(r,e){return an(r,e)>0}var un=yn(cn());var lo=/\.(jpe?g|png|gif|svg)$/i;function Kt(r,e,t){switch(r.kind){case"FsMany":{if(r.payload.some(i=>{switch(i.kind){case"Fs":return!(i.payload.path.match(/\.css(?:\.map)?$/i)||i.payload.path.match(lo));case"FsMany":throw new Error("unreachable")}}))return window.__playwright?.record?window.__playwright?.record({kind:"reloadPage"}):t.reloadPage();for(let i of r.payload)Kt(i,e,t);break}case"Fs":{let n=r.payload.path,i={liveCSS:!0,liveImg:!0,reloadMissingCSS:!0,originalPath:"",overrideURL:"",serverURL:""};window.__playwright?.record?window.__playwright?.record({kind:"reload",args:{path:n,opts:i}}):(e.trace("will reload a file with path ",n),t.reload(n,i))}}}var Xt={name:"dom plugin",globalSetup:(r,e)=>{let t=new It(window,e,un.Timer);return[r,[e,t]]},resetSink(r,e,t){let[n,i]=e;return r.pipe(de(o=>o.kind==="Change"),K(o=>o.payload),Be(o=>{n.trace("incoming message",JSON.stringify({change:o,config:t},null,2));let a=Ct.parse(o);Kt(a,n,i)}),ze())}};(r=>{tn.parse(r);let t=Pr().create(r.connect),[n,i]=zt.globalSetup(t,Nr),[o,a]=Xt.globalSetup(t,i),s=t.pipe(de(d=>d.kind==="WsConnection"),K(d=>d.payload),$t()),u=t.pipe(de(d=>d.kind==="Config"),K(d=>d.payload)),l=t.pipe(de(d=>d.kind==="Change"),K(d=>d.payload));xt(u,s).pipe(Wt(d=>{let v=[Xt.resetSink(o,a,d),zt.resetSink(n,i,d)];return xt(...v)})).subscribe(),s.subscribe(d=>{i.info("\u{1F7E2} Browsersync Live connected",{config:d})})})(window.$BSLIVE_INJECT_CONFIG$); From 5edf9361bff4505541aee1eb66d035b311f8b12d Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Sat, 5 Jul 2025 19:37:22 +0100 Subject: [PATCH 03/13] sse tests --- crates/bsnext_client/src/lib.rs | 2 +- crates/bsnext_core/src/export.rs | 6 +-- crates/bsnext_core/src/handlers/proxy.rs | 2 +- crates/bsnext_core/src/raw_loader.rs | 42 +++++++++++++--- .../bsnext_core/src/server/handler_listen.rs | 6 +-- .../bsnext_core/src/server/router/pub_api.rs | 2 +- crates/bsnext_dto/src/archy.rs | 4 +- crates/bsnext_dto/src/external_events.rs | 2 +- crates/bsnext_dto/src/internal.rs | 2 +- crates/bsnext_dto/src/internal_events.rs | 18 +++---- crates/bsnext_dto/src/startup_events.rs | 10 ++-- crates/bsnext_html/src/lib.rs | 2 +- crates/bsnext_input/src/server_config.rs | 4 +- crates/bsnext_md/src/md_writer.rs | 6 +-- crates/bsnext_system/src/path_watchable.rs | 4 +- crates/bsnext_system/src/route_watchable.rs | 50 +++++++++++++------ crates/bsnext_system/src/watch/mod.rs | 4 +- examples/openai/bslive.yml | 9 +++- examples/openai/sse/01.txt | 1 - tests/sse.spec.ts | 29 ++++++++++- 20 files changed, 142 insertions(+), 63 deletions(-) diff --git a/crates/bsnext_client/src/lib.rs b/crates/bsnext_client/src/lib.rs index 18f223ba..f48aaaff 100644 --- a/crates/bsnext_client/src/lib.rs +++ b/crates/bsnext_client/src/lib.rs @@ -8,7 +8,7 @@ pub const WS_PATH: &str = "/__bs_ws"; pub fn html_with_base(base_override: &str) -> String { let base = UI_HTML; - let next = format!("", base_override); + let next = format!(""); let replaced = base.replace("", next.as_str()); replaced } diff --git a/crates/bsnext_core/src/export.rs b/crates/bsnext_core/src/export.rs index e4881a14..9a451210 100644 --- a/crates/bsnext_core/src/export.rs +++ b/crates/bsnext_core/src/export.rs @@ -38,14 +38,14 @@ impl OutputWriterTrait for ExportError { "kind": named, "error": error_string }); - writeln!(_sink, "{}", v)?; + writeln!(_sink, "{v}")?; Ok(()) } fn write_pretty(&self, sink: &mut W) -> anyhow::Result<()> { match self { ExportError::Fs(fs_write_error) => { - writeln!(sink, "[export]: Error! {}", fs_write_error)?; + writeln!(sink, "[export]: Error! {fs_write_error}")?; } } Ok(()) @@ -55,7 +55,7 @@ impl OutputWriterTrait for ExportError { impl OutputWriterTrait for ExportEvent { fn write_json(&self, _sink: &mut W) -> anyhow::Result<()> { let str = serde_json::to_string(&self)?; - writeln!(_sink, "{}", str)?; + writeln!(_sink, "{str}")?; Ok(()) } diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index fea55748..21ed68f2 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -61,7 +61,7 @@ pub async fn proxy_handler( .map(|v| v.as_str()) .unwrap_or(path); - let uri = format!("{}{}", target, path_query); + let uri = format!("{target}{path_query}"); let parsed = Uri::try_from(uri).context("tried to parse")?; let target = match (parsed.host(), parsed.port()) { diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index b01eca55..c171c633 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -1,10 +1,10 @@ -use std::convert::Infallible; - use axum::extract::{Request, State}; use axum::middleware::Next; use axum::response::{Html, IntoResponse, Response, Sse}; use axum::Json; use http::header::CONTENT_TYPE; +use std::convert::Infallible; +use std::fs; use axum::body::Body; use axum::response::sse::Event; @@ -31,17 +31,45 @@ async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { RawRoute::Sse { sse: SseOpts { body, throttle_ms }, } => { - let l = body + let file_prefix = body.split_once("file:"); + let body = match file_prefix { + None => body.to_owned(), + Some((_, path)) => { + let span = tracing::debug_span!("reading SSE body content", path = path); + let _g = span.enter(); + tracing::debug!(?path, "will read sse file content"); + match fs::read_to_string(path) { + Ok(str) => { + tracing::debug!("did read {} bytes", str.len()); + str + } + Err(err) => { + tracing::error!(?err, ?path); + return ( + StatusCode::NOT_FOUND, + format!("{path} was not found. err {err}"), + ) + .into_response(); + } + } + } + }; + let iter = body .lines() - .map(|l| l.to_owned()) - .map(|l| l.strip_prefix("data:").unwrap_or(&l).trim().to_owned()) + .map(|l| { + l.trim() + .strip_prefix("data:") + .unwrap_or(l) + .trim() + .to_owned() + }) .filter(|l| !l.is_empty()) .collect::>(); - tracing::trace!(lines.count = l.len(), "sending EventStream"); + tracing::trace!(lines.count = iter.len(), "sending EventStream"); let milli = throttle_ms.unwrap_or(10); - let stream = tokio_stream::iter(l) + let stream = tokio_stream::iter(iter) .throttle(Duration::from_millis(milli)) .map(|chu| Event::default().data(chu)) .map(Ok::<_, Infallible>); diff --git a/crates/bsnext_core/src/server/handler_listen.rs b/crates/bsnext_core/src/server/handler_listen.rs index 03c05f09..b5f58361 100644 --- a/crates/bsnext_core/src/server/handler_listen.rs +++ b/crates/bsnext_core/src/server/handler_listen.rs @@ -108,14 +108,14 @@ impl actix::Handler for ServerActor { } _ => { tracing::debug!("❌ {:?} [not-started] UNKNOWN {}", identity, e); - Err(ServerError::Unknown(format!("{}", e))) + Err(ServerError::Unknown(format!("{e}"))) } }, }; if !oneshot_send.is_closed() { let _r = oneshot_send.send(result); } else { - tracing::debug!("a channel was closed? {:?}", result); + tracing::debug!("a channel was closed? {result:?}"); } }; @@ -134,7 +134,7 @@ impl actix::Handler for ServerActor { Ok(Err(server_error)) => Err(server_error), Err(other) => { tracing::error!("-->{other}"); - Err(ServerError::Unknown(format!("{:?}", other))) + Err(ServerError::Unknown(format!("{other:?}"))) } } }) diff --git a/crates/bsnext_core/src/server/router/pub_api.rs b/crates/bsnext_core/src/server/router/pub_api.rs index 19c2c0cf..a78c275d 100644 --- a/crates/bsnext_core/src/server/router/pub_api.rs +++ b/crates/bsnext_core/src/server/router/pub_api.rs @@ -22,7 +22,7 @@ async fn all_servers_handler(State(app): State>, _uri: Uri) -> } Err(err) => ( StatusCode::INTERNAL_SERVER_ERROR, - format!("Could not fetch servers: {}", err), + format!("Could not fetch servers: {err}"), ) .into_response(), }, diff --git a/crates/bsnext_dto/src/archy.rs b/crates/bsnext_dto/src/archy.rs index 416d2c3f..b97c2f1c 100644 --- a/crates/bsnext_dto/src/archy.rs +++ b/crates/bsnext_dto/src/archy.rs @@ -46,7 +46,7 @@ impl Display for Token { Token::RightTurnContinueDown => '├', Token::T => '┬', }; - write!(f, "{}", t) + write!(f, "{t}") } } @@ -103,7 +103,7 @@ pub fn archy(obj: &ArchyNode, line_prefix: Option<&str>) -> String { .collect::>() .join(""); - format!("{}{}{}{}", line_prefix, connector, branch, subtree_sliced) + format!("{line_prefix}{connector}{branch}{subtree_sliced}") }) .collect::>() .join(""); diff --git a/crates/bsnext_dto/src/external_events.rs b/crates/bsnext_dto/src/external_events.rs index ba2b8e6d..7d4f3a45 100644 --- a/crates/bsnext_dto/src/external_events.rs +++ b/crates/bsnext_dto/src/external_events.rs @@ -100,7 +100,7 @@ where pub fn print_stopped_watching(w: &mut W, evt: &StoppedWatchingDTO) -> anyhow::Result<()> { for x in &evt.paths { - writeln!(w, "[watching:stopped] {}", x)?; + writeln!(w, "[watching:stopped] {x}")?; } Ok(()) } diff --git a/crates/bsnext_dto/src/internal.rs b/crates/bsnext_dto/src/internal.rs index 852ec7e3..e746a1f7 100644 --- a/crates/bsnext_dto/src/internal.rs +++ b/crates/bsnext_dto/src/internal.rs @@ -198,7 +198,7 @@ impl Display for TaskResult { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.status { TaskStatus::Ok(_s) => write!(f, "✅"), - TaskStatus::Err(err) => write!(f, "❌, {}", err), + TaskStatus::Err(err) => write!(f, "❌, {err}"), } } } diff --git a/crates/bsnext_dto/src/internal_events.rs b/crates/bsnext_dto/src/internal_events.rs index bfb04823..f6cb4d63 100644 --- a/crates/bsnext_dto/src/internal_events.rs +++ b/crates/bsnext_dto/src/internal_events.rs @@ -63,18 +63,18 @@ impl OutputWriterTrait for InternalEvents { let n = miette::GraphicalReportHandler::new(); let mut inner = String::new(); n.render_report(&mut inner, bs_rules).expect("write?"); - writeln!(sink, "{}", inner)?; + writeln!(sink, "{inner}")?; } InternalEvents::InputError(err) => { - writeln!(sink, "{}", err)?; + writeln!(sink, "{err}")?; } InternalEvents::StartupError(err) => { - writeln!(sink, "{}", err)?; + writeln!(sink, "{err}")?; } InternalEvents::TaskReport(TaskReportAndTree { report, tree }) => { if report.has_errors() { let s = archy(tree, None); - write!(sink, "{}", s)?; + write!(sink, "{s}")?; } } } @@ -136,19 +136,19 @@ pub fn print_server_updates(evts: &[ChildResult]) -> Vec { pub fn server_display(identity_dto: &ServerIdentityDTO, socket_addr: &str) -> String { match &identity_dto { ServerIdentityDTO::Both { name, .. } => { - format!("[server] [{}] http://{}", name, socket_addr) + format!("[server] [{name}] http://{socket_addr}") } ServerIdentityDTO::Address { .. } => { - format!("[server] http://{}", socket_addr) + format!("[server] http://{socket_addr}") } ServerIdentityDTO::Named { name } => { - format!("[server] [{}] http://{}", name, &socket_addr) + format!("[server] [{name}] http://{socket_addr}") } ServerIdentityDTO::Port { port } => { - format!("[server] [{}] http://{}", port, &socket_addr) + format!("[server] [{port}] http://{socket_addr}") } ServerIdentityDTO::PortNamed { name, .. } => { - format!("[server] [{}] http://{}", name, &socket_addr) + format!("[server] [{name}] http://{socket_addr}") } } } diff --git a/crates/bsnext_dto/src/startup_events.rs b/crates/bsnext_dto/src/startup_events.rs index e4adba14..a42d561e 100644 --- a/crates/bsnext_dto/src/startup_events.rs +++ b/crates/bsnext_dto/src/startup_events.rs @@ -24,19 +24,19 @@ impl OutputWriterTrait for StartupEvent { let n = miette::GraphicalReportHandler::new(); let mut inner = String::new(); n.render_report(&mut inner, bs_rules).expect("write?"); - writeln!(sink, "{}", inner)?; + writeln!(sink, "{inner}")?; } StartupError::InputError(err) => { - writeln!(sink, "{}", err)?; + writeln!(sink, "{err}")?; } StartupError::Other(e) => { - writeln!(sink, "{}", e)?; + writeln!(sink, "{e}")?; } StartupError::ServerError(e) => { - writeln!(sink, "{}", e)?; + writeln!(sink, "{e}")?; } StartupError::Any(e) => { - writeln!(sink, "{}", e)?; + writeln!(sink, "{e}")?; } } } diff --git a/crates/bsnext_html/src/lib.rs b/crates/bsnext_html/src/lib.rs index 7c497558..b4cab48d 100644 --- a/crates/bsnext_html/src/lib.rs +++ b/crates/bsnext_html/src/lib.rs @@ -71,7 +71,7 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result Some(format!("{} {}", name, content)), + (Some(name), Some(content)) => Some(format!("{name} {content}")), (Some(name), None) => Some(name.to_string()), _ => None, }; diff --git a/crates/bsnext_input/src/server_config.rs b/crates/bsnext_input/src/server_config.rs index d749ae8e..b49b60bd 100644 --- a/crates/bsnext_input/src/server_config.rs +++ b/crates/bsnext_input/src/server_config.rs @@ -95,7 +95,7 @@ where if value <= u16::MAX as u64 { Ok(value as u16) } else { - Err(E::custom(format!("port number out of range: {}", value))) + Err(E::custom(format!("port number out of range: {value}"))) } } @@ -105,7 +105,7 @@ where { value .parse::() - .map_err(|_| E::custom(format!("invalid port number: {}", value))) + .map_err(|_| E::custom(format!("invalid port number: {value}"))) } } diff --git a/crates/bsnext_md/src/md_writer.rs b/crates/bsnext_md/src/md_writer.rs index 694564d4..aae27632 100644 --- a/crates/bsnext_md/src/md_writer.rs +++ b/crates/bsnext_md/src/md_writer.rs @@ -79,15 +79,15 @@ fn route_to_markdown(kind: &RouteKind, path: &str) -> String { } fn fenced_input(code: &str) -> String { - format!("```yaml bslive_input\n{}```", code) + format!("```yaml bslive_input\n{code}```") } fn fenced_route(code: &str) -> String { - format!("```yaml bslive_route\n{}```", code) + format!("```yaml bslive_route\n{code}```") } fn fenced_playground(code: &str) -> String { - format!("```html playground\n{}\n```", code) + format!("```html playground\n{code}\n```") } fn fenced_body(lang: &str, code: &str) -> String { diff --git a/crates/bsnext_system/src/path_watchable.rs b/crates/bsnext_system/src/path_watchable.rs index 43c74076..e5949a21 100644 --- a/crates/bsnext_system/src/path_watchable.rs +++ b/crates/bsnext_system/src/path_watchable.rs @@ -25,7 +25,7 @@ impl Display for PathWatchable { .map(|x| format!("'{}'", x.display())) .collect::>() .join(", "); - write!(f, "PathWatchable::Server({})", lines)?; + write!(f, "PathWatchable::Server({lines})")?; } PathWatchable::Route(route) => { write!(f, "PathWatchable::Route('{}')", route.dir.display())?; @@ -37,7 +37,7 @@ impl Display for PathWatchable { .map(|x| format!("'{}'", x.display())) .collect::>() .join(", "); - write!(f, "PathWatchable::Any({})", lines)?; + write!(f, "PathWatchable::Any({lines})")?; } } Ok(()) diff --git a/crates/bsnext_system/src/route_watchable.rs b/crates/bsnext_system/src/route_watchable.rs index e1cd1105..a638cac8 100644 --- a/crates/bsnext_system/src/route_watchable.rs +++ b/crates/bsnext_system/src/route_watchable.rs @@ -1,10 +1,10 @@ use crate::server_watchable::to_task_list; use crate::task_list::TaskList; -use bsnext_input::route::{DirRoute, FilterKind, RouteKind, Spec}; +use bsnext_input::route::{DirRoute, FilterKind, RawRoute, RouteKind, Spec, SseOpts}; use bsnext_input::server_config::ServerIdentity; use bsnext_input::watch_opts::WatchOpts; use bsnext_input::Input; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone)] pub struct RouteWatchable { @@ -24,25 +24,43 @@ pub fn to_route_watchables(input: &Input) -> Vec { .routes .iter() .filter(|r| r.opts.watch.is_enabled()) - .filter_map(|r| match &r.kind { - RouteKind::Raw(_) => None, - RouteKind::Proxy(_) => None, - RouteKind::Dir(DirRoute { dir, .. }) => { - let spec = to_spec(&r.opts.watch); - let run = to_task_list(&spec); - Some(RouteWatchable { - server_identity: server_config.identity.clone(), - route_path: r.path.as_str().to_owned(), - dir: PathBuf::from(dir), - spec, - task_list: run, - }) - } + .filter_map(|r| { + let output = match &r.kind { + RouteKind::Raw(route) => maybe_source_file(route).map(PathBuf::from), + RouteKind::Dir(DirRoute { dir, .. }) => Some(PathBuf::from(dir)), + RouteKind::Proxy(_) => None, + }; + + // exit early if there's no related path + let pb = output?; + + let identity = server_config.identity.clone(); + + let spec = to_spec(&r.opts.watch); + let run = to_task_list(&spec); + let route_path = r.path.as_str().to_owned(); + + Some(RouteWatchable { + server_identity: identity, + route_path, + dir: pb, + spec, + task_list: run, + }) }) }) .collect() } +pub fn maybe_source_file(raw_route: &RawRoute) -> Option<&Path> { + match raw_route { + RawRoute::Sse { + sse: SseOpts { body, .. }, + } => body.split_once("file:").map(|(_, path)| Path::new(path)), + _ => None, + } +} + pub fn to_spec(wo: &WatchOpts) -> Spec { match wo { WatchOpts::Bool(enabled) if !*enabled => unreachable!("should be handled..."), diff --git a/crates/bsnext_system/src/watch/mod.rs b/crates/bsnext_system/src/watch/mod.rs index 7a96470f..4380fb29 100644 --- a/crates/bsnext_system/src/watch/mod.rs +++ b/crates/bsnext_system/src/watch/mod.rs @@ -35,7 +35,7 @@ impl From for MultiWatch { .map(ToOwned::to_owned) .enumerate() .map(move |(index, item)| { - let name = Some(format!("command:{}", index)); + let name = Some(format!("command:{index}")); let prefix = value.no_prefix.then_some(PrefixOpt::Bool(false)); RunOptItem::Sh(ShRunOptItem { sh: item, @@ -51,7 +51,7 @@ impl From for MultiWatch { .map(ToOwned::to_owned) .enumerate() .map(move |(index, item)| { - let name = Some(format!("initial:{}", index)); + let name = Some(format!("initial:{index}")); let prefix = value.no_prefix.then_some(PrefixOpt::Bool(false)); BeforeRunOptItem::Sh(ShRunOptItem { sh: item, diff --git a/examples/openai/bslive.yml b/examples/openai/bslive.yml index 3088aecd..ab01392f 100644 --- a/examples/openai/bslive.yml +++ b/examples/openai/bslive.yml @@ -16,4 +16,11 @@ servers: data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} data: [DONE] - + - name: openai-file + routes: + - path: / + dir: ./examples/openai + watch: false + - path: /openai/v1/chat/completions + sse: + body: file:./examples/openai/sse/01.txt diff --git a/examples/openai/sse/01.txt b/examples/openai/sse/01.txt index 83ebb702..6788cf24 100644 --- a/examples/openai/sse/01.txt +++ b/examples/openai/sse/01.txt @@ -4,5 +4,4 @@ data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.c data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} -data: {"id":"chatcmpl-98DH53xoEHQ7RhaBF9Djt0GoczbM2","object":"chat.completion.chunk","created":1711744679,"model":"gpt-3.5-turbo-0125","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} data: [DONE] diff --git a/tests/sse.spec.ts b/tests/sse.spec.ts index 5aaedb24..812288e1 100644 --- a/tests/sse.spec.ts +++ b/tests/sse.spec.ts @@ -1,4 +1,4 @@ -import { cli, test } from "./utils"; +import { cli, installMockHandler, readCalls, test } from "./utils"; import { expect } from "@playwright/test"; test.describe( @@ -23,5 +23,32 @@ test.describe( const html = await page.innerHTML("#output"); expect(html).toMatchSnapshot(); }); + test("server sent events - from file", async ({ page, bs }) => { + await page.goto(bs.named("openai-file", "/"), { + waitUntil: "networkidle", + }); + await expect(page.locator("pre")).toMatchAriaSnapshot( + `- code: "\\"\\" \\"This\\" \\" is\\" \\" a\\" \\" test\\" \\".\\""`, + ); + + await test.step("reloading on file change", async () => { + await page.evaluate(installMockHandler); + + bs.touch("examples/openai/sse/01.txt"); + // Wait for the reloadPage call + await page.waitForFunction(() => { + return window.__playwright?.calls?.length === 1; + }); + // Verify the calls + const calls = await page.evaluate(readCalls); + expect(calls).toStrictEqual([ + [ + { + kind: "reloadPage", + }, + ], + ]); + }); + }); }, ); From f8a61fff06902d312cf231f236d5a69c2f7a66c1 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Sun, 6 Jul 2025 15:10:57 +0100 Subject: [PATCH 04/13] experiment with simpler handler --- _bslive.yml | 32 ++-- crates/bsnext_core/src/handler_stack.rs | 174 +++++++++++++++++- crates/bsnext_core/src/raw_loader.rs | 2 +- crates/bsnext_core/src/serve_dir.rs | 6 +- crates/bsnext_guards/src/route_guard.rs | 12 +- crates/bsnext_input/src/lib.rs | 1 + crates/bsnext_input/src/playground.rs | 1 + crates/bsnext_input/src/route.rs | 3 + .../bsnext_input__when_guard__when_guard.snap | 13 ++ ...ext_input__when_guard__when_guard_has.snap | 13 ++ crates/bsnext_input/src/when_guard.rs | 56 ++++++ examples/basic/handler_stack.yml | 23 ++- 12 files changed, 300 insertions(+), 36 deletions(-) create mode 100644 crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap create mode 100644 crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap create mode 100644 crates/bsnext_input/src/when_guard.rs diff --git a/_bslive.yml b/_bslive.yml index 0cad30d1..afb29472 100644 --- a/_bslive.yml +++ b/_bslive.yml @@ -1,23 +1,15 @@ servers: - - port: 3000 - clients: - log: trace - watchers: - - dir: crates - run: - - sh: cargo build + - port: 3001 routes: - - path: / + - path: /shane + raw: "here2" + when: + query: + - has: 'abc' + - path: /shane + raw: "here" + when: + query: + - has: 'def' + - path: /shane dir: examples/basic/public - watch: - debounce: - ms: 300 - run: - - all: - - sh: echo 2 - - sh: exit 0 - - sh: sleep 1 && echo '->hehe' - name: '[another attempt--->]' - - seq: - - sleep 1 - - echo 'after?' \ No newline at end of file diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index 50dd2c70..13e1d344 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -4,15 +4,28 @@ use crate::optional_layers::optional_layers; use crate::raw_loader::serve_raw_one; use crate::runtime_ctx::RuntimeCtx; use crate::serve_dir::try_many_services_dir; +use axum::body::Body; +use axum::extract::{Request, State}; use axum::handler::Handler; -use axum::middleware::{from_fn, from_fn_with_state}; +use axum::middleware::{from_fn, from_fn_with_state, Next}; +use axum::response::{IntoResponse, Response}; use axum::routing::{any, any_service, get_service, MethodRouter}; -use axum::{Extension, Router}; +use axum::{middleware, Extension, Router}; +use bsnext_guards::route_guard::RouteGuard; use bsnext_input::route::{DirRoute, FallbackRoute, Opts, ProxyRoute, RawRoute, Route, RouteKind}; +use bsnext_input::when_guard::{HasGuard, WhenGuard}; +use http::request::Parts; +use http::{Method, StatusCode, Uri}; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use tower::ServiceExt; use tower_http::services::{ServeDir, ServeFile}; +#[derive(Debug, PartialEq)] +pub struct HandlerStackAlt { + pub routes: Vec, +} + #[derive(Debug, PartialEq)] pub enum HandlerStack { None, @@ -134,11 +147,34 @@ impl RouteMap { route_list.len() ); - let stack = routes_to_stack(route_list); - let path_router = stack_to_router(&path, stack, ctx); + // let stack = routes_to_stack(route_list); + // let path_router = stack_to_router(&path, stack, ctx); + let stack = routes_to_alt_stack(path.as_str(), route_list, ctx.clone()); + tracing::trace!("will merge router at path: `{path}`"); tracing::trace!("will merge router at path: `{path}`"); - router = router.merge(path_router); + router = router.merge(stack); + } + + router + } + + pub fn into_alt_router(self, ctx: &RuntimeCtx) -> Router { + let mut router = Router::new(); + + tracing::trace!("processing `{}` different routes", self.mapping.len()); + + for (path, route_list) in self.mapping { + tracing::trace!( + "processing path: `{}` with `{}` routes", + path, + route_list.len() + ); + + let stack = routes_to_alt_stack(path.as_str(), route_list, ctx.clone()); + tracing::trace!("will merge router at path: `{path}`"); + + router = router.merge(stack); } router @@ -235,6 +271,125 @@ pub fn routes_to_stack(routes: Vec) -> HandlerStack { routes.into_iter().fold(HandlerStack::None, append_stack) } +pub fn routes_to_alt_stack(path: &str, routes: Vec, ctx: RuntimeCtx) -> Router { + let r = Router::new().layer(from_fn_with_state((path.to_string(), routes, ctx), try_one)); + Router::new().nest_service(path, r) +} + +pub async fn try_one( + State((path, routes, ctx)): State<(String, Vec, RuntimeCtx)>, + uri: Uri, + req: Request, + next: Next, +) -> impl IntoResponse { + println!("before uri={uri}, path={path}"); + + let (original_parts, original_body) = req.into_parts(); + + for (index, route) in routes.into_iter().enumerate() { + let accept = route.when.as_ref().unwrap_or(&WhenGuard::Always); + + let will_serve = match accept { + WhenGuard::Always => true, + WhenGuard::Never => false, + WhenGuard::Query { query } => query + .iter() + .map(QueryHasGuard) + .any(|x| x.accept_req_parts(&original_parts)), + }; + + if !will_serve { + continue; + } + + let m_r = match route.kind { + RouteKind::Raw(raw) => any_service(serve_raw_one.with_state(raw.clone())), + RouteKind::Proxy(_) => todo!("RouteKind::Proxy"), + RouteKind::Dir(dir_route) => { + let svc = match &dir_route.base { + Some(base_dir) => { + tracing::trace!( + "combining root: `{}` with given path: `{}`", + base_dir.display(), + dir_route.dir + ); + ServeDir::new(base_dir.join(&dir_route.dir)) + } + None => { + let pb = PathBuf::from(&dir_route.dir); + if pb.is_absolute() { + tracing::trace!("no root given, using `{}` directly", dir_route.dir); + ServeDir::new(&dir_route.dir) + } else { + let joined = ctx.cwd().join(pb); + tracing::trace!( + "prepending the current directory to relative path {} {}", + ctx.cwd().display(), + joined.display() + ); + ServeDir::new(joined) + } + } + } + .append_index_html_on_directories(true); + get_service(svc) + } + }; + + let raw_out = optional_layers(m_r, &route.opts); + let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); + let result = raw_out.oneshot(req_clone).await; + + match result { + Ok(result) if result.status() == 404 => { + tracing::trace!(" ❌ not found at index {}, trying another", index); + continue; + } + Ok(result) if result.status() == 405 => { + tracing::trace!(" ❌ 405, trying another..."); + continue; + } + Ok(result) => { + println!("got a result {index}"); + tracing::trace!( + ?index, + " - ✅ a non-404 response was given {}", + result.status() + ); + return result.into_response(); + } + Err(e) => { + tracing::error!(?e); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + } + } + + let r = Request::from_parts(original_parts.clone(), original_body); + let res = next.run(r).await; + println!("after"); + res +} + +pub async fn serve_one(uri: Uri, req: Request) -> Response { + (StatusCode::NOT_FOUND, format!("No route for {uri}")).into_response() +} + +struct QueryHasGuard<'a>(pub &'a HasGuard); + +impl RouteGuard for QueryHasGuard<'_> { + fn accept_req_parts(&self, parts: &Parts) -> bool { + let Some(query) = parts.uri.query() else { + return false; + }; + match &self.0 { + HasGuard::Is { is } => is == query, + HasGuard::Has { has } => query.contains(has), + HasGuard::NotHas { not_has } => !query.contains(not_has), + } + } +} + pub fn stack_to_router(path: &str, stack: HandlerStack, ctx: &RuntimeCtx) -> Router { match stack { HandlerStack::None => unreachable!(), @@ -380,14 +535,15 @@ mod test { .unwrap(); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_router(&RuntimeCtx::default()); + let router = route_map.into_alt_router(&RuntimeCtx::default()); let request = Request::get("/styles.css").body(Body::empty())?; // Define the request // Make a one-shot request on the router let response = router.oneshot(request).await?; - let (_parts, body) = to_resp_parts_and_body(response).await; + let (parts, body) = to_resp_parts_and_body(response).await; + assert_eq!(parts.status.as_u16(), 200); assert_eq!(body, "body { background: red }"); } @@ -406,7 +562,7 @@ mod test { .unwrap(); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_router(&RuntimeCtx::default()); + let router = route_map.into_alt_router(&RuntimeCtx::default()); let raw_request = Request::get("/").body(Body::empty())?; let response = router.oneshot(raw_request).await?; let (_parts, body) = to_resp_parts_and_body(response).await; @@ -416,7 +572,7 @@ mod test { let cwd = cwd.ancestors().nth(2).unwrap(); let ctx = RuntimeCtx::new(cwd); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_router(&ctx); + let router = route_map.into_alt_router(&ctx); let dir_request = Request::get("/script.js").body(Body::empty())?; let response = router.oneshot(dir_request).await?; let (_parts, body) = to_resp_parts_and_body(response).await; diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index c171c633..4ed8fa1a 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -20,7 +20,7 @@ pub async fn serve_raw_one(uri: Uri, state: State, req: Request) -> Re raw_resp_for(uri, &state.0).await.into_response() } -async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { +pub async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { match route { RawRoute::Html { html } => { tracing::trace!("raw_resp_for will respond with HTML"); diff --git a/crates/bsnext_core/src/serve_dir.rs b/crates/bsnext_core/src/serve_dir.rs index 9e04433f..b9e78e33 100644 --- a/crates/bsnext_core/src/serve_dir.rs +++ b/crates/bsnext_core/src/serve_dir.rs @@ -14,11 +14,11 @@ pub async fn try_many_services_dir( ) -> impl IntoResponse { tracing::trace!(?uri, "{} services", router_list.len()); - let (a, b) = req.into_parts(); + let (original_parts, original_body) = req.into_parts(); for (index, method_router) in router_list.into_iter().enumerate() { // tracing::trace!(?path_buf); - let req_clone = Request::from_parts(a.clone(), Body::empty()); + let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); let result = method_router.oneshot(req_clone).await; match result { Ok(result) if result.status() == 404 => { @@ -44,7 +44,7 @@ pub async fn try_many_services_dir( } } tracing::trace!(" - REQUEST was NOT HANDLED BY SERVE_DIR (will be sent onwards)"); - let r = Request::from_parts(a.clone(), b); + let r = Request::from_parts(original_parts.clone(), original_body); next.run(r).await // StatusCode::NOT_FOUND.into_response() } diff --git a/crates/bsnext_guards/src/route_guard.rs b/crates/bsnext_guards/src/route_guard.rs index b5f10f9a..ef450d4d 100644 --- a/crates/bsnext_guards/src/route_guard.rs +++ b/crates/bsnext_guards/src/route_guard.rs @@ -1,7 +1,15 @@ use axum::extract::Request; +use http::request::Parts; use http::Response; pub trait RouteGuard { - fn accept_req(&self, req: &Request) -> bool; - fn accept_res(&self, res: &Response) -> bool; + fn accept_req(&self, _req: &Request) -> bool { + true + } + fn accept_req_parts(&self, _parts: &Parts) -> bool { + true + } + fn accept_res(&self, _res: &Response) -> bool { + true + } } diff --git a/crates/bsnext_input/src/lib.rs b/crates/bsnext_input/src/lib.rs index 2f67e4fe..1606d610 100644 --- a/crates/bsnext_input/src/lib.rs +++ b/crates/bsnext_input/src/lib.rs @@ -24,6 +24,7 @@ pub mod target; #[cfg(test)] pub mod watch_opt_test; pub mod watch_opts; +pub mod when_guard; pub mod yml; #[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)] diff --git a/crates/bsnext_input/src/playground.rs b/crates/bsnext_input/src/playground.rs index 18bac1b2..fc2ce6b3 100644 --- a/crates/bsnext_input/src/playground.rs +++ b/crates/bsnext_input/src/playground.rs @@ -44,6 +44,7 @@ impl Playground { kind: RouteKind::new_html(FALLBACK_HTML), opts: Default::default(), }), + when: Default::default(), }; let js_route = Route { path: js, diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index 757a7db2..cb055e18 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -1,6 +1,7 @@ use crate::path_def::PathDef; use crate::route_cli::RouteCli; use crate::watch_opts::WatchOpts; +use crate::when_guard::WhenGuard; use bsnext_resp::cache_opts::CacheOpts; use bsnext_resp::inject_opts::InjectOpts; use matchit::InsertError; @@ -20,6 +21,7 @@ pub struct Route { #[serde(flatten)] pub opts: Opts, pub fallback: Option, + pub when: Option, } #[derive(Debug, PartialEq, thiserror::Error)] @@ -65,6 +67,7 @@ impl Default for Route { ..Default::default() }, fallback: Default::default(), + when: Default::default(), } } } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap new file mode 100644 index 00000000..56f253cc --- /dev/null +++ b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap @@ -0,0 +1,13 @@ +--- +source: crates/bsnext_input/src/when_guard.rs +expression: "&when" +--- +A { + when: Query { + query: [ + NotHas { + not_has: "here", + }, + ], + }, +} diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap new file mode 100644 index 00000000..7ae33342 --- /dev/null +++ b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap @@ -0,0 +1,13 @@ +--- +source: crates/bsnext_input/src/when_guard.rs +expression: "&when" +--- +A { + when: Query { + query: [ + Has { + has: "here", + }, + ], + }, +} diff --git a/crates/bsnext_input/src/when_guard.rs b/crates/bsnext_input/src/when_guard.rs new file mode 100644 index 00000000..0394cb3c --- /dev/null +++ b/crates/bsnext_input/src/when_guard.rs @@ -0,0 +1,56 @@ +#[derive(Debug, Default, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum WhenGuard { + #[default] + Always, + Never, + Query { + query: Vec, + }, +} + +#[test] +fn test_when_guard() { + use insta::assert_debug_snapshot; + #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] + struct A { + when: WhenGuard, + } + let input = r#" + when: + query: + - not.has: "here" + "#; + let when: A = serde_yaml::from_str(input).expect("test"); + assert_debug_snapshot!(&when); +} + +#[test] +fn test_when_guard_has() { + use insta::assert_debug_snapshot; + #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] + struct A { + when: WhenGuard, + } + let input = r#" + when: + query: + - has: "here" + "#; + let when: A = serde_yaml::from_str(input).expect("test"); + assert_debug_snapshot!(&when); +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum HasGuard { + /// A direct, exact match + Is { is: String }, + /// Contains the substring + Has { has: String }, + /// Contains the substring + NotHas { + #[serde(rename = "not.has")] + not_has: String, + }, +} diff --git a/examples/basic/handler_stack.yml b/examples/basic/handler_stack.yml index 1be90ecc..ef0c259d 100644 --- a/examples/basic/handler_stack.yml +++ b/examples/basic/handler_stack.yml @@ -3,6 +3,12 @@ servers: routes: - path: /styles.css raw: 'body { background: red }' + - path: /styles.css + raw: 'body { background: blue }' + - path: /kittens.css + raw: 'body { background: grey }' + - path: /other + raw: 'body { background: pink }' - name: "raw+dir" routes: - path: / @@ -27,4 +33,19 @@ servers: routes: - path: /script.js raw: 'console.log("hello world!")' - cors: true \ No newline at end of file + cors: true + - name: "multiple raw routes" + port: 3000 + routes: + - path: /script.js + raw: "hello from 1st" + when: + query: + - has: 'include' + - path: /script.js + raw: "hello from 2nd" + when: + query: + - is: '2nd=please' + - path: /script.js + dir: examples/basic/public/script.js \ No newline at end of file From 2f947ca0dcaff215453871f5e76ed1d82cce7938 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Mon, 7 Jul 2025 09:37:44 +0100 Subject: [PATCH 05/13] working example --- Cargo.lock | 1 + _bslive.yml | 20 + crates/bsnext_core/Cargo.toml | 1 + crates/bsnext_core/src/handler_stack.rs | 524 +++++----------------- crates/bsnext_core/src/handlers/proxy.rs | 3 + crates/bsnext_core/src/optional_layers.rs | 50 ++- crates/bsnext_core/src/raw_loader.rs | 2 +- crates/bsnext_core/src/serve_dir.rs | 56 +-- crates/bsnext_core/tests/inject_tests.rs | 8 +- crates/bsnext_resp/src/inject_opts.rs | 1 + 10 files changed, 207 insertions(+), 459 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d10a8be..ac11ebf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,6 +514,7 @@ dependencies = [ "hyper-tls", "hyper-util", "insta", + "log", "mime_guess", "serde", "serde_json", diff --git a/_bslive.yml b/_bslive.yml index afb29472..f17f7a49 100644 --- a/_bslive.yml +++ b/_bslive.yml @@ -1,6 +1,17 @@ servers: + - port: 3002 + routes: + - path: /api + json: [ 1,2 ] + when: + query: + - has: 'mock' + - path: /api + proxy: https://duckduckgo.github.io - port: 3001 routes: + - path: /a/really/:long/path + dir: examples/basic/public - path: /shane raw: "here2" when: @@ -13,3 +24,12 @@ servers: - has: 'def' - path: /shane dir: examples/basic/public + - path: /shane + dir: examples/html + - path: / + html: "4" + when: + query: + - is: 'a=b' + - path: / + html: "3" diff --git a/crates/bsnext_core/Cargo.toml b/crates/bsnext_core/Cargo.toml index 3156a89a..606ffb6e 100644 --- a/crates/bsnext_core/Cargo.toml +++ b/crates/bsnext_core/Cargo.toml @@ -45,6 +45,7 @@ serde_yaml = { workspace = true } serde_json = { workspace = true } bytes = { workspace = true } http = { workspace = true } +log = "0.4.21" [[example]] name = "abc" diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index 13e1d344..7a87c9ae 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -1,121 +1,42 @@ use crate::handlers::proxy::{proxy_handler, ProxyConfig}; -use crate::not_found::not_found_service::not_found_loader; -use crate::optional_layers::optional_layers; +use crate::optional_layers::{optional_layers, optional_layers_lol}; use crate::raw_loader::serve_raw_one; use crate::runtime_ctx::RuntimeCtx; -use crate::serve_dir::try_many_services_dir; use axum::body::Body; use axum::extract::{Request, State}; use axum::handler::Handler; use axum::middleware::{from_fn, from_fn_with_state, Next}; -use axum::response::{IntoResponse, Response}; +use axum::response::IntoResponse; use axum::routing::{any, any_service, get_service, MethodRouter}; use axum::{middleware, Extension, Router}; use bsnext_guards::route_guard::RouteGuard; -use bsnext_input::route::{DirRoute, FallbackRoute, Opts, ProxyRoute, RawRoute, Route, RouteKind}; +use bsnext_input::route::{Route, RouteKind}; use bsnext_input::when_guard::{HasGuard, WhenGuard}; use http::request::Parts; use http::{Method, StatusCode, Uri}; use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use tower::ServiceExt; -use tower_http::services::{ServeDir, ServeFile}; - -#[derive(Debug, PartialEq)] -pub struct HandlerStackAlt { - pub routes: Vec, -} - -#[derive(Debug, PartialEq)] -pub enum HandlerStack { - None, - // todo: make this a separate thing - Raw(RawRouteOpts), - RawAndDirs { - raw: RawRouteOpts, - dirs: Vec, - }, - Dirs(Vec), - Proxy { - proxy: ProxyRoute, - opts: Opts, - }, - DirsProxy { - dirs: Vec, - proxy: ProxyRoute, - opts: Opts, - }, -} - -#[derive(Debug, PartialEq)] -pub struct DirRouteOpts { - dir_route: DirRoute, - opts: Opts, - fallback_route: Option, -} - -#[derive(Debug, PartialEq)] -pub struct RawRouteOpts { - raw_route: RawRoute, - opts: Opts, -} - -impl DirRouteOpts { - pub fn as_serve_dir(&self, cwd: &Path) -> ServeDir { - match &self.dir_route.base { - Some(base_dir) => { - tracing::trace!( - "combining root: `{}` with given path: `{}`", - base_dir.display(), - self.dir_route.dir - ); - ServeDir::new(base_dir.join(&self.dir_route.dir)) - } - None => { - let pb = PathBuf::from(&self.dir_route.dir); - if pb.is_absolute() { - tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); - ServeDir::new(&self.dir_route.dir) - } else { - let joined = cwd.join(pb); - tracing::trace!( - "prepending the current directory to relative path {} {}", - cwd.display(), - joined.display() - ); - ServeDir::new(joined) - } - } - } - .append_index_html_on_directories(true) - } - pub fn as_serve_file(&self) -> ServeFile { - match &self.dir_route.base { - Some(base_dir) => { - tracing::trace!( - "combining root: `{}` with given path: `{}`", - base_dir.display(), - self.dir_route.dir - ); - ServeFile::new(base_dir.join(&self.dir_route.dir)) - } - None => { - tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); - ServeFile::new(&self.dir_route.dir) - } - } - } -} - -impl DirRouteOpts { - fn new(p0: DirRoute, p1: Opts, fallback_route: Option) -> DirRouteOpts { - Self { - dir_route: p0, - opts: p1, - fallback_route, - } - } -} +use tower_http::services::ServeDir; +use tracing::{trace, trace_span}; +// impl DirRouteOpts { +// pub fn as_serve_file(&self) -> ServeFile { +// match &self.dir_route.base { +// Some(base_dir) => { +// tracing::trace!( +// "combining root: `{}` with given path: `{}`", +// base_dir.display(), +// self.dir_route.dir +// ); +// ServeFile::new(base_dir.join(&self.dir_route.dir)) +// } +// None => { +// tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); +// ServeFile::new(&self.dir_route.dir) +// } +// } +// } +// } pub struct RouteMap { pub mapping: HashMap>, @@ -135,45 +56,20 @@ impl RouteMap { } } + #[tracing::instrument(skip(self))] pub fn into_router(self, ctx: &RuntimeCtx) -> Router { let mut router = Router::new(); - tracing::trace!("processing `{}` different routes", self.mapping.len()); + trace!("processing `{}` different routes", self.mapping.len()); - for (path, route_list) in self.mapping { - tracing::trace!( - "processing path: `{}` with `{}` routes", - path, - route_list.len() - ); + for (index, (path, route_list)) in self.mapping.into_iter().enumerate() { + trace!(?index, ?path, "creating for `{}` routes", route_list.len()); // let stack = routes_to_stack(route_list); // let path_router = stack_to_router(&path, stack, ctx); - let stack = routes_to_alt_stack(path.as_str(), route_list, ctx.clone()); - tracing::trace!("will merge router at path: `{path}`"); - - tracing::trace!("will merge router at path: `{path}`"); - router = router.merge(stack); - } - - router - } - - pub fn into_alt_router(self, ctx: &RuntimeCtx) -> Router { - let mut router = Router::new(); - - tracing::trace!("processing `{}` different routes", self.mapping.len()); - - for (path, route_list) in self.mapping { - tracing::trace!( - "processing path: `{}` with `{}` routes", - path, - route_list.len() - ); - - let stack = routes_to_alt_stack(path.as_str(), route_list, ctx.clone()); - tracing::trace!("will merge router at path: `{path}`"); + let stack = route_list_for_path(path.as_str(), route_list, ctx.clone()); + trace!(?index, ?path, "will merge router"); router = router.merge(stack); } @@ -181,115 +77,48 @@ impl RouteMap { } } -pub fn append_stack(state: HandlerStack, route: Route) -> HandlerStack { - match state { - HandlerStack::None => match route.kind { - RouteKind::Raw(raw_route) => HandlerStack::Raw(RawRouteOpts { - raw_route, - opts: route.opts, - }), - RouteKind::Proxy(new_proxy_route) => HandlerStack::Proxy { - proxy: new_proxy_route, - opts: route.opts, - }, - RouteKind::Dir(dir) => { - HandlerStack::Dirs(vec![DirRouteOpts::new(dir, route.opts, route.fallback)]) - } - }, - HandlerStack::Raw(RawRouteOpts { raw_route, opts }) => match route.kind { - // if a second 'raw' is seen, just use it, discarding the previous - RouteKind::Raw(raw_route) => HandlerStack::Raw(RawRouteOpts { - raw_route, - opts: route.opts, - }), - RouteKind::Dir(dir) => HandlerStack::RawAndDirs { - dirs: vec![DirRouteOpts::new(dir, route.opts, None)], - raw: RawRouteOpts { raw_route, opts }, - }, - // 'raw' handlers never get updated - _ => HandlerStack::Raw(RawRouteOpts { raw_route, opts }), - }, - HandlerStack::RawAndDirs { .. } => { - todo!("support RawAndDirs") - } - HandlerStack::Dirs(mut dirs) => match route.kind { - RouteKind::Dir(next_dir) => { - dirs.push(DirRouteOpts::new(next_dir, route.opts, route.fallback)); - HandlerStack::Dirs(dirs) - } - RouteKind::Proxy(proxy) => HandlerStack::DirsProxy { - dirs, - proxy, - opts: route.opts, - }, - _ => HandlerStack::Dirs(dirs), - }, - HandlerStack::Proxy { proxy, opts } => match route.kind { - RouteKind::Dir(dir) => HandlerStack::DirsProxy { - dirs: vec![DirRouteOpts::new(dir, route.opts, route.fallback)], - proxy, - opts, - }, - _ => HandlerStack::Proxy { proxy, opts }, - }, - HandlerStack::DirsProxy { - mut dirs, - proxy, - opts, - } => match route.kind { - RouteKind::Dir(dir) => { - dirs.push(DirRouteOpts::new(dir, route.opts, route.fallback)); - HandlerStack::DirsProxy { dirs, proxy, opts } - } - // todo(alpha): how to handle multiple proxies? should it just override for now? - _ => HandlerStack::DirsProxy { dirs, proxy, opts }, - }, - } +#[tracing::instrument(skip_all)] +pub fn route_list_for_path(path: &str, routes: Vec, ctx: RuntimeCtx) -> Router { + // let r1 = from_fn_with_state((path.to_string(), routes, ctx), try_one); + let svc = any_service(try_one.with_state((path.to_string(), routes, ctx))); + // if path.contains("{") { + // tracing::trace!(?path, "route"); + // return Router::new().route(path, svc); + // } + tracing::trace!("nest_service"); + Router::new() + .nest_service(path, svc) + .layer(from_fn(uri_extension)) } - -pub fn fallback_to_layered_method_router(route: FallbackRoute) -> MethodRouter { - match route.kind { - RouteKind::Raw(raw_route) => { - let svc = any_service(serve_raw_one.with_state(raw_route)); - optional_layers(svc, &route.opts) - } - RouteKind::Proxy(_new_proxy_route) => { - // todo(alpha): make a decision proxy as a fallback - todo!("add support for RouteKind::Proxy as a fallback?") - } - RouteKind::Dir(dir) => { - tracing::trace!("creating fallback for dir {:?}", dir); - let item = DirRouteOpts::new(dir, route.opts, None); - let serve_dir_service = item.as_serve_file(); - let service = get_service(serve_dir_service); - optional_layers(service, &item.opts) - } - } -} - -pub fn routes_to_stack(routes: Vec) -> HandlerStack { - routes.into_iter().fold(HandlerStack::None, append_stack) -} - -pub fn routes_to_alt_stack(path: &str, routes: Vec, ctx: RuntimeCtx) -> Router { - let r = Router::new().layer(from_fn_with_state((path.to_string(), routes, ctx), try_one)); - Router::new().nest_service(path, r) +#[derive(Debug, Clone)] +pub struct OuterUri(pub Uri); +pub async fn uri_extension(uri: Uri, mut req: Request, next: Next) -> impl IntoResponse { + req.extensions_mut().insert(OuterUri(uri)); + next.run(req).await } pub async fn try_one( State((path, routes, ctx)): State<(String, Vec, RuntimeCtx)>, - uri: Uri, + Extension(uri): Extension, + local_uri: Uri, req: Request, - next: Next, ) -> impl IntoResponse { - println!("before uri={uri}, path={path}"); + let span = trace_span!("try_one", uri = ?uri.0, path = path, local_uri = ?local_uri); + let _g = span.enter(); - let (original_parts, original_body) = req.into_parts(); + let (mut original_parts, original_body) = req.into_parts(); + // original_parts.uri = uri.0; + trace!(?original_parts); + trace!("will try {} routes", routes.len()); for (index, route) in routes.into_iter().enumerate() { + let span = trace_span!("index", index = index); + let _g = span.enter(); + let accept = route.when.as_ref().unwrap_or(&WhenGuard::Always); + trace!(?accept); - let will_serve = match accept { + let can_serve = match accept { WhenGuard::Always => true, WhenGuard::Never => false, WhenGuard::Query { query } => query @@ -298,60 +127,39 @@ pub async fn try_one( .any(|x| x.accept_req_parts(&original_parts)), }; - if !will_serve { + trace!(?can_serve); + + if !can_serve { continue; } - let m_r = match route.kind { - RouteKind::Raw(raw) => any_service(serve_raw_one.with_state(raw.clone())), - RouteKind::Proxy(_) => todo!("RouteKind::Proxy"), - RouteKind::Dir(dir_route) => { - let svc = match &dir_route.base { - Some(base_dir) => { - tracing::trace!( - "combining root: `{}` with given path: `{}`", - base_dir.display(), - dir_route.dir - ); - ServeDir::new(base_dir.join(&dir_route.dir)) - } - None => { - let pb = PathBuf::from(&dir_route.dir); - if pb.is_absolute() { - tracing::trace!("no root given, using `{}` directly", dir_route.dir); - ServeDir::new(&dir_route.dir) - } else { - let joined = ctx.cwd().join(pb); - tracing::trace!( - "prepending the current directory to relative path {} {}", - ctx.cwd().display(), - joined.display() - ); - ServeDir::new(joined) - } - } - } - .append_index_html_on_directories(true); - get_service(svc) + trace!(?original_parts); + + let result = match to_method_router(&path, &route, &ctx) { + Either::Left(router) => { + let raw_out = optional_layers(router, &route.opts); + let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); + raw_out.oneshot(req_clone).await + } + Either::Right(method_router) => { + let raw_out = optional_layers_lol(method_router, &route.opts); + let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); + raw_out.oneshot(req_clone).await } }; - let raw_out = optional_layers(m_r, &route.opts); - let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); - let result = raw_out.oneshot(req_clone).await; - match result { Ok(result) if result.status() == 404 => { - tracing::trace!(" ❌ not found at index {}, trying another", index); + trace!(" ❌ not found at index {}, trying another", index); continue; } Ok(result) if result.status() == 405 => { - tracing::trace!(" ❌ 405, trying another..."); + trace!(" ❌ 405, trying another..."); continue; } Ok(result) => { println!("got a result {index}"); - tracing::trace!( + trace!( ?index, " - ✅ a non-404 response was given {}", result.status() @@ -365,14 +173,51 @@ pub async fn try_one( } } - let r = Request::from_parts(original_parts.clone(), original_body); - let res = next.run(r).await; - println!("after"); - res + StatusCode::NOT_FOUND.into_response() } -pub async fn serve_one(uri: Uri, req: Request) -> Response { - (StatusCode::NOT_FOUND, format!("No route for {uri}")).into_response() +enum Either { + Left(Router), + Right(MethodRouter), +} +fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> Either { + match &route.kind { + RouteKind::Raw(raw) => Either::Right(any_service(serve_raw_one.with_state(raw.clone()))), + RouteKind::Proxy(proxy) => { + let proxy_config = ProxyConfig { + target: proxy.proxy.clone(), + path: path.to_string(), + }; + let proxy_with_decompression = proxy_handler.layer(Extension(proxy_config.clone())); + let as_service = any(proxy_with_decompression); + Either::Left(Router::new().nest_service(path, as_service)) + } + RouteKind::Dir(dir_route) => { + let svc = match &dir_route.base { + Some(base_dir) => { + tracing::trace!( + "combining root: `{}` with given path: `{}`", + base_dir.display(), + dir_route.dir + ); + ServeDir::new(base_dir.join(&dir_route.dir)) + } + None => { + let pb = PathBuf::from(&dir_route.dir); + if pb.is_absolute() { + trace!("no root given, using `{}` directly", dir_route.dir); + ServeDir::new(&dir_route.dir) + } else { + let joined = ctx.cwd().join(pb); + trace!(?joined, "serving"); + ServeDir::new(joined) + } + } + } + .append_index_html_on_directories(true); + Either::Right(get_service(svc)) + } + } } struct QueryHasGuard<'a>(pub &'a HasGuard); @@ -390,76 +235,6 @@ impl RouteGuard for QueryHasGuard<'_> { } } -pub fn stack_to_router(path: &str, stack: HandlerStack, ctx: &RuntimeCtx) -> Router { - match stack { - HandlerStack::None => unreachable!(), - HandlerStack::Raw(RawRouteOpts { raw_route, opts }) => { - let svc = any_service(serve_raw_one.with_state(raw_route)); - let out = optional_layers(svc, &opts); - Router::new().route_service(path, out) - } - HandlerStack::RawAndDirs { - dirs, - raw: RawRouteOpts { raw_route, opts }, - } => { - let svc = any_service(serve_raw_one.with_state(raw_route)); - let raw_out = optional_layers(svc, &opts); - let service = serve_dir_layer(&dirs, Router::new(), ctx); - Router::new().route(path, raw_out).fallback_service(service) - } - HandlerStack::Dirs(dirs) => { - let service = serve_dir_layer(&dirs, Router::new(), ctx); - Router::new() - .nest_service(path, service) - .layer(from_fn(not_found_loader)) - } - HandlerStack::Proxy { proxy, opts } => { - let proxy_config = ProxyConfig { - target: proxy.proxy.clone(), - path: path.to_string(), - }; - - let proxy_with_decompression = proxy_handler.layer(Extension(proxy_config.clone())); - let as_service = any(proxy_with_decompression); - - Router::new().nest_service(path, optional_layers(as_service, &opts)) - } - HandlerStack::DirsProxy { dirs, proxy, opts } => { - let proxy_router = stack_to_router(path, HandlerStack::Proxy { proxy, opts }, ctx); - let r1 = serve_dir_layer(&dirs, Router::new().fallback_service(proxy_router), ctx); - Router::new().nest_service(path, r1) - } - } -} - -fn serve_dir_layer( - dir_list_with_opts: &[DirRouteOpts], - initial: Router, - ctx: &RuntimeCtx, -) -> Router { - let serve_dir_items = dir_list_with_opts - .iter() - .map(|dir_route| match &dir_route.fallback_route { - None => { - let serve_dir_service = dir_route.as_serve_dir(ctx.cwd()); - let service = get_service(serve_dir_service); - optional_layers(service, &dir_route.opts) - } - Some(fallback) => { - let stack = fallback_to_layered_method_router(fallback.clone()); - let serve_dir_service = dir_route - .as_serve_dir(ctx.cwd()) - .fallback(stack) - .call_fallback_on_method_not_allowed(true); - let service = any_service(serve_dir_service); - optional_layers(service, &dir_route.opts) - } - }) - .collect::>(); - - initial.layer(from_fn_with_state(serve_dir_items, try_many_services_dir)) -} - #[cfg(test)] mod test { use super::*; @@ -469,59 +244,8 @@ mod test { use bsnext_input::Input; use http::Request; - use insta::assert_debug_snapshot; - use tower::ServiceExt; - #[test] - fn test_handler_stack_01() -> anyhow::Result<()> { - let yaml = include_str!("../../../examples/basic/handler_stack.yml"); - let input = serde_yaml::from_str::(&yaml)?; - let first = input - .servers - .iter() - .find(|x| x.identity.is_named("raw")) - .map(ToOwned::to_owned) - .unwrap(); - - let actual = routes_to_stack(first.routes); - assert_debug_snapshot!(actual); - Ok(()) - } - - #[test] - fn test_handler_stack_02() -> anyhow::Result<()> { - let yaml = include_str!("../../../examples/basic/handler_stack.yml"); - let input = serde_yaml::from_str::(&yaml)?; - let first = input - .servers - .iter() - .find(|x| x.identity.is_named("2dirs+proxy")) - .map(ToOwned::to_owned) - .unwrap(); - - let actual = routes_to_stack(first.routes); - - assert_debug_snapshot!(actual); - Ok(()) - } - #[test] - fn test_handler_stack_03() -> anyhow::Result<()> { - let yaml = include_str!("../../../examples/basic/handler_stack.yml"); - let input = serde_yaml::from_str::(&yaml)?; - let first = input - .servers - .iter() - .find(|s| s.identity.is_named("raw+opts")) - .map(ToOwned::to_owned) - .unwrap(); - - let actual = routes_to_stack(first.routes); - - assert_debug_snapshot!(actual); - Ok(()) - } - #[tokio::test] async fn test_routes_to_router() -> anyhow::Result<()> { let yaml = include_str!("../../../examples/basic/handler_stack.yml"); @@ -535,7 +259,7 @@ mod test { .unwrap(); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_alt_router(&RuntimeCtx::default()); + let router = route_map.into_router(&RuntimeCtx::default()); let request = Request::get("/styles.css").body(Body::empty())?; // Define the request @@ -562,7 +286,7 @@ mod test { .unwrap(); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_alt_router(&RuntimeCtx::default()); + let router = route_map.into_router(&RuntimeCtx::default()); let raw_request = Request::get("/").body(Body::empty())?; let response = router.oneshot(raw_request).await?; let (_parts, body) = to_resp_parts_and_body(response).await; @@ -572,7 +296,7 @@ mod test { let cwd = cwd.ancestors().nth(2).unwrap(); let ctx = RuntimeCtx::new(cwd); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_alt_router(&ctx); + let router = route_map.into_router(&ctx); let dir_request = Request::get("/script.js").body(Body::empty())?; let response = router.oneshot(dir_request).await?; let (_parts, body) = to_resp_parts_and_body(response).await; diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index 21ed68f2..e69c4db0 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -112,6 +112,7 @@ pub async fn proxy_handler( .into_response()) } +#[tracing::instrument(skip_all)] async fn serve_one_proxy_req(req: Request) -> Response { tracing::trace!("serve_one_proxy_req {}", req.uri().to_string()); let client = { @@ -120,12 +121,14 @@ async fn serve_one_proxy_req(req: Request) -> Response { .expect("must have a client, move this to an extractor?") }; tracing::trace!(req.headers = ?req.headers()); + tracing::trace!(req.method = ?req.method()); let client_c = client.clone(); let resp = client_c .request(req) .await .map_err(|_| StatusCode::BAD_REQUEST) .into_response(); + tracing::trace!(resp.status = resp.status().as_u16()); tracing::trace!(resp.headers = ?resp.headers()); resp } diff --git a/crates/bsnext_core/src/optional_layers.rs b/crates/bsnext_core/src/optional_layers.rs index 4517245f..b6232eb2 100644 --- a/crates/bsnext_core/src/optional_layers.rs +++ b/crates/bsnext_core/src/optional_layers.rs @@ -3,7 +3,7 @@ use axum::extract::{Request, State}; use axum::middleware::{map_response_with_state, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::MethodRouter; -use axum::{middleware, Extension}; +use axum::{middleware, Extension, Router}; use axum_extra::middleware::option_layer; use bsnext_input::route::{CompType, CompressionOpts, CorsOpts, DelayKind, DelayOpts, Opts}; use bsnext_resp::{response_modifications_layer, InjectHandling}; @@ -17,7 +17,53 @@ use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::cors::CorsLayer; -pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter { +pub fn optional_layers(app: Router, opts: &Opts) -> Router { + let mut app = app; + let cors_enabled_layer = opts + .cors + .as_ref() + .filter(|v| **v == CorsOpts::Cors(true)) + .map(|_| CorsLayer::permissive()); + + let compression_layer = opts.compression.as_ref().and_then(comp_opts_to_layer); + + let delay_enabled_layer = opts + .delay + .as_ref() + .map(|delay| middleware::from_fn_with_state(delay.clone(), delay_mw)); + + let injections = opts.inject.as_injections(); + + let set_response_headers_layer = opts + .headers + .as_ref() + .map(|headers| map_response_with_state(headers.clone(), set_resp_headers_from_strs)); + + let headers = opts.cache.as_headers(); + let prevent_cache_headers_layer = map_response_with_state(headers, set_resp_headers); + + let optional_stack = ServiceBuilder::new() + .layer(middleware::from_fn(dynamic_query_params_handler)) + .layer(middleware::from_fn(response_modifications_layer)) + .layer(prevent_cache_headers_layer) + .layer(option_layer(set_response_headers_layer)) + .layer(option_layer(cors_enabled_layer)) + .layer(option_layer(delay_enabled_layer)); + + app = app.layer(optional_stack); + + // The compression layer has a different type, so needs to apply outside the optional stack + // this essentially wrapping everything. + // I'm sure there's a cleaner way... + if let Some(cl) = compression_layer { + app = app.layer(cl); + } + + app.layer(Extension(InjectHandling { + items: injections.items, + })) +} +pub fn optional_layers_lol(app: MethodRouter, opts: &Opts) -> MethodRouter { let mut app = app; let cors_enabled_layer = opts .cors diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index 4ed8fa1a..93b88d3b 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -114,7 +114,7 @@ mod raw_test { // Make a one-shot request on the router let response = router.oneshot(request).await?; let (_parts, body) = to_resp_parts_and_body(response).await; - assert_eq!(body, "

Welcome to Route 1.1

"); + assert_eq!(body, "

Welcome to Route 1

"); } { diff --git a/crates/bsnext_core/src/serve_dir.rs b/crates/bsnext_core/src/serve_dir.rs index b9e78e33..f7dde170 100644 --- a/crates/bsnext_core/src/serve_dir.rs +++ b/crates/bsnext_core/src/serve_dir.rs @@ -1,63 +1,15 @@ -use axum::body::Body; -use axum::extract::{Request, State}; -use axum::middleware::Next; -use axum::response::IntoResponse; -use axum::routing::MethodRouter; -use http::{StatusCode, Uri}; -use tower::ServiceExt; - -pub async fn try_many_services_dir( - State(router_list): State>, - uri: Uri, - req: Request, - next: Next, -) -> impl IntoResponse { - tracing::trace!(?uri, "{} services", router_list.len()); - - let (original_parts, original_body) = req.into_parts(); - - for (index, method_router) in router_list.into_iter().enumerate() { - // tracing::trace!(?path_buf); - let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); - let result = method_router.oneshot(req_clone).await; - match result { - Ok(result) if result.status() == 404 => { - tracing::trace!(" ❌ not found at index {}, trying another", index,); - continue; - } - Ok(result) if result.status() == 405 => { - tracing::trace!(" ❌ 405, trying another..."); - continue; - } - Ok(result) => { - tracing::trace!( - ?index, - " - ✅ a non-404 response was given {}", - result.status() - ); - return result.into_response(); - } - Err(e) => { - tracing::error!(?e); - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - } - } - tracing::trace!(" - REQUEST was NOT HANDLED BY SERVE_DIR (will be sent onwards)"); - let r = Request::from_parts(original_parts.clone(), original_body); - next.run(r).await - // StatusCode::NOT_FOUND.into_response() -} - #[cfg(test)] mod test { - use super::*; + use crate::handler_stack::RouteMap; use crate::server::router::common::to_resp_parts_and_body; use crate::runtime_ctx::RuntimeCtx; + use axum::body::Body; + use axum::extract::Request; use bsnext_input::route::Route; use std::env::current_dir; + use tower::ServiceExt; #[tokio::test] async fn test() -> anyhow::Result<()> { diff --git a/crates/bsnext_core/tests/inject_tests.rs b/crates/bsnext_core/tests/inject_tests.rs index 3154c3a7..d8cdf273 100644 --- a/crates/bsnext_core/tests/inject_tests.rs +++ b/crates/bsnext_core/tests/inject_tests.rs @@ -31,7 +31,7 @@ servers: only: '/*.css' "#; let input: Input = serde_yaml::from_str(input).expect("input"); - let config: ServerConfig = input.servers.get(0).expect("fiirst").to_owned(); + let config: ServerConfig = input.servers.get(0).expect("first").to_owned(); let state = into_state(config).into(); state } @@ -43,9 +43,9 @@ async fn test_handlers_raw_inject() -> Result<(), anyhow::Error> { assert_eq!(body, "body{}lol"); // with param - let state = yaml_server_01(); - let body = req_to_body(state, "/styles.css?oops=does_not_affect").await; - assert_eq!(body, "body{}lol"); + // let state = yaml_server_01(); + // let body = req_to_body(state, "/styles.css?oops=does_not_affect").await; + // assert_eq!(body, "body{}lol"); Ok(()) } diff --git a/crates/bsnext_resp/src/inject_opts.rs b/crates/bsnext_resp/src/inject_opts.rs index 64dbf58a..aafe2ef9 100644 --- a/crates/bsnext_resp/src/inject_opts.rs +++ b/crates/bsnext_resp/src/inject_opts.rs @@ -75,6 +75,7 @@ impl RouteGuard for InjectionItem { None => true, Some(ml) => ml.test_uri(req.uri()), }; + println!("{}", uri_is_allowed); if uri_is_allowed { match &self.inner { Injection::BsLive(built_ins) => built_ins.accept_req(req), From 1f5bd8996e1ed59b2c759ea959fca1fe166afb98 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 9 Jul 2025 13:38:30 +0100 Subject: [PATCH 06/13] let the proxy consume the body --- Cargo.toml | 2 +- _bslive.yml | 21 +- crates/bsnext_core/src/handler_stack.rs | 184 +++++++++---- crates/bsnext_core/src/handlers/proxy.rs | 249 ++++++++++++++++-- crates/bsnext_core/src/optional_layers.rs | 50 +--- crates/bsnext_core/src/proxy_loader.rs | 196 +++++++++++++- crates/bsnext_core/src/raw_loader.rs | 17 +- .../bsnext_core/src/server/router/common.rs | 8 +- crates/bsnext_core/tests/delays.rs | 2 + crates/bsnext_dto/src/lib.rs | 6 +- crates/bsnext_guards/src/lib.rs | 10 + crates/bsnext_guards/src/route_guard.rs | 12 +- ...ut__input_test__deserialize_3_headers.snap | 1 + ...t_test__deserialize_3_headers_control.snap | 1 + crates/bsnext_input/src/route.rs | 28 +- crates/bsnext_input/src/route_cli.rs | 6 +- ...ext_input__route_cli__test__serve_dir.snap | 1 + ...put__route_cli__test__serve_dir_delay.snap | 1 + ...ut__route_cli__test__serve_dir_shared.snap | 1 + .../bsnext_input__when_guard__when_guard.snap | 8 +- ...ext_input__when_guard__when_guard_has.snap | 8 +- crates/bsnext_input/src/when_guard.rs | 14 +- .../md_playground__md_playground.snap | 4 + crates/bsnext_resp/src/builtin_strings.rs | 18 +- crates/bsnext_resp/src/connector.rs | 6 +- crates/bsnext_resp/src/debug.rs | 6 +- crates/bsnext_resp/src/inject_addition.rs | 6 +- crates/bsnext_resp/src/inject_opts.rs | 21 +- crates/bsnext_resp/src/inject_replacement.rs | 6 +- crates/bsnext_resp/src/js_connector.rs | 6 +- crates/bsnext_resp/src/lib.rs | 9 +- crates/bsnext_resp/tests/inject.rs | 5 +- ..._kind__start_from_paths__test__test-2.snap | 1 + ...rt_kind__start_from_paths__test__test.snap | 1 + examples/basic/handler_stack.yml | 6 +- 35 files changed, 713 insertions(+), 208 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c910e646..1b445f61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1.15", features = ["sync"] } futures = "0.3.30" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } -tower = { version = "0.4", features = ['steer'] } +tower = { version = "0.4", features = ['steer', 'util'] } tower-http = { version = "0.6.0", features = ['trace', 'fs', 'compression-full', 'decompression-full', 'catch-panic', 'cors', 'timeout', 'set-header', ] } tracing = { version = "0.1.35", default-features = false, features = ["std", "attributes"] } actix = "0.13.1" diff --git a/_bslive.yml b/_bslive.yml index f17f7a49..cc6715a1 100644 --- a/_bslive.yml +++ b/_bslive.yml @@ -1,11 +1,18 @@ servers: + - port: 3003 + routes: + - path: /api + proxy: https://example.com + rewrite_uri: false - port: 3002 routes: + - path: / + html: lol - path: /api json: [ 1,2 ] when: query: - - has: 'mock' + has: 'mock' - path: /api proxy: https://duckduckgo.github.io - port: 3001 @@ -15,13 +22,13 @@ servers: - path: /shane raw: "here2" when: - query: - - has: 'abc' + - query: + has: 'abc' - path: /shane raw: "here" when: - query: - - has: 'def' + - query: + has: 'def' - path: /shane dir: examples/basic/public - path: /shane @@ -29,7 +36,7 @@ servers: - path: / html: "4" when: - query: - - is: 'a=b' + - query: + is: 'a=b' - path: / html: "3" diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index 7a87c9ae..d1060e8d 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -1,19 +1,21 @@ -use crate::handlers::proxy::{proxy_handler, ProxyConfig}; -use crate::optional_layers::{optional_layers, optional_layers_lol}; +use crate::handlers::proxy::{proxy_handler, ProxyConfig, RewriteKind}; +use crate::optional_layers::optional_layers; use crate::raw_loader::serve_raw_one; use crate::runtime_ctx::RuntimeCtx; use axum::body::Body; use axum::extract::{Request, State}; use axum::handler::Handler; -use axum::middleware::{from_fn, from_fn_with_state, Next}; +use axum::middleware::from_fn; use axum::response::IntoResponse; use axum::routing::{any, any_service, get_service, MethodRouter}; -use axum::{middleware, Extension, Router}; +use axum::{Extension, Router}; use bsnext_guards::route_guard::RouteGuard; -use bsnext_input::route::{Route, RouteKind}; +use bsnext_guards::{uri_extension, OuterUri}; +use bsnext_input::route::{ListOrSingle, Route, RouteKind}; use bsnext_input::when_guard::{HasGuard, WhenGuard}; use http::request::Parts; -use http::{Method, StatusCode, Uri}; +use http::uri::PathAndQuery; +use http::{Response, StatusCode, Uri}; use std::collections::HashMap; use std::path::PathBuf; use tower::ServiceExt; @@ -90,64 +92,85 @@ pub fn route_list_for_path(path: &str, routes: Vec, ctx: RuntimeCtx) -> R .nest_service(path, svc) .layer(from_fn(uri_extension)) } -#[derive(Debug, Clone)] -pub struct OuterUri(pub Uri); -pub async fn uri_extension(uri: Uri, mut req: Request, next: Next) -> impl IntoResponse { - req.extensions_mut().insert(OuterUri(uri)); - next.run(req).await -} pub async fn try_one( State((path, routes, ctx)): State<(String, Vec, RuntimeCtx)>, - Extension(uri): Extension, - local_uri: Uri, + Extension(OuterUri(outer_uri)): Extension, + parts: Parts, + uri: Uri, req: Request, ) -> impl IntoResponse { - let span = trace_span!("try_one", uri = ?uri.0, path = path, local_uri = ?local_uri); + let span = trace_span!("try_one", outer_uri = ?outer_uri, path = path, local_uri = ?uri); let _g = span.enter(); - let (mut original_parts, original_body) = req.into_parts(); - // original_parts.uri = uri.0; - trace!(?original_parts); - trace!("will try {} routes", routes.len()); + let pq = outer_uri.path_and_query(); - for (index, route) in routes.into_iter().enumerate() { - let span = trace_span!("index", index = index); - let _g = span.enter(); + trace!(?parts); + trace!("will try {} routes", routes.len()); - let accept = route.when.as_ref().unwrap_or(&WhenGuard::Always); - trace!(?accept); + let candidates = routes + .iter() + .enumerate() + .filter(|(index, route)| { + let span = trace_span!("early filter for candidates", index = index); + let _g = span.enter(); + + trace!(?route.kind); + + let can_serve: bool = route + .when + .as_ref() + .map(|when| match &when { + ListOrSingle::WhenOne(when) => match_one(when, &outer_uri, &path, pq, &parts), + ListOrSingle::WhenMany(many) => many + .iter() + .all(|when| match_one(when, &outer_uri, &path, pq, &parts)), + }) + .unwrap_or(true); + + trace!(?can_serve); + + if !can_serve { + return false; + } - let can_serve = match accept { - WhenGuard::Always => true, - WhenGuard::Never => false, - WhenGuard::Query { query } => query - .iter() - .map(QueryHasGuard) - .any(|x| x.accept_req_parts(&original_parts)), - }; + true + }) + .collect::>(); - trace!(?can_serve); + trace!("{} candidates", candidates.len()); - if !can_serve { - continue; + let mut body: Option = candidates.last().and_then(|(_, route)| { + if matches!(route.kind, RouteKind::Proxy(..)) { + trace!("will consume body for proxy"); + Some(req.into_body()) + } else { + None } + }); - trace!(?original_parts); + for (index, route) in &candidates { + let span = trace_span!("index", index = index); + let _g = span.enter(); - let result = match to_method_router(&path, &route, &ctx) { - Either::Left(router) => { - let raw_out = optional_layers(router, &route.opts); - let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); - raw_out.oneshot(req_clone).await - } - Either::Right(method_router) => { - let raw_out = optional_layers_lol(method_router, &route.opts); - let req_clone = Request::from_parts(original_parts.clone(), Body::empty()); - raw_out.oneshot(req_clone).await + trace!(?parts); + + let method_router = to_method_router(&path, route, &ctx); + let raw_out: MethodRouter = optional_layers(method_router, &route.opts); + let req_clone = match route.kind { + RouteKind::Raw(_) => Request::from_parts(parts.clone(), Body::empty()), + RouteKind::Proxy(_) => { + if let Some(body) = body.take() { + Request::from_parts(parts.clone(), body) + } else { + Request::from_parts(parts.clone(), Body::empty()) + } } + RouteKind::Dir(_) => Request::from_parts(parts.clone(), Body::empty()), }; + let result = raw_out.oneshot(req_clone).await; + match result { Ok(result) if result.status() == 404 => { trace!(" ❌ not found at index {}, trying another", index); @@ -158,7 +181,6 @@ pub async fn try_one( continue; } Ok(result) => { - println!("got a result {index}"); trace!( ?index, " - ✅ a non-404 response was given {}", @@ -176,21 +198,35 @@ pub async fn try_one( StatusCode::NOT_FOUND.into_response() } -enum Either { - Left(Router), - Right(MethodRouter), +fn match_one( + when_guard: &WhenGuard, + outer_uri: &Uri, + path: &str, + pq: Option<&PathAndQuery>, + parts: &Parts, +) -> bool { + match when_guard { + WhenGuard::Always => true, + WhenGuard::Never => false, + WhenGuard::ExactUri { exact_uri: true } => path == pq.map(|pq| pq.as_str()).unwrap_or("/"), + WhenGuard::ExactUri { exact_uri: false } => path != pq.map(|pq| pq.as_str()).unwrap_or("/"), + WhenGuard::Query { query } => QueryHasGuard(query).accept_req_parts(parts, outer_uri), + WhenGuard::Accept { accept } => AcceptHasGuard(accept).accept_req_parts(parts, outer_uri), + } } -fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> Either { + +fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> MethodRouter { match &route.kind { - RouteKind::Raw(raw) => Either::Right(any_service(serve_raw_one.with_state(raw.clone()))), + RouteKind::Raw(raw) => any_service(serve_raw_one.with_state(raw.clone())), RouteKind::Proxy(proxy) => { let proxy_config = ProxyConfig { target: proxy.proxy.clone(), path: path.to_string(), + headers: proxy.proxy_headers.clone().unwrap_or_default(), + rewrite_kind: RewriteKind::from(proxy.rewrite_uri), }; let proxy_with_decompression = proxy_handler.layer(Extension(proxy_config.clone())); - let as_service = any(proxy_with_decompression); - Either::Left(Router::new().nest_service(path, as_service)) + any(proxy_with_decompression) } RouteKind::Dir(dir_route) => { let svc = match &dir_route.base { @@ -215,7 +251,7 @@ fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> Either { } } .append_index_html_on_directories(true); - Either::Right(get_service(svc)) + get_service(svc) } } } @@ -223,17 +259,51 @@ fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> Either { struct QueryHasGuard<'a>(pub &'a HasGuard); impl RouteGuard for QueryHasGuard<'_> { - fn accept_req_parts(&self, parts: &Parts) -> bool { + fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool { + true + } + + fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool { + true + } + + fn accept_req_parts(&self, parts: &Parts, _outer_uri: &Uri) -> bool { let Some(query) = parts.uri.query() else { return false; }; match &self.0 { - HasGuard::Is { is } => is == query, + HasGuard::Is { is } | HasGuard::Literal(is) => is == query, HasGuard::Has { has } => query.contains(has), HasGuard::NotHas { not_has } => !query.contains(not_has), } } } +struct AcceptHasGuard<'a>(pub &'a HasGuard); + +impl RouteGuard for AcceptHasGuard<'_> { + fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool { + true + } + + fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool { + true + } + + fn accept_req_parts(&self, parts: &Parts, _outer_uri: &Uri) -> bool { + let Some(query) = parts.headers.get("accept") else { + return false; + }; + let Ok(str) = std::str::from_utf8(query.as_bytes()) else { + tracing::error!("bytes incorrrect"); + return false; + }; + match &self.0 { + HasGuard::Literal(is) | HasGuard::Is { is } => is == str, + HasGuard::Has { has } => str.contains(has), + HasGuard::NotHas { not_has } => !str.contains(not_has), + } + } +} #[cfg(test)] mod test { diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index e69c4db0..af23c3ae 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -6,11 +6,15 @@ use axum::response::{IntoResponse, Response}; use axum::routing::any; use axum::Extension; use bsnext_guards::route_guard::RouteGuard; +use bsnext_guards::OuterUri; use bsnext_resp::InjectHandling; -use http::{HeaderValue, StatusCode, Uri}; +use http::uri::{Parts, PathAndQuery}; +use http::{HeaderName, HeaderValue, StatusCode, Uri}; use hyper_tls::HttpsConnector; use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::client::legacy::Client; +use std::collections::BTreeMap; +use std::str::FromStr; use tower::ServiceExt; use tower_http::decompression::DecompressionLayer; use tracing::{trace_span, Instrument}; @@ -19,6 +23,23 @@ use tracing::{trace_span, Instrument}; pub struct ProxyConfig { pub target: String, pub path: String, + pub headers: BTreeMap, + pub rewrite_kind: RewriteKind, +} + +#[derive(Debug, Clone)] +pub enum RewriteKind { + Alias, + Nested, +} + +impl From> for RewriteKind { + fn from(value: Option) -> Self { + match value { + None | Some(true) => RewriteKind::Nested, + Some(false) => RewriteKind::Alias, + } + } } // Make our own error that wraps `anyhow::Error`. @@ -45,47 +66,70 @@ where pub async fn proxy_handler( Extension(config): Extension, + Extension(OuterUri(outer_uri)): Extension, + uri: Uri, req: Request, ) -> Result { - let target = config.target.clone(); - let path = req.uri().path(); - let span = trace_span!("proxy_handler"); let _g = span.enter(); - tracing::trace!(?config); + tracing::trace!(outer_uri = ?outer_uri); + tracing::trace!(uri = ?uri); + tracing::trace!(config.path = ?config.path); + tracing::trace!(config.target = ?config.target); + tracing::trace!(config.headers = ?config.headers); + tracing::trace!(config.rewrite_kind = ?config.rewrite_kind); - let path_query = req - .uri() - .path_and_query() - .map(|v| v.as_str()) - .unwrap_or(path); + let target = config.target.clone(); + let target_uri = Uri::try_from(&target)?; - let uri = format!("{target}{path_query}"); - let parsed = Uri::try_from(uri).context("tried to parse")?; + tracing::trace!(target = ?target); + tracing::trace!(target.uri = ?target_uri); + tracing::trace!(target.uri.authority = ?target_uri.authority()); + tracing::trace!(target.uri.path_and_query = ?target_uri.path_and_query()); - let target = match (parsed.host(), parsed.port()) { - (Some(host), Some(port)) => format!("{host}:{port}"), - (Some(host), None) => host.to_owned(), - _ => unreachable!("could not extract `host` from url"), + let args = IntoTarget { + target_uri: &target_uri, + outer_uri: &outer_uri, + uri: &uri, + config: &config, }; - + let parsed = into_target_uri(args)?; tracing::trace!(outgoing.uri = %parsed); - let host_header_value = HeaderValue::from_str(&target).unwrap(); - let (parts, body) = req.into_parts(); + let mut req = Request::from_parts(parts, body); - *req.uri_mut() = parsed; + let target = match (parsed.host(), parsed.port()) { + (Some(host), Some(port)) => format!("{host}:{port}"), + (Some(host), None) => host.to_owned(), + _ => unreachable!("could not extract `host` from url"), + }; - // todo(alpha): Which other headers to mod here? + *req.uri_mut() = parsed; + let host_header_value = HeaderValue::from_str(&target)?; req.headers_mut().insert("host", host_header_value); req.headers_mut().remove("referer"); + for (k, v) in config.headers { + match ( + HeaderName::from_bytes(k.as_bytes()), + HeaderValue::from_bytes(v.as_bytes()), + ) { + (Ok(hn), Ok(hv)) => { + tracing::trace!(?hn, ?hv, "add cookie to outgoing request"); + req.headers_mut().insert(hn, hv); + } + _ => { + // noop + } + } + } + // decompress requests if needed if let Some(h) = req.extensions().get::() { - let req_accepted = h.items.iter().any(|item| item.accept_req(&req)); + let req_accepted = h.items.iter().any(|item| item.accept_req(&req, &outer_uri)); tracing::trace!( req.accepted = req_accepted, req.accept.header = req @@ -132,3 +176,164 @@ async fn serve_one_proxy_req(req: Request) -> Response { tracing::trace!(resp.headers = ?resp.headers()); resp } + +struct IntoTarget<'a> { + target_uri: &'a Uri, + outer_uri: &'a Uri, + uri: &'a Uri, + config: &'a ProxyConfig, +} + +fn into_target_uri( + IntoTarget { + target_uri, + outer_uri, + uri, + config, + }: IntoTarget, +) -> anyhow::Result { + let uri_src = match config.rewrite_kind { + RewriteKind::Alias => &outer_uri, + RewriteKind::Nested => &uri, + }; + + let req_query = uri_src.path_and_query().and_then(|x| x.query()); + let target = target_uri.path_and_query().filter(|x| x.path() != "/"); + let src = uri_src.path_and_query().filter(|x| x.path() != "/"); + + let next_pq = match (target, src) { + (Some(target), Some(req)) => { + let p1 = target.path(); + let p2 = req.path(); + let next = match req_query { + None => format!("{p1}{p2}"), + Some(q) => format!("{p1}{p2}?{q}"), + }; + let v = + PathAndQuery::from_str(&next).unwrap_or(uri_src.path_and_query().unwrap().clone()); + v + } + (Some(target_only), None) => { + dbg!(&target_only); + let path = target_only.path(); + + let next = match req_query { + None => path, + Some(q) => &format!("{path}?{q}"), + }; + let v = + PathAndQuery::from_str(next).unwrap_or(uri_src.path_and_query().unwrap().clone()); + v + } + (None, Some(req_only)) => req_only.to_owned(), + (None, None) => uri_src.path_and_query().unwrap().clone(), + }; + + let mut next_parts = Parts::default(); + if let Some(scheme) = target_uri.scheme() { + next_parts.scheme = Some(scheme.to_owned()) + } + if let Some(auth) = target_uri.authority() { + next_parts.authority = Some(auth.to_owned()); + } + + next_parts.path_and_query = Some(next_pq); + + Uri::from_parts(next_parts).context("tried to parse") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + struct TestCase { + name: &'static str, + target: &'static str, + outer: &'static str, + uri: &'static str, + rewrite: RewriteKind, + expect: &'static str, + } + + #[test] + fn test_into_target_uri() { + let test_cases = vec![ + TestCase { + name: "basic", + target: "http://example.com", + outer: "/", + uri: "/", + rewrite: RewriteKind::Alias, + expect: "http://example.com/", + }, + TestCase { + name: "basic nested alias - the `/api` section is preserved here", + target: "http://example.com", + outer: "/api/abc", + uri: "/abc", + rewrite: RewriteKind::Alias, + expect: "http://example.com/api/abc", + }, + TestCase { + name: "basic nested rewrite - the `/api` prefixed is dropped here", + target: "http://example.com", + outer: "/api/abc", + uri: "/abc", + rewrite: RewriteKind::Nested, + expect: "http://example.com/abc", + }, + TestCase { + name: "basic nested rewrite", + target: "http://example.com/api", + outer: "http://localhost:3000/test", + uri: "/users/123", + rewrite: RewriteKind::Nested, + expect: "http://example.com/api/users/123", + }, + TestCase { + name: "basic alias rewrite", + target: "http://example.com/api", + outer: "http://localhost:3000/test/users/123", + uri: "/users/123", + rewrite: RewriteKind::Alias, + expect: "http://example.com/api/test/users/123", + }, + TestCase { + name: "with query params", + target: "http://example.com/api", + outer: "http://localhost:3000/test?foo=bar", + uri: "/users/123?foo=bar", + rewrite: RewriteKind::Nested, + expect: "http://example.com/api/users/123?foo=bar", + }, + ]; + + for tc in test_cases { + let target_uri = Uri::from_str(tc.target).unwrap(); + let outer_uri = Uri::from_str(tc.outer).unwrap(); + let uri = Uri::from_str(tc.uri).unwrap(); + let config = ProxyConfig { + target: tc.target.to_string(), + path: String::new(), + headers: BTreeMap::new(), + rewrite_kind: tc.rewrite, + }; + + let args = IntoTarget { + target_uri: &target_uri, + outer_uri: &outer_uri, + uri: &uri, + config: &config, + }; + + let result = into_target_uri(args).unwrap(); + assert_eq!( + result.to_string(), + tc.expect, + "failed test case: {}", + tc.name + ); + } + } +} diff --git a/crates/bsnext_core/src/optional_layers.rs b/crates/bsnext_core/src/optional_layers.rs index b6232eb2..4517245f 100644 --- a/crates/bsnext_core/src/optional_layers.rs +++ b/crates/bsnext_core/src/optional_layers.rs @@ -3,7 +3,7 @@ use axum::extract::{Request, State}; use axum::middleware::{map_response_with_state, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::MethodRouter; -use axum::{middleware, Extension, Router}; +use axum::{middleware, Extension}; use axum_extra::middleware::option_layer; use bsnext_input::route::{CompType, CompressionOpts, CorsOpts, DelayKind, DelayOpts, Opts}; use bsnext_resp::{response_modifications_layer, InjectHandling}; @@ -17,53 +17,7 @@ use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::cors::CorsLayer; -pub fn optional_layers(app: Router, opts: &Opts) -> Router { - let mut app = app; - let cors_enabled_layer = opts - .cors - .as_ref() - .filter(|v| **v == CorsOpts::Cors(true)) - .map(|_| CorsLayer::permissive()); - - let compression_layer = opts.compression.as_ref().and_then(comp_opts_to_layer); - - let delay_enabled_layer = opts - .delay - .as_ref() - .map(|delay| middleware::from_fn_with_state(delay.clone(), delay_mw)); - - let injections = opts.inject.as_injections(); - - let set_response_headers_layer = opts - .headers - .as_ref() - .map(|headers| map_response_with_state(headers.clone(), set_resp_headers_from_strs)); - - let headers = opts.cache.as_headers(); - let prevent_cache_headers_layer = map_response_with_state(headers, set_resp_headers); - - let optional_stack = ServiceBuilder::new() - .layer(middleware::from_fn(dynamic_query_params_handler)) - .layer(middleware::from_fn(response_modifications_layer)) - .layer(prevent_cache_headers_layer) - .layer(option_layer(set_response_headers_layer)) - .layer(option_layer(cors_enabled_layer)) - .layer(option_layer(delay_enabled_layer)); - - app = app.layer(optional_stack); - - // The compression layer has a different type, so needs to apply outside the optional stack - // this essentially wrapping everything. - // I'm sure there's a cleaner way... - if let Some(cl) = compression_layer { - app = app.layer(cl); - } - - app.layer(Extension(InjectHandling { - items: injections.items, - })) -} -pub fn optional_layers_lol(app: MethodRouter, opts: &Opts) -> MethodRouter { +pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter { let mut app = app; let cors_enabled_layer = opts .cors diff --git a/crates/bsnext_core/src/proxy_loader.rs b/crates/bsnext_core/src/proxy_loader.rs index ab5eb82b..005b5536 100644 --- a/crates/bsnext_core/src/proxy_loader.rs +++ b/crates/bsnext_core/src/proxy_loader.rs @@ -6,13 +6,15 @@ mod test { use crate::server::router::common::{test_proxy, to_resp_parts_and_body}; use axum::body::Body; use axum::extract::Request; - use axum::routing::get; - use axum::{Extension, Router}; + use axum::routing::{get, post}; + use axum::{Extension, Json, Router}; use bsnext_input::route::Route; + use hyper_tls::HttpsConnector; use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::client::legacy::Client; use hyper_util::rt::TokioExecutor; + use serde_json::json; use tower::ServiceExt; #[tokio::test] @@ -56,6 +58,196 @@ mod test { proxy.destroy().await?; + Ok(()) + } + #[tokio::test] + async fn test_post() -> anyhow::Result<()> { + let https = HttpsConnector::new(); + let client: Client, Body> = + Client::builder(TokioExecutor::new()).build(https); + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + struct Person { + name: String, + } + + let proxy_app = Router::new().route( + "/did-post", + post(|Json(mut person): Json| async move { + dbg!(&person); + person.name = format!("-->{}", person.name); + Json(person) + }), + ); + + let proxy = test_proxy(proxy_app).await?; + + let routes_input = format!( + r#" + - path: / + proxy: {http} + "#, + http = proxy.http_addr + ); + + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client)); + + let expected_response_body = "{\"name\":\"-->shane\"}"; + + let outgoing_body = serde_json::to_string(&json!({ + "name": "shane" + }))?; + + // Define the request + let request = Request::post("/did-post") + .header("Content-Type", "application/json") + .body(outgoing_body)?; + + // Make a one-shot request on the router + let response = router.oneshot(request).await?; + + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + assert_eq!(actual_body, expected_response_body); + } + + proxy.destroy().await?; + + Ok(()) + } + #[tokio::test] + async fn test_path_rewriting() -> anyhow::Result<()> { + let https = HttpsConnector::new(); + let client: Client, Body> = + Client::builder(TokioExecutor::new()).build(https); + + let proxy_app = Router::new() + .route("/", get(|| async { "did rewrite" })) + .route("/no-rewrite", get(|| async { "did not rewrite" })) + .route("/api", get(|| async { "api" })) + .route("/api/rewrite-alt-append", get(|| async { "api+appended" })) + .route( + "/a/b/nested/no-rewrite", + get(|| async { "did not rewrite (nested)" }), + ); + + let proxy = test_proxy(proxy_app).await?; + + let routes_input = format!( + r#" + - path: /rewrite + proxy: {http} + - path: /no-rewrite + proxy: {http} + rewrite_uri: false + - path: /a/b/nested/rewrite + proxy: {http} + - path: /a/b/nested/no-rewrite + proxy: {http} + rewrite_uri: false + + + - path: /rewrite-alt + proxy: {http}/api + - path: /rewrite-alt-append + proxy: {http}/api + rewrite_uri: false + "#, + http = proxy.http_addr + ); + + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client.clone())); + + let expected_body = "did rewrite"; + + let request = Request::get("/rewrite").body(Body::empty())?; + let response = router.oneshot(request).await?; + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + + assert_eq!(actual_body, expected_body); + } + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client.clone())); + + let expected_body = "did not rewrite"; + + let request = Request::get("/no-rewrite").body(Body::empty())?; + let response = router.oneshot(request).await?; + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + + assert_eq!(actual_body, expected_body); + } + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client.clone())); + + let expected_body = "did rewrite"; + + let request = Request::get("/a/b/nested/rewrite").body(Body::empty())?; + let response = router.oneshot(request).await?; + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + + assert_eq!(actual_body, expected_body); + } + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client.clone())); + + let expected_body = "did not rewrite (nested)"; + + let request = Request::get("/a/b/nested/no-rewrite").body(Body::empty())?; + let response = router.oneshot(request).await?; + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + + assert_eq!(actual_body, expected_body); + } + + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client.clone())); + + let expected_body = "api"; + + let request = Request::get("/rewrite-alt").body(Body::empty())?; + let response = router.oneshot(request).await?; + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + + assert_eq!(actual_body, expected_body); + } + + { + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client.clone())); + + let expected_body = "api+appended"; + + let request = Request::get("/rewrite-alt-append").body(Body::empty())?; + let response = router.oneshot(request).await?; + let (_parts, actual_body) = to_resp_parts_and_body(response).await; + + assert_eq!(actual_body, expected_body); + } + + proxy.destroy().await?; + Ok(()) } } diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index 93b88d3b..baeb657c 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -1,13 +1,14 @@ use axum::extract::{Request, State}; use axum::middleware::Next; use axum::response::{Html, IntoResponse, Response, Sse}; -use axum::Json; +use axum::{Extension, Json}; use http::header::CONTENT_TYPE; use std::convert::Infallible; use std::fs; use axum::body::Body; use axum::response::sse::Event; +use bsnext_guards::OuterUri; use bsnext_input::route::{RawRoute, SseOpts}; use bytes::Bytes; use http::{StatusCode, Uri}; @@ -15,12 +16,18 @@ use http_body_util::BodyExt; use std::time::Duration; use tokio_stream::StreamExt; -pub async fn serve_raw_one(uri: Uri, state: State, req: Request) -> Response { - tracing::trace!("serve_raw_one {}", req.uri().to_string()); - raw_resp_for(uri, &state.0).await.into_response() +pub async fn serve_raw_one( + uri: Uri, + Extension(outer_uri): Extension, + state: State, + _req: Request, +) -> Response { + tracing::trace!(?outer_uri, ?uri, "serve_raw_one"); + raw_resp_for(outer_uri, &state.0).await.into_response() } -pub async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { +pub async fn raw_resp_for(outer_uri: OuterUri, route: &RawRoute) -> impl IntoResponse { + let uri = outer_uri.0; match route { RawRoute::Html { html } => { tracing::trace!("raw_resp_for will respond with HTML"); diff --git a/crates/bsnext_core/src/server/router/common.rs b/crates/bsnext_core/src/server/router/common.rs index 4cc2f635..8868db3c 100644 --- a/crates/bsnext_core/src/server/router/common.rs +++ b/crates/bsnext_core/src/server/router/common.rs @@ -6,7 +6,8 @@ use crate::handler_stack::RouteMap; use crate::runtime_ctx::RuntimeCtx; use axum::body::Body; use axum::extract::Request; -use axum::response::Response; +use axum::middleware::{from_fn, Next}; +use axum::response::{IntoResponse, Response}; use axum::Router; use bsnext_dto::ClientEvent; use bsnext_input::server_config::ServerConfig; @@ -133,6 +134,11 @@ pub async fn test_proxy(router: Router) -> anyhow::Result { // give consumers the address address_sender.send(socket_addr).expect("can send"); + async fn logger(req: Request, next: Next) -> impl IntoResponse { + next.run(req).await + } + + let router = router.layer(from_fn(logger)); // serve and wait for shutdown axum::serve(listener, router) diff --git a/crates/bsnext_core/tests/delays.rs b/crates/bsnext_core/tests/delays.rs index cc80906c..61db15ce 100644 --- a/crates/bsnext_core/tests/delays.rs +++ b/crates/bsnext_core/tests/delays.rs @@ -71,6 +71,8 @@ async fn test_proxy_delay() -> Result<(), anyhow::Error> { ); proxy_route.kind = RouteKind::Proxy(ProxyRoute { proxy: proxy.http_addr.clone(), + rewrite_uri: None, + proxy_headers: None, }); let state = into_state(config); diff --git a/crates/bsnext_dto/src/lib.rs b/crates/bsnext_dto/src/lib.rs index 62f6a3e9..6c44d78c 100644 --- a/crates/bsnext_dto/src/lib.rs +++ b/crates/bsnext_dto/src/lib.rs @@ -81,7 +81,11 @@ impl From for RouteKindDTO { sse: SseDTOOpts { body: opts.body }, }, }, - RouteKind::Proxy(ProxyRoute { proxy }) => RouteKindDTO::Proxy { proxy }, + RouteKind::Proxy(ProxyRoute { + proxy, + proxy_headers: _outgoing_headers, + rewrite_uri: _rewrite, + }) => RouteKindDTO::Proxy { proxy }, RouteKind::Dir(DirRoute { dir, base }) => RouteKindDTO::Dir { dir, base: base.map(|b| b.to_string_lossy().to_string()), diff --git a/crates/bsnext_guards/src/lib.rs b/crates/bsnext_guards/src/lib.rs index c0ca76e4..5cb6a134 100644 --- a/crates/bsnext_guards/src/lib.rs +++ b/crates/bsnext_guards/src/lib.rs @@ -1,4 +1,7 @@ use crate::path_matcher::PathMatcher; +use axum::extract::Request; +use axum::middleware::Next; +use axum::response::IntoResponse; use http::Uri; pub mod path_matcher; @@ -22,3 +25,10 @@ impl MatcherList { } } } + +#[derive(Debug, Clone)] +pub struct OuterUri(pub Uri); +pub async fn uri_extension(uri: Uri, mut req: Request, next: Next) -> impl IntoResponse { + req.extensions_mut().insert(OuterUri(uri)); + next.run(req).await +} diff --git a/crates/bsnext_guards/src/route_guard.rs b/crates/bsnext_guards/src/route_guard.rs index ef450d4d..09e728bf 100644 --- a/crates/bsnext_guards/src/route_guard.rs +++ b/crates/bsnext_guards/src/route_guard.rs @@ -1,15 +1,11 @@ use axum::extract::Request; use http::request::Parts; -use http::Response; +use http::{Response, Uri}; pub trait RouteGuard { - fn accept_req(&self, _req: &Request) -> bool { - true - } - fn accept_req_parts(&self, _parts: &Parts) -> bool { - true - } - fn accept_res(&self, _res: &Response) -> bool { + fn accept_req(&self, req: &Request, outer_uri: &Uri) -> bool; + fn accept_res(&self, res: &Response, outer_uri: &Uri) -> bool; + fn accept_req_parts(&self, _parts: &Parts, _outer_uri: &Uri) -> bool { true } } diff --git a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap index dbede578..4699a15c 100644 --- a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap +++ b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap @@ -36,6 +36,7 @@ Config { compression: None, }, fallback: None, + when: None, }, ], } diff --git a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap index 3c377943..5f0369ab 100644 --- a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap +++ b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap @@ -32,6 +32,7 @@ Config { compression: None, }, fallback: None, + when: None, }, ], } diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index cb055e18..545de8ae 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -21,7 +21,14 @@ pub struct Route { #[serde(flatten)] pub opts: Opts, pub fallback: Option, - pub when: Option, + pub when: Option>, +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum ListOrSingle { + WhenOne(T), + WhenMany(Vec), } #[derive(Debug, PartialEq, thiserror::Error)] @@ -114,6 +121,8 @@ impl Route { }, kind: RouteKind::Proxy(ProxyRoute { proxy: a.as_ref().to_string(), + proxy_headers: None, + rewrite_uri: None, }), ..Default::default() } @@ -128,6 +137,21 @@ pub enum RouteKind { Dir(DirRoute), } +impl Display for RouteKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RouteKind::Raw(raw) => match raw { + RawRoute::Html { html: _ } => write!(f, "Raw(HTML)"), + RawRoute::Json { json: _ } => write!(f, "Raw(JSON)"), + RawRoute::Raw { raw: _ } => write!(f, "Raw(Text)"), + RawRoute::Sse { sse: _ } => write!(f, "Raw(SSE)"), + }, + RouteKind::Proxy(proxy) => write!(f, "Proxy({})", proxy.proxy), + RouteKind::Dir(dir) => write!(f, "Dir({})", dir.dir), + } + } +} + impl RouteKind { pub fn new_html(html: impl Into) -> Self { RouteKind::Raw(RawRoute::Html { html: html.into() }) @@ -185,6 +209,8 @@ pub struct DirRoute { #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct ProxyRoute { pub proxy: String, + pub proxy_headers: Option>, + pub rewrite_uri: Option, } #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] diff --git a/crates/bsnext_input/src/route_cli.rs b/crates/bsnext_input/src/route_cli.rs index 64fa54c7..42ddbea8 100644 --- a/crates/bsnext_input/src/route_cli.rs +++ b/crates/bsnext_input/src/route_cli.rs @@ -32,7 +32,11 @@ impl TryInto for RouteCli { }, SubCommands::Proxy { path, target, opts } => Route { path: PathDef::try_new(path)?, - kind: RouteKind::Proxy(ProxyRoute { proxy: target }), + kind: RouteKind::Proxy(ProxyRoute { + proxy: target, + proxy_headers: None, + rewrite_uri: None, + }), opts: opts_to_route_opts(&opts), ..std::default::Default::default() }, diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap index 83ba872c..0775ddd9 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap @@ -26,4 +26,5 @@ Route { compression: None, }, fallback: None, + when: None, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap index a0cf7714..674e95d9 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap @@ -32,4 +32,5 @@ Route { compression: None, }, fallback: None, + when: None, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap index b315c315..eca5af63 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap @@ -36,4 +36,5 @@ Route { compression: None, }, fallback: None, + when: None, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap index 56f253cc..4b06249e 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard.snap @@ -4,10 +4,8 @@ expression: "&when" --- A { when: Query { - query: [ - NotHas { - not_has: "here", - }, - ], + query: NotHas { + not_has: "here", + }, }, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap index 7ae33342..621ebff8 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__when_guard__when_guard_has.snap @@ -4,10 +4,8 @@ expression: "&when" --- A { when: Query { - query: [ - Has { - has: "here", - }, - ], + query: Has { + has: "here", + }, }, } diff --git a/crates/bsnext_input/src/when_guard.rs b/crates/bsnext_input/src/when_guard.rs index 0394cb3c..4c74b2d9 100644 --- a/crates/bsnext_input/src/when_guard.rs +++ b/crates/bsnext_input/src/when_guard.rs @@ -4,8 +4,14 @@ pub enum WhenGuard { #[default] Always, Never, + ExactUri { + exact_uri: bool, + }, Query { - query: Vec, + query: HasGuard, + }, + Accept { + accept: HasGuard, }, } @@ -19,7 +25,7 @@ fn test_when_guard() { let input = r#" when: query: - - not.has: "here" + not.has: "here" "#; let when: A = serde_yaml::from_str(input).expect("test"); assert_debug_snapshot!(&when); @@ -35,7 +41,7 @@ fn test_when_guard_has() { let input = r#" when: query: - - has: "here" + has: "here" "#; let when: A = serde_yaml::from_str(input).expect("test"); assert_debug_snapshot!(&when); @@ -44,6 +50,8 @@ fn test_when_guard_has() { #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] #[serde(untagged)] pub enum HasGuard { + /// A direct, exact match + Literal(String), /// A direct, exact match Is { is: String }, /// Contains the substring diff --git a/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap b/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap index 2abb8d1d..11f10fec 100644 --- a/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap +++ b/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap @@ -94,6 +94,7 @@ expression: routes }, }, ), + when: None, }, Route { path: PathDef { @@ -118,6 +119,7 @@ expression: routes compression: None, }, fallback: None, + when: None, }, Route { path: PathDef { @@ -142,6 +144,7 @@ expression: routes compression: None, }, fallback: None, + when: None, }, Route { path: PathDef { @@ -166,5 +169,6 @@ expression: routes compression: None, }, fallback: None, + when: None, }, ] diff --git a/crates/bsnext_resp/src/builtin_strings.rs b/crates/bsnext_resp/src/builtin_strings.rs index 0870e96e..4e6c0142 100644 --- a/crates/bsnext_resp/src/builtin_strings.rs +++ b/crates/bsnext_resp/src/builtin_strings.rs @@ -4,7 +4,7 @@ use crate::injector_guard::ByteReplacer; use crate::js_connector::JsConnector; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct BuiltinStringDef { @@ -21,19 +21,19 @@ pub enum BuiltinStrings { } impl RouteGuard for BuiltinStringDef { - fn accept_req(&self, req: &Request) -> bool { + fn accept_req(&self, req: &Request, outer_uri: &Uri) -> bool { match self.name { - BuiltinStrings::Connector => Connector.accept_req(req), - BuiltinStrings::Debug => Debug.accept_req(req), - BuiltinStrings::JsConnector => JsConnector.accept_req(req), + BuiltinStrings::Connector => Connector.accept_req(req, outer_uri), + BuiltinStrings::Debug => Debug.accept_req(req, outer_uri), + BuiltinStrings::JsConnector => JsConnector.accept_req(req, outer_uri), } } - fn accept_res(&self, res: &Response) -> bool { + fn accept_res(&self, res: &Response, outer_uri: &Uri) -> bool { match self.name { - BuiltinStrings::Connector => Connector.accept_res(res), - BuiltinStrings::Debug => Debug.accept_res(res), - BuiltinStrings::JsConnector => JsConnector.accept_res(res), + BuiltinStrings::Connector => Connector.accept_res(res, outer_uri), + BuiltinStrings::Debug => Debug.accept_res(res, outer_uri), + BuiltinStrings::JsConnector => JsConnector.accept_res(res, outer_uri), } } } diff --git a/crates/bsnext_resp/src/connector.rs b/crates/bsnext_resp/src/connector.rs index cdbc8a6c..f2f51496 100644 --- a/crates/bsnext_resp/src/connector.rs +++ b/crates/bsnext_resp/src/connector.rs @@ -2,17 +2,17 @@ use crate::injector_guard::ByteReplacer; use crate::RespMod; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, Default)] pub struct Connector; impl RouteGuard for Connector { - fn accept_req(&self, req: &Request) -> bool { + fn accept_req(&self, req: &Request, _outer_uri: &Uri) -> bool { RespMod::accepts_html(req) } - fn accept_res(&self, res: &Response) -> bool { + fn accept_res(&self, res: &Response, _outer_uri: &Uri) -> bool { RespMod::is_html(res) } } diff --git a/crates/bsnext_resp/src/debug.rs b/crates/bsnext_resp/src/debug.rs index 0bd3807f..d0d04fb2 100644 --- a/crates/bsnext_resp/src/debug.rs +++ b/crates/bsnext_resp/src/debug.rs @@ -1,17 +1,17 @@ use crate::injector_guard::ByteReplacer; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, Default)] pub struct Debug; impl RouteGuard for Debug { - fn accept_req(&self, req: &Request) -> bool { + fn accept_req(&self, req: &Request, _uri: &Uri) -> bool { req.uri().path().contains("core.css") } - fn accept_res(&self, _res: &Response) -> bool { + fn accept_res(&self, _res: &Response, _uri: &Uri) -> bool { true } } diff --git a/crates/bsnext_resp/src/inject_addition.rs b/crates/bsnext_resp/src/inject_addition.rs index a8e32503..e75a9484 100644 --- a/crates/bsnext_resp/src/inject_addition.rs +++ b/crates/bsnext_resp/src/inject_addition.rs @@ -1,7 +1,7 @@ use crate::injector_guard::ByteReplacer; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct InjectAddition { @@ -17,11 +17,11 @@ pub enum AdditionPosition { } impl RouteGuard for InjectAddition { - fn accept_req(&self, _req: &Request) -> bool { + fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool { true } - fn accept_res(&self, _res: &Response) -> bool { + fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool { true } } diff --git a/crates/bsnext_resp/src/inject_opts.rs b/crates/bsnext_resp/src/inject_opts.rs index aafe2ef9..40225a2c 100644 --- a/crates/bsnext_resp/src/inject_opts.rs +++ b/crates/bsnext_resp/src/inject_opts.rs @@ -5,7 +5,7 @@ use crate::injector_guard::ByteReplacer; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; use bsnext_guards::MatcherList; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] #[serde(untagged)] @@ -70,30 +70,29 @@ pub struct UnknownStringDef { pub name: String, } impl RouteGuard for InjectionItem { - fn accept_req(&self, req: &Request) -> bool { + fn accept_req(&self, req: &Request, outer_uri: &Uri) -> bool { let uri_is_allowed = match self.only.as_ref() { None => true, - Some(ml) => ml.test_uri(req.uri()), + Some(ml) => ml.test_uri(outer_uri), }; - println!("{}", uri_is_allowed); if uri_is_allowed { match &self.inner { - Injection::BsLive(built_ins) => built_ins.accept_req(req), + Injection::BsLive(built_ins) => built_ins.accept_req(req, outer_uri), Injection::UnknownNamed(_) => todo!("accept_req Injection::UnknownNamed"), - Injection::Replacement(def) => def.accept_req(req), - Injection::Addition(add) => add.accept_req(req), + Injection::Replacement(def) => def.accept_req(req, outer_uri), + Injection::Addition(add) => add.accept_req(req, outer_uri), } } else { false } } - fn accept_res(&self, res: &Response) -> bool { + fn accept_res(&self, res: &Response, outer_uri: &Uri) -> bool { match &self.inner { - Injection::BsLive(built_ins) => built_ins.accept_res(res), + Injection::BsLive(built_ins) => built_ins.accept_res(res, outer_uri), Injection::UnknownNamed(_) => todo!("accept_res Injection::UnknownNamed"), - Injection::Replacement(def) => def.accept_res(res), - Injection::Addition(add) => add.accept_res(res), + Injection::Replacement(def) => def.accept_res(res, outer_uri), + Injection::Addition(add) => add.accept_res(res, outer_uri), } } } diff --git a/crates/bsnext_resp/src/inject_replacement.rs b/crates/bsnext_resp/src/inject_replacement.rs index e2db864c..78c9255e 100644 --- a/crates/bsnext_resp/src/inject_replacement.rs +++ b/crates/bsnext_resp/src/inject_replacement.rs @@ -1,7 +1,7 @@ use crate::injector_guard::ByteReplacer; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct InjectReplacement { @@ -19,11 +19,11 @@ pub enum Pos { } impl RouteGuard for InjectReplacement { - fn accept_req(&self, _req: &Request) -> bool { + fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool { true } - fn accept_res(&self, _res: &Response) -> bool { + fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool { true } } diff --git a/crates/bsnext_resp/src/js_connector.rs b/crates/bsnext_resp/src/js_connector.rs index 38d642ac..63805875 100644 --- a/crates/bsnext_resp/src/js_connector.rs +++ b/crates/bsnext_resp/src/js_connector.rs @@ -2,17 +2,17 @@ use crate::injector_guard::ByteReplacer; use crate::RespMod; use axum::extract::Request; use bsnext_guards::route_guard::RouteGuard; -use http::Response; +use http::{Response, Uri}; #[derive(Debug, Default)] pub struct JsConnector; impl RouteGuard for JsConnector { - fn accept_req(&self, _req: &Request) -> bool { + fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool { true } - fn accept_res(&self, res: &Response) -> bool { + fn accept_res(&self, res: &Response, _outer_uri: &Uri) -> bool { let is_js = RespMod::is_js(res); tracing::trace!("is_js: {}", is_js); is_js diff --git a/crates/bsnext_resp/src/lib.rs b/crates/bsnext_resp/src/lib.rs index 6a56a1a6..24c864d3 100644 --- a/crates/bsnext_resp/src/lib.rs +++ b/crates/bsnext_resp/src/lib.rs @@ -19,6 +19,7 @@ use axum::middleware::Next; use axum::response::IntoResponse; use axum::Extension; use bsnext_guards::route_guard::RouteGuard; +use bsnext_guards::OuterUri; use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; use http::{Response, StatusCode}; use http_body_util::BodyExt; @@ -58,17 +59,19 @@ pub struct InjectHandling { pub async fn response_modifications_layer( Extension(inject): Extension, + Extension(OuterUri(outer_uri)): Extension, req: Request, next: Next, ) -> Result { - let span = span!(parent: None, Level::TRACE, "resp-mod", uri=req.uri().path()); + let span = + span!(parent: None, Level::TRACE, "resp-mod", uri=req.uri().path(), outer_uri=?outer_uri); let _guard = span.enter(); // bail when there are no accepting modifications let req_accepted: Vec = inject .items .into_iter() - .filter(|item| item.accept_req(&req)) + .filter(|item| item.accept_req(&req, &outer_uri)) .collect(); if req_accepted.is_empty() { @@ -81,7 +84,7 @@ pub async fn response_modifications_layer( // also bail if no responses are accepted let res_accepted: Vec = req_accepted .into_iter() - .filter(|item| item.accept_res(&res)) + .filter(|item| item.accept_res(&res, &outer_uri)) .collect(); if res_accepted.is_empty() { return Ok(res.into_response()); diff --git a/crates/bsnext_resp/tests/inject.rs b/crates/bsnext_resp/tests/inject.rs index 49e06cba..200b2969 100644 --- a/crates/bsnext_resp/tests/inject.rs +++ b/crates/bsnext_resp/tests/inject.rs @@ -47,8 +47,10 @@ async fn test_inject_only_1_level() -> anyhow::Result<()> { mod helpers { use axum::body::Body; + use axum::middleware::from_fn; use axum::response::IntoResponse; use axum::{middleware, Extension, Router}; + use bsnext_guards::uri_extension; use bsnext_resp::inject_opts::InjectionItem; use bsnext_resp::{response_modifications_layer, InjectHandling}; use http::Request; @@ -60,7 +62,8 @@ mod helpers { .layer(middleware::from_fn(response_modifications_layer)) .layer(Extension(InjectHandling { items: items.to_vec(), - })); + })) + .layer(from_fn(uri_extension)); let r = Request::get(uri).body(Body::empty())?; let output = router.oneshot(r).await?; diff --git a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap index 69b2d905..91fd05a3 100644 --- a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap +++ b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap @@ -14,6 +14,7 @@ servers: cache: prevent compression: ~ fallback: ~ + when: ~ watchers: [] playground: ~ clients: diff --git a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap index 7af4eb60..a1659e62 100644 --- a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap +++ b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap @@ -33,6 +33,7 @@ Input { compression: None, }, fallback: None, + when: None, }, ], watchers: [], diff --git a/examples/basic/handler_stack.yml b/examples/basic/handler_stack.yml index ef0c259d..9cbdc85b 100644 --- a/examples/basic/handler_stack.yml +++ b/examples/basic/handler_stack.yml @@ -13,6 +13,8 @@ servers: routes: - path: / html: hello world! + when: + exact_uri: true - path: / dir: examples/basic/public - name: "2dirs" @@ -41,11 +43,11 @@ servers: raw: "hello from 1st" when: query: - - has: 'include' + has: 'include' - path: /script.js raw: "hello from 2nd" when: query: - - is: '2nd=please' + is: '2nd=please' - path: /script.js dir: examples/basic/public/script.js \ No newline at end of file From 805130e00be8d346ff9a3e89def1671ae8c63d97 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 10 Jul 2025 21:15:05 +0100 Subject: [PATCH 07/13] fix fallbacks --- crates/bsnext_core/src/handler_stack.rs | 82 +++++++++------------ crates/bsnext_core/src/server/router/mod.rs | 5 +- playwright.config.ts | 3 +- 3 files changed, 37 insertions(+), 53 deletions(-) diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index d1060e8d..c70c5e88 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -19,26 +19,8 @@ use http::{Response, StatusCode, Uri}; use std::collections::HashMap; use std::path::PathBuf; use tower::ServiceExt; -use tower_http::services::ServeDir; +use tower_http::services::{ServeDir, ServeFile}; use tracing::{trace, trace_span}; -// impl DirRouteOpts { -// pub fn as_serve_file(&self) -> ServeFile { -// match &self.dir_route.base { -// Some(base_dir) => { -// tracing::trace!( -// "combining root: `{}` with given path: `{}`", -// base_dir.display(), -// self.dir_route.dir -// ); -// ServeFile::new(base_dir.join(&self.dir_route.dir)) -// } -// None => { -// tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); -// ServeFile::new(&self.dir_route.dir) -// } -// } -// } -// } pub struct RouteMap { pub mapping: HashMap>, @@ -83,10 +65,6 @@ impl RouteMap { pub fn route_list_for_path(path: &str, routes: Vec, ctx: RuntimeCtx) -> Router { // let r1 = from_fn_with_state((path.to_string(), routes, ctx), try_one); let svc = any_service(try_one.with_state((path.to_string(), routes, ctx))); - // if path.contains("{") { - // tracing::trace!(?path, "route"); - // return Router::new().route(path, svc); - // } tracing::trace!("nest_service"); Router::new() .nest_service(path, svc) @@ -155,7 +133,7 @@ pub async fn try_one( trace!(?parts); - let method_router = to_method_router(&path, route, &ctx); + let method_router = to_method_router(&path, &route.kind, &ctx); let raw_out: MethodRouter = optional_layers(method_router, &route.opts); let req_clone = match route.kind { RouteKind::Raw(_) => Request::from_parts(parts.clone(), Body::empty()), @@ -172,22 +150,24 @@ pub async fn try_one( let result = raw_out.oneshot(req_clone).await; match result { - Ok(result) if result.status() == 404 => { - trace!(" ❌ not found at index {}, trying another", index); - continue; - } - Ok(result) if result.status() == 405 => { - trace!(" ❌ 405, trying another..."); - continue; - } - Ok(result) => { - trace!( - ?index, - " - ✅ a non-404 response was given {}", - result.status() - ); - return result.into_response(); - } + Ok(result) => match result.status().as_u16() { + 404 | 405 => { + if let Some(fallback) = &route.fallback { + let mr = to_method_router(&path, &fallback.kind, &ctx); + let raw_out: MethodRouter = optional_layers(mr, &fallback.opts); + let raw_fb = Request::from_parts(parts.clone(), Body::empty()); + return raw_out.oneshot(raw_fb).await.into_response(); + } + } + _ => { + trace!( + ?index, + " - ✅ a non-404 response was given {}", + result.status() + ); + return result.into_response(); + } + }, Err(e) => { tracing::error!(?e); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); @@ -195,6 +175,7 @@ pub async fn try_one( } } + tracing::trace!("StatusCode::NOT_FOUND"); StatusCode::NOT_FOUND.into_response() } @@ -215,8 +196,8 @@ fn match_one( } } -fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> MethodRouter { - match &route.kind { +fn to_method_router(path: &str, route_kind: &RouteKind, ctx: &RuntimeCtx) -> MethodRouter { + match route_kind { RouteKind::Raw(raw) => any_service(serve_raw_one.with_state(raw.clone())), RouteKind::Proxy(proxy) => { let proxy_config = ProxyConfig { @@ -229,29 +210,32 @@ fn to_method_router(path: &str, route: &Route, ctx: &RuntimeCtx) -> MethodRouter any(proxy_with_decompression) } RouteKind::Dir(dir_route) => { - let svc = match &dir_route.base { + tracing::trace!(?dir_route); + match &dir_route.base { Some(base_dir) => { tracing::trace!( "combining root: `{}` with given path: `{}`", base_dir.display(), dir_route.dir ); - ServeDir::new(base_dir.join(&dir_route.dir)) + get_service(ServeDir::new(base_dir.join(&dir_route.dir))) } None => { let pb = PathBuf::from(&dir_route.dir); - if pb.is_absolute() { + if pb.is_file() { + get_service(ServeFile::new(pb)) + } else if pb.is_absolute() { trace!("no root given, using `{}` directly", dir_route.dir); - ServeDir::new(&dir_route.dir) + get_service( + ServeDir::new(&dir_route.dir).append_index_html_on_directories(true), + ) } else { let joined = ctx.cwd().join(pb); trace!(?joined, "serving"); - ServeDir::new(joined) + get_service(ServeDir::new(joined).append_index_html_on_directories(true)) } } } - .append_index_html_on_directories(true); - get_service(svc) } } } diff --git a/crates/bsnext_core/src/server/router/mod.rs b/crates/bsnext_core/src/server/router/mod.rs index 1ff897e6..5aaeecc5 100644 --- a/crates/bsnext_core/src/server/router/mod.rs +++ b/crates/bsnext_core/src/server/router/mod.rs @@ -9,6 +9,7 @@ use axum::routing::{get, MethodRouter}; use axum::{http, middleware, Extension, Router}; use crate::meta::MetaData; +use crate::not_found::not_found_service::not_found_loader; use crate::server::router::assets::pub_ui_assets; use crate::server::router::pub_api::pub_api; use crate::server::state::ServerState; @@ -105,9 +106,7 @@ pub fn dynamic_loaders(state: Arc) -> Router { .layer( ServiceBuilder::new() .layer(from_fn_with_state(state.clone(), tagging_layer)) - // .layer(from_fn_with_state(state.clone(), maybe_proxy)) - // todo(alpha): have the order listed here instead: static -> dir -> proxy - // .layer(from_fn_with_state(state.clone(), not_found_loader)) + .layer(from_fn_with_state(state.clone(), not_found_loader)) .layer(from_fn_with_state(state.clone(), dynamic_router)), ) .layer(CatchPanicLayer::custom(handle_panic)) diff --git a/playwright.config.ts b/playwright.config.ts index 746c3f92..b1a0b73c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -22,7 +22,8 @@ export default defineConfig({ // workers: process.env.CI ? 1 : undefined, workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", + reporter: [["html", { open: "never" }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ From bbb5934a9fd6e5eb6e4ca8a7e212759ab30a8cd6 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 10 Jul 2025 21:39:56 +0100 Subject: [PATCH 08/13] limit playgrounds / to exact --- crates/bsnext_input/src/playground.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/bsnext_input/src/playground.rs b/crates/bsnext_input/src/playground.rs index fc2ce6b3..acdddd83 100644 --- a/crates/bsnext_input/src/playground.rs +++ b/crates/bsnext_input/src/playground.rs @@ -1,5 +1,6 @@ use crate::path_def::PathDef; -use crate::route::{FallbackRoute, Opts, Route, RouteKind}; +use crate::route::{FallbackRoute, ListOrSingle, Opts, Route, RouteKind}; +use crate::when_guard::WhenGuard; use bsnext_guards::path_matcher::PathMatcher; use bsnext_guards::MatcherList; use bsnext_resp::builtin_strings::{BuiltinStringDef, BuiltinStrings}; @@ -44,7 +45,9 @@ impl Playground { kind: RouteKind::new_html(FALLBACK_HTML), opts: Default::default(), }), - when: Default::default(), + when: Some(ListOrSingle::WhenOne(WhenGuard::ExactUri { + exact_uri: true, + })), }; let js_route = Route { path: js, From b8859f8068bbfe3218fa2a4b52ea09809b8a7275 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 11 Jul 2025 11:35:28 +0100 Subject: [PATCH 09/13] make it impossible to provide incorrect path --- Cargo.lock | 2 + _bslive.yml | 3 + crates/bsnext_guards/Cargo.toml | 2 + crates/bsnext_guards/src/lib.rs | 1 + crates/bsnext_guards/src/path_matcher.rs | 51 +++++++++----- crates/bsnext_guards/src/root_path.rs | 70 +++++++++++++++++++ crates/bsnext_input/src/playground.rs | 21 +++--- crates/bsnext_input/src/server_config.rs | 12 ++-- crates/bsnext_md/tests/md_playground.rs | 2 +- .../md_playground__md_playground.snap | 20 ++++-- crates/bsnext_resp/src/inject_opt_test/mod.rs | 6 +- 11 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 crates/bsnext_guards/src/root_path.rs diff --git a/Cargo.lock b/Cargo.lock index ac11ebf3..9f5c3c4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -588,9 +588,11 @@ dependencies = [ name = "bsnext_guards" version = "0.18.0" dependencies = [ + "anyhow", "axum", "http", "serde", + "thiserror", "tracing", "urlpattern", ] diff --git a/_bslive.yml b/_bslive.yml index cc6715a1..e31de230 100644 --- a/_bslive.yml +++ b/_bslive.yml @@ -4,6 +4,9 @@ servers: - path: /api proxy: https://example.com rewrite_uri: false + inject: + - append: 'abc' + only: /index.html - port: 3002 routes: - path: / diff --git a/crates/bsnext_guards/Cargo.toml b/crates/bsnext_guards/Cargo.toml index ec3e6829..6574b242 100644 --- a/crates/bsnext_guards/Cargo.toml +++ b/crates/bsnext_guards/Cargo.toml @@ -9,3 +9,5 @@ serde = { workspace = true } tracing = { workspace = true } axum = { workspace = true } http = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/bsnext_guards/src/lib.rs b/crates/bsnext_guards/src/lib.rs index 5cb6a134..2cd3402a 100644 --- a/crates/bsnext_guards/src/lib.rs +++ b/crates/bsnext_guards/src/lib.rs @@ -5,6 +5,7 @@ use axum::response::IntoResponse; use http::Uri; pub mod path_matcher; +pub mod root_path; pub mod route_guard; #[derive(Debug, Default, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] diff --git a/crates/bsnext_guards/src/path_matcher.rs b/crates/bsnext_guards/src/path_matcher.rs index 87a9a6ad..95a3e571 100644 --- a/crates/bsnext_guards/src/path_matcher.rs +++ b/crates/bsnext_guards/src/path_matcher.rs @@ -1,5 +1,7 @@ +use crate::root_path::{RootPath, RootPathError}; use http::Uri; use std::str::FromStr; +use tracing::trace_span; use urlpattern::UrlPatternInit; use urlpattern::UrlPatternMatchInput; use urlpattern::{UrlPattern, UrlPatternOptions}; @@ -7,14 +9,19 @@ use urlpattern::{UrlPattern, UrlPatternOptions}; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] #[serde(untagged)] pub enum PathMatcher { - Str(String), + Str(#[serde(with = "crate::root_path")] RootPath), Def(PathMatcherDef), } -impl PathMatcher { - pub fn str(str: impl Into) -> Self { - Self::Str(str.into()) +impl FromStr for PathMatcher { + type Err = RootPathError; + + fn from_str(s: &str) -> Result { + Ok(PathMatcher::Str(RootPath::from_str(s)?)) } +} + +impl PathMatcher { pub fn pathname(str: impl Into) -> Self { Self::Def(PathMatcherDef { pathname: Some(str.into()), @@ -37,6 +44,8 @@ pub struct PathMatcherDef { impl PathMatcher { pub fn test_uri(&self, uri: &Uri) -> bool { + let span = trace_span!("test_uri", uri = ?uri, path_matcher = ?self); + let _g = span.enter(); let Some(path_and_query) = uri.path_and_query() else { tracing::error!("how is this possible?"); return false; @@ -51,11 +60,13 @@ impl PathMatcher { ..Default::default() }; + tracing::trace!(?incoming); + // convert the config into UrlPatternInit // example: /style.css let matching_options: UrlPatternInit = match self { PathMatcher::Str(str) => { - if let Ok(uri) = &Uri::from_str(str) { + if let Ok(uri) = &Uri::from_str(str.as_str()) { if let Some(pq) = uri.path_and_query() { let path = pq.path(); let query = pq.query(); @@ -65,11 +76,11 @@ impl PathMatcher { ..Default::default() } } else { - tracing::error!("could not parse the matching string you gave {}", str); + tracing::trace!(?str, "path and query was missing"); Default::default() } } else { - tracing::error!("could not parse the matching string you gave {}", str); + tracing::trace!(?str, "Uri::from_str failed"); Default::default() } } @@ -80,11 +91,14 @@ impl PathMatcher { }, }; let opts = UrlPatternOptions::default(); + tracing::trace!(?opts); + let Ok(pattern) = ::parse(matching_options, opts) else { tracing::error!("could not parse the input"); return false; }; - // dbg!(&incoming); + tracing::trace!(?pattern); + match pattern.test(UrlPatternMatchInput::Init(incoming)) { Ok(true) => { tracing::trace!("matched!"); @@ -106,6 +120,7 @@ impl PathMatcher { mod test { use crate::path_matcher::PathMatcher; use http::Uri; + use std::str::FromStr; #[test] fn test_url_pattern_pathname() { @@ -128,14 +143,14 @@ mod test { ); } #[test] - fn test_url_pattern_query() { - let pm = PathMatcher::str("/?abc=true"); + fn test_url_pattern_query() -> anyhow::Result<()> { + let pm = PathMatcher::from_str("/?abc=true")?; assert_eq!(pm.test_uri(&Uri::from_static("/")), false); assert_eq!(pm.test_uri(&Uri::from_static("/?def=true")), false); assert_eq!(pm.test_uri(&Uri::from_static("/?abc=true")), true); assert_eq!(pm.test_uri(&Uri::from_static("/?abc=")), false); - let pm2 = PathMatcher::str("/**/*?delayms"); + let pm2 = PathMatcher::from_str("/**/*?delayms")?; assert_eq!(pm2.test_uri(&Uri::from_static("/?delayms")), true); let pm2 = PathMatcher::query("?*a*b*c*foo=bar"); @@ -143,18 +158,19 @@ mod test { pm2.test_uri(&Uri::from_static("/?delay.ms=2000&a-b-c-foo=bar")), true ); + Ok(()) } #[test] - fn test_url_pattern_str() { - let pm = PathMatcher::str("/"); + fn test_url_pattern_str() -> anyhow::Result<()> { + let pm = PathMatcher::from_str("/")?; assert_eq!(pm.test_uri(&Uri::from_static("/")), true); - let pm = PathMatcher::str("/*.css"); + let pm = PathMatcher::from_str("/*.css")?; assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); - let pm = PathMatcher::str("/here/*.css"); + let pm = PathMatcher::from_str("/here/*.css")?; assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), false); - let pm = PathMatcher::str("/**/*.css"); + let pm = PathMatcher::from_str("/**/*.css")?; assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); - let pm = PathMatcher::str("/**/*.css"); + let pm = PathMatcher::from_str("/**/*.css")?; assert_eq!( pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.css")), true @@ -163,5 +179,6 @@ mod test { pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.html")), false ); + Ok(()) } } diff --git a/crates/bsnext_guards/src/root_path.rs b/crates/bsnext_guards/src/root_path.rs new file mode 100644 index 00000000..ba23ba93 --- /dev/null +++ b/crates/bsnext_guards/src/root_path.rs @@ -0,0 +1,70 @@ +use serde::{de, Deserialize, Deserializer, Serializer}; +use std::path::PathBuf; +use std::str::FromStr; + +#[derive(Debug, PartialEq, Hash, Clone)] +pub struct RootPath { + inner: String, +} + +#[derive(Debug, PartialEq, Hash, Clone, thiserror::Error)] +pub enum RootPathError { + #[error("must start with forward slash")] + MissingSlash, +} + +impl FromStr for RootPath { + type Err = RootPathError; + + fn from_str(s: &str) -> Result { + Self::try_new(s) + } +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) +} + +pub fn serialize(x: &RootPath, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(x.inner.as_str()) +} + +impl RootPath { + pub fn as_pb(&self) -> PathBuf { + PathBuf::from(&self.inner) + } + pub fn as_str(&self) -> &str { + self.inner.as_str() + } + pub fn try_new>(input: A) -> Result { + let str = input.as_ref(); + let is_path = str.starts_with("/"); + if !is_path { + return Err(RootPathError::MissingSlash); + } + Ok(Self { + inner: String::from(str), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_verify() -> anyhow::Result<()> { + let input = "abc/one"; + let actual = RootPath::try_new(input).unwrap_err(); + assert_eq!(actual, RootPathError::MissingSlash); + let input = "/abc/one"; + let _actual = RootPath::try_new(input).unwrap(); + Ok(()) + } +} diff --git a/crates/bsnext_input/src/playground.rs b/crates/bsnext_input/src/playground.rs index acdddd83..553e317f 100644 --- a/crates/bsnext_input/src/playground.rs +++ b/crates/bsnext_input/src/playground.rs @@ -6,6 +6,7 @@ use bsnext_guards::MatcherList; use bsnext_resp::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use bsnext_resp::inject_addition::{AdditionPosition, InjectAddition}; use bsnext_resp::inject_opts::{InjectOpts, Injection, InjectionItem}; +use std::str::FromStr; #[derive(Debug, PartialEq, Default, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct Playground { @@ -17,7 +18,7 @@ pub struct Playground { const FALLBACK_HTML: &str = "This is a BSLIVE playground"; impl Playground { - pub fn as_routes(&self) -> Vec { + pub fn as_routes(&self) -> anyhow::Result> { let home_path = PathDef::try_new("/"); let js_path = PathDef::try_new("/__bslive_playground.js"); let css_path = PathDef::try_new("/__bslive_playground.css"); @@ -26,7 +27,7 @@ impl Playground { let (Ok(home), Ok(js), Ok(css), Ok(reset_path)) = (home_path, js_path, css_path, reset_path) else { - return vec![]; + return Ok(vec![]); }; let home_route = Route { @@ -36,7 +37,7 @@ impl Playground { cors: None, delay: None, watch: Default::default(), - inject: playground_wrap(), + inject: playground_wrap()?, headers: None, compression: None, ..Default::default() @@ -72,11 +73,11 @@ impl Playground { kind: RouteKind::new_raw(include_str!("../../../ui/styles/reset.css")), ..Default::default() }; - vec![home_route, js_route, css_route, reset_route] + Ok(vec![home_route, js_route, css_route, reset_route]) } } -fn playground_wrap() -> InjectOpts { +fn playground_wrap() -> anyhow::Result { let prepend = r#" @@ -93,24 +94,24 @@ fn playground_wrap() -> InjectOpts { "#; - InjectOpts::Items(vec![ + Ok(InjectOpts::Items(vec![ InjectionItem { inner: Injection::Addition(InjectAddition { addition_position: AdditionPosition::Prepend(prepend.to_string()), }), - only: Some(MatcherList::Item(PathMatcher::Str("/".to_string()))), + only: Some(MatcherList::Item(PathMatcher::from_str("/")?)), }, InjectionItem { inner: Injection::Addition(InjectAddition { addition_position: AdditionPosition::Append(append.to_string()), }), - only: Some(MatcherList::Item(PathMatcher::Str("/".to_string()))), + only: Some(MatcherList::Item(PathMatcher::from_str("/")?)), }, InjectionItem { inner: Injection::BsLive(BuiltinStringDef { name: BuiltinStrings::Connector, }), - only: Some(MatcherList::Item(PathMatcher::Str("/".to_string()))), + only: Some(MatcherList::Item(PathMatcher::from_str("/")?)), }, - ]) + ])) } diff --git a/crates/bsnext_input/src/server_config.rs b/crates/bsnext_input/src/server_config.rs index b49b60bd..6f43c36e 100644 --- a/crates/bsnext_input/src/server_config.rs +++ b/crates/bsnext_input/src/server_config.rs @@ -29,11 +29,13 @@ impl ServerConfig { let routes = self.routes.clone(); match &self.playground { None => self.routes.clone(), - Some(playground) => { - let mut pg_routes = playground.as_routes(); - pg_routes.extend(routes); - pg_routes - } + Some(playground) => match playground.as_routes() { + Ok(mut pg_routes) => { + pg_routes.extend(routes); + pg_routes + } + Err(_) => routes, + }, } } pub fn raw_routes(&self) -> &[Route] { diff --git a/crates/bsnext_md/tests/md_playground.rs b/crates/bsnext_md/tests/md_playground.rs index 3906fe48..91e65a88 100644 --- a/crates/bsnext_md/tests/md_playground.rs +++ b/crates/bsnext_md/tests/md_playground.rs @@ -33,7 +33,7 @@ console.log("hello world") let routes = first_server .playground .as_ref() - .map(|x| x.as_routes()) + .map(|x| x.as_routes().unwrap()) .unwrap(); insta::assert_debug_snapshot!(routes); Ok(()) diff --git a/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap b/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap index 11f10fec..81f37746 100644 --- a/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap +++ b/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap @@ -31,7 +31,9 @@ expression: routes only: Some( Item( Str( - "/", + RootPath { + inner: "/", + }, ), ), ), @@ -47,7 +49,9 @@ expression: routes only: Some( Item( Str( - "/", + RootPath { + inner: "/", + }, ), ), ), @@ -61,7 +65,9 @@ expression: routes only: Some( Item( Str( - "/", + RootPath { + inner: "/", + }, ), ), ), @@ -94,7 +100,13 @@ expression: routes }, }, ), - when: None, + when: Some( + WhenOne( + ExactUri { + exact_uri: true, + }, + ), + ), }, Route { path: PathDef { diff --git a/crates/bsnext_resp/src/inject_opt_test/mod.rs b/crates/bsnext_resp/src/inject_opt_test/mod.rs index fcd14769..a8f91149 100644 --- a/crates/bsnext_resp/src/inject_opt_test/mod.rs +++ b/crates/bsnext_resp/src/inject_opt_test/mod.rs @@ -191,6 +191,7 @@ fn test_inject_append_prepend() { #[test] fn test_path_matchers() { + use std::str::FromStr; #[derive(Debug, serde::Deserialize)] struct A { inject: InjectOpts, @@ -208,7 +209,7 @@ inject: addition_position: AdditionPosition::Append("lol".to_string()), }), only: Some(MatcherList::Items(vec![ - PathMatcher::str("/*.css"), + PathMatcher::from_str("/*.css").unwrap(), PathMatcher::pathname("/*.css"), ])), }), @@ -219,6 +220,7 @@ inject: #[test] fn test_path_matcher_single() { + use std::str::FromStr; #[derive(Debug, serde::Deserialize)] struct A { inject: InjectOpts, @@ -233,7 +235,7 @@ fn test_path_matcher_single() { inner: Injection::Addition(InjectAddition { addition_position: AdditionPosition::Append("lol".to_string()), }), - only: Some(MatcherList::Item(PathMatcher::str("/*.css"))), + only: Some(MatcherList::Item(PathMatcher::from_str("/*.css").unwrap())), }), }; let actual: Result = serde_yaml::from_str(input); From 758d9851ee1d7c78edf4539f18920ca25817b1cf Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 11 Jul 2025 12:24:21 +0100 Subject: [PATCH 10/13] allow configurable HTTP logging --- crates/bsnext_core/src/server/router/mod.rs | 6 ++-- crates/bsnext_core/src/shared_args.rs | 4 +++ crates/bsnext_fs/examples/watch_cmd.rs | 5 +++- crates/bsnext_system/src/cli.rs | 8 ++++- crates/bsnext_tracing/src/lib.rs | 33 +++++++++++++++++---- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/crates/bsnext_core/src/server/router/mod.rs b/crates/bsnext_core/src/server/router/mod.rs index 5aaeecc5..18dfc6f2 100644 --- a/crates/bsnext_core/src/server/router/mod.rs +++ b/crates/bsnext_core/src/server/router/mod.rs @@ -28,6 +28,7 @@ use std::sync::Arc; use tower::{ServiceBuilder, ServiceExt}; use tower_http::catch_panic::CatchPanicLayer; use tower_http::cors::CorsLayer; +use tower_http::trace::TraceLayer; use tracing::{span, Level}; mod assets; @@ -43,10 +44,7 @@ pub fn make_router(state: &Arc) -> Router { .merge(dynamic_loaders(state.clone())); router - // todo(alpha): enable tracing on a per-item basis? - // .layer(TraceLayer::new_for_http()) - // todo(alpha): re-enable in the correct place? - // .layer(TimeoutLayer::new(Duration::from_secs(10))) + .layer(TraceLayer::new_for_http()) .layer(Extension(client)) // todo: When to add this compression back in? // .layer(CompressionLayer::new()) diff --git a/crates/bsnext_core/src/shared_args.rs b/crates/bsnext_core/src/shared_args.rs index e6286cc7..5de91445 100644 --- a/crates/bsnext_core/src/shared_args.rs +++ b/crates/bsnext_core/src/shared_args.rs @@ -10,6 +10,10 @@ pub struct LoggingOpts { #[arg(long, name = "write-log")] pub write_log: bool, + /// allow http-level logging overrides + #[arg(long, name = "log-http")] + pub log_http: Option, + /// output internal logs to bslive.log in the current directory #[arg(long)] pub filenames: bool, diff --git a/crates/bsnext_fs/examples/watch_cmd.rs b/crates/bsnext_fs/examples/watch_cmd.rs index f71973b6..15137af4 100644 --- a/crates/bsnext_fs/examples/watch_cmd.rs +++ b/crates/bsnext_fs/examples/watch_cmd.rs @@ -10,7 +10,10 @@ use tokio::process::Command; #[actix_rt::main] async fn main() -> anyhow::Result<()> { - let str = bsnext_tracing::level(Some(bsnext_tracing::LogLevel::Trace)); + let str = bsnext_tracing::level( + Some(bsnext_tracing::LogLevel::Trace), + bsnext_tracing::LogHttp::Off, + ); let with = format!("{str},watch_cmd=trace"); bsnext_tracing::raw_tracing::init_tracing_subscriber( &with, diff --git a/crates/bsnext_system/src/cli.rs b/crates/bsnext_system/src/cli.rs index c40f09f4..dcd676eb 100644 --- a/crates/bsnext_system/src/cli.rs +++ b/crates/bsnext_system/src/cli.rs @@ -41,7 +41,13 @@ where }; let format = args.format(); - init_tracing(logging.log_level, format, write_log_opt, line_opts); + init_tracing( + logging.log_level, + logging.log_http.unwrap_or_default(), + format, + write_log_opt, + line_opts, + ); tracing::debug!("{:#?}", args); diff --git a/crates/bsnext_tracing/src/lib.rs b/crates/bsnext_tracing/src/lib.rs index 262ee51f..a3972254 100644 --- a/crates/bsnext_tracing/src/lib.rs +++ b/crates/bsnext_tracing/src/lib.rs @@ -75,27 +75,48 @@ pub enum OtelOption { Off, } +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum LogHttp { + #[default] + On, + Off, +} + pub fn init_tracing( log_level: Option, + log_http: LogHttp, format: OutputFormat, write_option: WriteOption, line_opts: LineNumberOption, ) -> Option<()> { - let level = level(log_level); + let level = level(log_level, log_http); raw_tracing::init_tracing_subscriber(&level, format, write_option, line_opts); None::<()> } -pub fn level(log_level: Option) -> String { - match log_level { - None => String::new(), - Some(level) => { +pub fn level(log_level: Option, log_http: LogHttp) -> String { + match (log_level, log_http) { + (None, LogHttp::Off) => String::new(), + (None, LogHttp::On) => "tower_http=debug".to_string(), + (Some(level), LogHttp::On) => { + let level = level.to_string(); + let lines = [ + format!("bsnext={level}"), + "bsnext_fs::stream=info".to_string(), + "bsnext_fs::buffered_debounce=info".to_string(), + format!("tower_http=debug"), + // "bsnext_fs::watcher=info".to_string(), + // "bsnext_core::server_actor=info".to_string(), + ]; + + lines.join(",") + } + (Some(level), LogHttp::Off) => { let level = level.to_string(); let lines = [ format!("bsnext={level}"), "bsnext_fs::stream=info".to_string(), "bsnext_fs::buffered_debounce=info".to_string(), - // format!("tower_http={level}"), // "bsnext_fs::watcher=info".to_string(), // "bsnext_core::server_actor=info".to_string(), ]; From d89fb16dae7640c43ddeccd0231b7f7f68d12cab Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 16 Jul 2025 11:25:45 +0100 Subject: [PATCH 11/13] implement a basic json body guard --- crates/bsnext_core/src/handler_stack.rs | 237 ++++++++++++++++-- crates/bsnext_core/src/proxy_loader.rs | 87 ++++++- crates/bsnext_guards/src/route_guard.rs | 5 + ...ut__input_test__deserialize_3_headers.snap | 1 + ...t_test__deserialize_3_headers_control.snap | 1 + crates/bsnext_input/src/playground.rs | 1 + crates/bsnext_input/src/route.rs | 4 +- ...ext_input__route_cli__test__serve_dir.snap | 1 + ...put__route_cli__test__serve_dir_delay.snap | 1 + ...ut__route_cli__test__serve_dir_shared.snap | 1 + crates/bsnext_input/src/when_guard.rs | 76 ++++-- .../md_playground__md_playground.snap | 4 + ..._kind__start_from_paths__test__test-2.snap | 1 + ...rt_kind__start_from_paths__test__test.snap | 1 + crates/bsnext_tracing/src/lib.rs | 2 +- 15 files changed, 382 insertions(+), 41 deletions(-) diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index c70c5e88..50503396 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -12,15 +12,20 @@ use axum::{Extension, Router}; use bsnext_guards::route_guard::RouteGuard; use bsnext_guards::{uri_extension, OuterUri}; use bsnext_input::route::{ListOrSingle, Route, RouteKind}; -use bsnext_input::when_guard::{HasGuard, WhenGuard}; +use bsnext_input::when_guard::{HasGuard, JsonGuard, JsonPropGuard, WhenBodyGuard, WhenGuard}; +use bytes::Bytes; +use http::header::CONTENT_TYPE; use http::request::Parts; use http::uri::PathAndQuery; -use http::{Response, StatusCode, Uri}; +use http::{Method, Response, StatusCode, Uri}; +use http_body_util::BodyExt; +use serde_json::Value; use std::collections::HashMap; +use std::ops::ControlFlow; use std::path::PathBuf; use tower::ServiceExt; use tower_http::services::{ServeDir, ServeFile}; -use tracing::{trace, trace_span}; +use tracing::{debug, trace, trace_span}; pub struct RouteMap { pub mapping: HashMap>, @@ -84,9 +89,9 @@ pub async fn try_one( let pq = outer_uri.path_and_query(); trace!(?parts); - trace!("will try {} routes", routes.len()); + debug!("will try {} candidate routes", routes.len()); - let candidates = routes + let candidates: Vec = routes .iter() .enumerate() .filter(|(index, route)| { @@ -95,6 +100,7 @@ pub async fn try_one( trace!(?route.kind); + // early checks from parts only let can_serve: bool = route .when .as_ref() @@ -106,36 +112,60 @@ pub async fn try_one( }) .unwrap_or(true); + // if this routes wants to inspect the body, check it was a POST + let can_consume = match &route.when_body { + None => true, + Some(when_body) => { + let consuming = NeedsJsonGuard(when_body).accept_req(&req, &outer_uri); + trace!(route.when_body.json = consuming); + consuming + } + }; + trace!(?can_serve); + trace!(?can_consume); - if !can_serve { + if !can_serve || !can_consume { return false; } true }) + .map(|(index, route)| { + let consume = route + .when_body + .as_ref() + .is_some_and(|body| NeedsJsonGuard(body).accept_req(&req, &outer_uri)); + RouteCandidate { + index, + consume, + route, + } + }) .collect::>(); - trace!("{} candidates", candidates.len()); + debug!("{} candidates passed early checks", candidates.len()); - let mut body: Option = candidates.last().and_then(|(_, route)| { - if matches!(route.kind, RouteKind::Proxy(..)) { - trace!("will consume body for proxy"); - Some(req.into_body()) - } else { - None - } - }); + let mut body: Option = Some(req.into_body()); - for (index, route) in &candidates { - let span = trace_span!("index", index = index); + 'find_candidates: for candidate in &candidates { + let span = trace_span!("index", index = candidate.index); let _g = span.enter(); trace!(?parts); - let method_router = to_method_router(&path, &route.kind, &ctx); - let raw_out: MethodRouter = optional_layers(method_router, &route.opts); - let req_clone = match route.kind { + let (next_body, control_flow) = candidate.try_exec(&mut body).await; + if next_body.is_some() { + body = next_body + } + + if control_flow.is_break() { + continue 'find_candidates; + } + + let method_router = to_method_router(&path, &candidate.route.kind, &ctx); + let raw_out: MethodRouter = optional_layers(method_router, &candidate.route.opts); + let req_clone = match candidate.route.kind { RouteKind::Raw(_) => Request::from_parts(parts.clone(), Body::empty()), RouteKind::Proxy(_) => { if let Some(body) = body.take() { @@ -151,8 +181,9 @@ pub async fn try_one( match result { Ok(result) => match result.status().as_u16() { + // todo: this is way too simplistic, it should allow 404 being deliberately returned etc 404 | 405 => { - if let Some(fallback) = &route.fallback { + if let Some(fallback) = &candidate.route.fallback { let mr = to_method_router(&path, &fallback.kind, &ctx); let raw_out: MethodRouter = optional_layers(mr, &fallback.opts); let raw_fb = Request::from_parts(parts.clone(), Body::empty()); @@ -161,7 +192,7 @@ pub async fn try_one( } _ => { trace!( - ?index, + ?candidate.index, " - ✅ a non-404 response was given {}", result.status() ); @@ -179,6 +210,24 @@ pub async fn try_one( StatusCode::NOT_FOUND.into_response() } +#[derive(Debug)] +struct RouteCandidate<'a> { + index: usize, + route: &'a Route, + consume: bool, +} + +impl RouteCandidate<'_> { + pub async fn try_exec(&self, body: &mut Option) -> (Option, ControlFlow<()>) { + if self.consume { + trace!("trying to collect body because candidate needs it"); + match_json_body(body, self.route).await + } else { + (None, ControlFlow::Continue(())) + } + } +} + fn match_one( when_guard: &WhenGuard, outer_uri: &Uri, @@ -289,6 +338,150 @@ impl RouteGuard for AcceptHasGuard<'_> { } } +struct NeedsJsonGuard<'a>(pub &'a ListOrSingle); +impl RouteGuard for NeedsJsonGuard<'_> { + #[tracing::instrument(skip_all, name = "NeedsJsonGuard.accept_req")] + fn accept_req(&self, req: &Request, _outer_uri: &Uri) -> bool { + let exec = match self.0 { + ListOrSingle::WhenOne(WhenBodyGuard::Json { .. }) => true, + ListOrSingle::WhenMany(items) => items + .iter() + .any(|item| matches!(item, WhenBodyGuard::Json { .. })), + _ => false, + }; + trace!(?exec); + if !exec { + return false; + } + let headers = req.headers(); + let method = req.method(); + let json = headers.get(CONTENT_TYPE).is_some_and(|header| { + header + .to_str() + .ok() + .map(|h| h.contains("application/json")) + .unwrap_or(false) + }); + trace!(?json, ?method, ?headers); + json && method == Method::POST + } + + fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool { + true + } +} + +impl NeedsJsonGuard<'_> { + pub fn match_body(&self, value: &Value) -> bool { + let matches: Vec<(&'_ WhenBodyGuard, bool)> = match self.0 { + ListOrSingle::WhenOne(one) => vec![(one, match_one_json(value, one))], + ListOrSingle::WhenMany(many) => many + .iter() + .map(|guard| (guard, match_one_json(value, guard))) + .collect(), + }; + matches.iter().all(|(_item, result)| *result) + } +} + +pub fn match_one_json(value: &Value, when_body_guard: &WhenBodyGuard) -> bool { + match when_body_guard { + WhenBodyGuard::Json { json } => match json { + JsonGuard::ArrayLast { items, last } => match value.pointer(items) { + Some(Value::Array(arr)) => match arr.last() { + None => false, + Some(last_item) => last.iter().all(|prop_guard| match prop_guard { + JsonPropGuard::PathIs { path, is } => match last_item.pointer(path) { + Some(Value::String(val_string)) => val_string == is, + _ => false, + }, + JsonPropGuard::PathHas { path, has } => match last_item.pointer(path) { + Some(Value::String(val_string)) => val_string.contains(has), + _ => false, + }, + }), + }, + _ => false, + }, + JsonGuard::ArrayAny { items, any } => match value.pointer(items) { + Some(Value::Array(arr)) if arr.is_empty() => false, + Some(Value::Array(val)) => val + .iter() + .any(|val| any.iter().any(|prop_guard| match_prop(val, prop_guard))), + _ => false, + }, + JsonGuard::ArrayAll { items, all } => match value.pointer(items) { + Some(Value::Array(arr)) if arr.is_empty() => false, + Some(Value::Array(arr)) => arr + .iter() + .any(|one_val| all.iter().all(|guard| match_prop(one_val, guard))), + _ => false, + }, + JsonGuard::Path(pg) => match_prop(value, pg), + }, + WhenBodyGuard::Never => false, + } +} + +async fn match_json_body( + body: &mut Option, + route: &Route, +) -> (Option, ControlFlow<()>) { + if let Some(inner_body) = body.take() { + let collected = inner_body.collect(); + let bytes = match collected.await { + Ok(collected) => collected.to_bytes(), + Err(err) => { + tracing::error!(?err, "could not collect bytes..."); + Bytes::new() + } + }; + + trace!("did collect {} bytes", bytes.len()); + + match serde_json::from_slice(bytes.iter().as_slice()) { + Ok(value) => { + let result = route + .when_body + .as_ref() + .map(|when_body| NeedsJsonGuard(when_body).match_body(&value)); + if result.is_some_and(|res| !res) { + trace!("ignoring, `when_body` was present, but didn't match the guards"); + trace!("restoring body from clone"); + (Some(Body::from(bytes)), ControlFlow::Break(())) + } else { + if result.is_some() { + trace!("✅ when_body produced a valid match"); + } else { + trace!("when_body didn't produce a value"); + } + (Some(Body::from(bytes)), ControlFlow::Continue(())) + } + } + Err(err) => { + tracing::error!(?err, "could not deserialize into Value"); + (Some(Body::from(bytes)), ControlFlow::Continue(())) + } + } + } else { + trace!("could not .take() body"); + (None, ControlFlow::Continue(())) + } +} + +pub fn match_prop(value: &Value, prop_guard: &JsonPropGuard) -> bool { + match prop_guard { + JsonPropGuard::PathIs { path, is } => match value.pointer(path) { + Some(Value::String(val_string)) => val_string == is, + _ => false, + }, + JsonPropGuard::PathHas { path, has } => match value.pointer(path) { + Some(Value::String(val_string)) => val_string.contains(has), + _ => false, + }, + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/bsnext_core/src/proxy_loader.rs b/crates/bsnext_core/src/proxy_loader.rs index 005b5536..4b58233a 100644 --- a/crates/bsnext_core/src/proxy_loader.rs +++ b/crates/bsnext_core/src/proxy_loader.rs @@ -3,9 +3,10 @@ mod test { use crate::handler_stack::RouteMap; use crate::runtime_ctx::RuntimeCtx; - use crate::server::router::common::{test_proxy, to_resp_parts_and_body}; + use crate::server::router::common::{test_proxy, to_resp_body, to_resp_parts_and_body}; use axum::body::Body; use axum::extract::Request; + use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Extension, Json, Router}; use bsnext_input::route::Route; @@ -118,6 +119,90 @@ mod test { Ok(()) } + #[tokio::test] + async fn test_post_guard() -> anyhow::Result<()> { + let https = HttpsConnector::new(); + let client: Client, Body> = + Client::builder(TokioExecutor::new()).build(https); + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + struct Person { + name: String, + } + + async fn handler(Json(mut person): Json) -> impl IntoResponse { + dbg!(&person); + person.name = format!("-->{}", person.name); + Json(person) + } + + let proxy_app = Router::new() + .route("/did-post", post(handler)) + .route("/control", post(handler)); + + let proxy = test_proxy(proxy_app).await?; + + let routes_input = format!( + r#" + - path: /did-post + rewrite_uri: false + proxy: {http} + when_body: + json: + path: "/name" + is: "shane" + - path: /control + rewrite_uri: false + proxy: {http} + when_body: + json: + path: "/name" + is: "kittie" + "#, + http = proxy.http_addr + ); + + let routes = serde_yaml::from_str::>(&routes_input)?; + let router = RouteMap::new_from_routes(&routes) + .into_router(&RuntimeCtx::default()) + .layer(Extension(client)); + + let expected_response_body = "{\"name\":\"-->shane\"}"; + + let outgoing_body = serde_json::to_string(&json!({ + "name": "shane" + }))?; + + // expected + { + // Define the request + let request = Request::post("/did-post") + .header("Content-Type", "application/json") + .body(outgoing_body.clone())?; + + // Make a one-shot request on the router + let response = router.clone().oneshot(request).await?; + let body = to_resp_body(response).await; + assert_eq!(body, expected_response_body); + } + + // control, this makes sure we can see a 404 + { + // Define the request + let request = Request::post("/control") + .header("Content-Type", "application/json") + .body(outgoing_body.clone())?; + + // Make a one-shot request on the router + let response = router.oneshot(request).await?; + assert_eq!(response.status().as_u16(), 404); + } + + proxy.destroy().await?; + + Ok(()) + } + #[tokio::test] async fn test_path_rewriting() -> anyhow::Result<()> { let https = HttpsConnector::new(); diff --git a/crates/bsnext_guards/src/route_guard.rs b/crates/bsnext_guards/src/route_guard.rs index 09e728bf..031d3100 100644 --- a/crates/bsnext_guards/src/route_guard.rs +++ b/crates/bsnext_guards/src/route_guard.rs @@ -1,3 +1,4 @@ +use axum::body::Body; use axum::extract::Request; use http::request::Parts; use http::{Response, Uri}; @@ -9,3 +10,7 @@ pub trait RouteGuard { true } } + +pub trait ConsumedRouteGuard { + fn accept_req(&self, parts: &Parts, body: Body, outer_uri: &Uri) -> bool; +} diff --git a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap index 4699a15c..44a0d7f4 100644 --- a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap +++ b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers.snap @@ -37,6 +37,7 @@ Config { }, fallback: None, when: None, + when_body: None, }, ], } diff --git a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap index 5f0369ab..3ed1f5f5 100644 --- a/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap +++ b/crates/bsnext_input/src/input_test/snapshots/bsnext_input__input_test__deserialize_3_headers_control.snap @@ -33,6 +33,7 @@ Config { }, fallback: None, when: None, + when_body: None, }, ], } diff --git a/crates/bsnext_input/src/playground.rs b/crates/bsnext_input/src/playground.rs index 553e317f..9dc6c80e 100644 --- a/crates/bsnext_input/src/playground.rs +++ b/crates/bsnext_input/src/playground.rs @@ -49,6 +49,7 @@ impl Playground { when: Some(ListOrSingle::WhenOne(WhenGuard::ExactUri { exact_uri: true, })), + when_body: None, }; let js_route = Route { path: js, diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index 545de8ae..b58fa340 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -1,7 +1,7 @@ use crate::path_def::PathDef; use crate::route_cli::RouteCli; use crate::watch_opts::WatchOpts; -use crate::when_guard::WhenGuard; +use crate::when_guard::{WhenBodyGuard, WhenGuard}; use bsnext_resp::cache_opts::CacheOpts; use bsnext_resp::inject_opts::InjectOpts; use matchit::InsertError; @@ -22,6 +22,7 @@ pub struct Route { pub opts: Opts, pub fallback: Option, pub when: Option>, + pub when_body: Option>, } #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -75,6 +76,7 @@ impl Default for Route { }, fallback: Default::default(), when: Default::default(), + when_body: None, } } } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap index 0775ddd9..a41a8ee3 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap @@ -27,4 +27,5 @@ Route { }, fallback: None, when: None, + when_body: None, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap index 674e95d9..cf3770e4 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap @@ -33,4 +33,5 @@ Route { }, fallback: None, when: None, + when_body: None, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap index eca5af63..6f87c1f5 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_shared.snap @@ -37,4 +37,5 @@ Route { }, fallback: None, when: None, + when_body: None, } diff --git a/crates/bsnext_input/src/when_guard.rs b/crates/bsnext_input/src/when_guard.rs index 4c74b2d9..97ef8a62 100644 --- a/crates/bsnext_input/src/when_guard.rs +++ b/crates/bsnext_input/src/when_guard.rs @@ -15,6 +15,66 @@ pub enum WhenGuard { }, } +#[derive(Debug, Default, PartialEq, Hash, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum WhenBodyGuard { + #[default] + Never, + Json { + json: JsonGuard, + }, +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum JsonGuard { + ArrayLast { + items: String, + last: Vec, + }, + ArrayAny { + items: String, + any: Vec, + }, + ArrayAll { + items: String, + all: Vec, + }, + Path(JsonPropGuard), +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum JsonPropGuard { + PathIs { path: String, is: String }, + PathHas { path: String, has: String }, +} + +impl JsonPropGuard { + pub fn path(&self) -> &str { + match self { + JsonPropGuard::PathIs { path, .. } => path, + JsonPropGuard::PathHas { path, .. } => path, + } + } +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum HasGuard { + /// A direct, exact match + Literal(String), + /// A direct, exact match + Is { is: String }, + /// Contains the substring + Has { has: String }, + /// Contains the substring + NotHas { + #[serde(rename = "not.has")] + not_has: String, + }, +} + #[test] fn test_when_guard() { use insta::assert_debug_snapshot; @@ -46,19 +106,3 @@ fn test_when_guard_has() { let when: A = serde_yaml::from_str(input).expect("test"); assert_debug_snapshot!(&when); } - -#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] -pub enum HasGuard { - /// A direct, exact match - Literal(String), - /// A direct, exact match - Is { is: String }, - /// Contains the substring - Has { has: String }, - /// Contains the substring - NotHas { - #[serde(rename = "not.has")] - not_has: String, - }, -} diff --git a/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap b/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap index 81f37746..a14f5a06 100644 --- a/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap +++ b/crates/bsnext_md/tests/snapshots/md_playground__md_playground.snap @@ -107,6 +107,7 @@ expression: routes }, ), ), + when_body: None, }, Route { path: PathDef { @@ -132,6 +133,7 @@ expression: routes }, fallback: None, when: None, + when_body: None, }, Route { path: PathDef { @@ -157,6 +159,7 @@ expression: routes }, fallback: None, when: None, + when_body: None, }, Route { path: PathDef { @@ -182,5 +185,6 @@ expression: routes }, fallback: None, when: None, + when_body: None, }, ] diff --git a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap index 91fd05a3..f1df15ff 100644 --- a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap +++ b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test-2.snap @@ -15,6 +15,7 @@ servers: compression: ~ fallback: ~ when: ~ + when_body: ~ watchers: [] playground: ~ clients: diff --git a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap index a1659e62..2610e896 100644 --- a/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap +++ b/crates/bsnext_system/src/start/start_kind/snapshots/bsnext_system__start__start_kind__start_from_paths__test__test.snap @@ -34,6 +34,7 @@ Input { }, fallback: None, when: None, + when_body: None, }, ], watchers: [], diff --git a/crates/bsnext_tracing/src/lib.rs b/crates/bsnext_tracing/src/lib.rs index a3972254..891945f5 100644 --- a/crates/bsnext_tracing/src/lib.rs +++ b/crates/bsnext_tracing/src/lib.rs @@ -104,7 +104,7 @@ pub fn level(log_level: Option, log_http: LogHttp) -> String { format!("bsnext={level}"), "bsnext_fs::stream=info".to_string(), "bsnext_fs::buffered_debounce=info".to_string(), - format!("tower_http=debug"), + "tower_http=debug".to_string(), // "bsnext_fs::watcher=info".to_string(), // "bsnext_core::server_actor=info".to_string(), ]; From 90cf08ba664ad4baef43aa7958747817c57fcb6e Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 18 Jul 2025 22:00:02 +0100 Subject: [PATCH 12/13] unstable mirror --- crates/bsnext_core/src/handler_stack.rs | 113 ++++++++++++++++++++++-- crates/bsnext_dto/src/lib.rs | 1 + crates/bsnext_input/src/route.rs | 7 ++ crates/bsnext_input/src/route_cli.rs | 1 + crates/bsnext_tracing/src/lib.rs | 2 +- 5 files changed, 116 insertions(+), 8 deletions(-) diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index 50503396..c14432d7 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -5,27 +5,36 @@ use crate::runtime_ctx::RuntimeCtx; use axum::body::Body; use axum::extract::{Request, State}; use axum::handler::Handler; -use axum::middleware::from_fn; +use axum::middleware::{from_fn, map_response, Next}; use axum::response::IntoResponse; use axum::routing::{any, any_service, get_service, MethodRouter}; -use axum::{Extension, Router}; +use axum::{middleware, Extension, Router}; +use axum_extra::middleware::option_layer; use bsnext_guards::route_guard::RouteGuard; use bsnext_guards::{uri_extension, OuterUri}; -use bsnext_input::route::{ListOrSingle, Route, RouteKind}; +use bsnext_input::route::{ListOrSingle, ProxyRoute, Route, RouteKind}; use bsnext_input::when_guard::{HasGuard, JsonGuard, JsonPropGuard, WhenBodyGuard, WhenGuard}; use bytes::Bytes; -use http::header::CONTENT_TYPE; +use http::header::{ACCEPT, CONTENT_TYPE}; use http::request::Parts; use http::uri::PathAndQuery; use http::{Method, Response, StatusCode, Uri}; use http_body_util::BodyExt; use serde_json::Value; use std::collections::HashMap; +use std::ffi::OsStr; +use std::io; use std::ops::ControlFlow; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tokio::fs::{create_dir_all, File}; +use tokio::io::{AsyncWriteExt, BufWriter}; +use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::StreamExt; use tower::ServiceExt; +use tower_http::decompression::DecompressionLayer; use tower_http::services::{ServeDir, ServeFile}; -use tracing::{debug, trace, trace_span}; +use tracing::{debug, error, trace, trace_span}; pub struct RouteMap { pub mapping: HashMap>, @@ -136,10 +145,28 @@ pub async fn try_one( .when_body .as_ref() .is_some_and(|body| NeedsJsonGuard(body).accept_req(&req, &outer_uri)); + + let css_req = req + .headers() + .get(ACCEPT) + .and_then(|h| h.to_str().ok()) + .map(|c| c.contains("text/css")) + .unwrap_or(false); + + let js_req = Path::new(req.uri().path()) + .extension() + .is_some_and(|ext| ext == OsStr::new("js")); + let mirror = if (css_req || js_req) { + RouteHelper(&route).mirror().map(|v| v.to_path_buf()) + } else { + None + }; + RouteCandidate { index, consume, route, + mirror, } }) .collect::>(); @@ -163,7 +190,18 @@ pub async fn try_one( continue 'find_candidates; } - let method_router = to_method_router(&path, &candidate.route.kind, &ctx); + trace!(mirror = ?candidate.mirror); + + let method_router = match &candidate.mirror { + None => to_method_router(&path, &candidate.route.kind, &ctx), + Some(mirror_path) => to_method_router(&path, &candidate.route.kind, &ctx) + .layer(DecompressionLayer::new()) + .layer(middleware::from_fn_with_state( + mirror_path.to_owned(), + mirror_handler, + )), + }; + let raw_out: MethodRouter = optional_layers(method_router, &candidate.route.opts); let req_clone = match candidate.route.kind { RouteKind::Raw(_) => Request::from_parts(parts.clone(), Body::empty()), @@ -177,6 +215,7 @@ pub async fn try_one( RouteKind::Dir(_) => Request::from_parts(parts.clone(), Body::empty()), }; + // MAKE THE REQUEST let result = raw_out.oneshot(req_clone).await; match result { @@ -210,11 +249,25 @@ pub async fn try_one( StatusCode::NOT_FOUND.into_response() } +struct RouteHelper<'a>(pub &'a Route); + +impl<'a> RouteHelper<'a> { + fn mirror(&self) -> Option<&Path> { + match &self.0.kind { + RouteKind::Proxy(ProxyRoute { + unstable_mirror, .. + }) => unstable_mirror.as_ref().map(|s| Path::new(s)), + _ => None, + } + } +} + #[derive(Debug)] struct RouteCandidate<'a> { index: usize, route: &'a Route, consume: bool, + mirror: Option, } impl RouteCandidate<'_> { @@ -289,6 +342,52 @@ fn to_method_router(path: &str, route_kind: &RouteKind, ctx: &RuntimeCtx) -> Met } } +async fn mirror_handler( + State(path): State, + req: Request, + next: Next, +) -> impl IntoResponse { + let (mut sender, receiver) = tokio::sync::mpsc::channel::>(100); + let as_stream = ReceiverStream::from(receiver); + let c = req.uri().clone(); + let p = path.join(c.path().strip_prefix("/").unwrap()); + + let r = next.run(req).await; + let s = r.into_body().into_data_stream(); + + tokio::spawn(async move { + let s = s.throttle(Duration::from_millis(10)); + tokio::pin!(s); + create_dir_all(&p.parent().unwrap()).await.unwrap(); + let mut file = BufWriter::new(File::create(p).await.unwrap()); + + while let Some(Ok(b)) = s.next().await { + match file.write(&b).await { + Ok(_) => {} + Err(e) => error!(?e, "could not write"), + }; + // match file.write("\n".as_bytes()).await { + // Ok(_) => {} + // Err(e) => error!(?e, "could not new line"), + // }; + match file.flush().await { + Ok(_) => {} + Err(e) => error!(?e, "could not flush"), + }; + match sender.send(Ok(b)).await { + Ok(_) => {} + Err(e) => { + error!(?e, "sender was dropped before reading was finished"); + error!("will break"); + break; + } + }; + } + }); + + Body::from_stream(as_stream).into_response() +} + struct QueryHasGuard<'a>(pub &'a HasGuard); impl RouteGuard for QueryHasGuard<'_> { diff --git a/crates/bsnext_dto/src/lib.rs b/crates/bsnext_dto/src/lib.rs index 6c44d78c..fe8bc811 100644 --- a/crates/bsnext_dto/src/lib.rs +++ b/crates/bsnext_dto/src/lib.rs @@ -85,6 +85,7 @@ impl From for RouteKindDTO { proxy, proxy_headers: _outgoing_headers, rewrite_uri: _rewrite, + .. }) => RouteKindDTO::Proxy { proxy }, RouteKind::Dir(DirRoute { dir, base }) => RouteKindDTO::Dir { dir, diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index b58fa340..fe30cac4 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -125,6 +125,7 @@ impl Route { proxy: a.as_ref().to_string(), proxy_headers: None, rewrite_uri: None, + unstable_mirror: None, }), ..Default::default() } @@ -213,6 +214,12 @@ pub struct ProxyRoute { pub proxy: String, pub proxy_headers: Option>, pub rewrite_uri: Option, + pub unstable_mirror: Option, +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +struct Mirror { + pub dir: String, } #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] diff --git a/crates/bsnext_input/src/route_cli.rs b/crates/bsnext_input/src/route_cli.rs index 42ddbea8..b5fc0bd5 100644 --- a/crates/bsnext_input/src/route_cli.rs +++ b/crates/bsnext_input/src/route_cli.rs @@ -36,6 +36,7 @@ impl TryInto for RouteCli { proxy: target, proxy_headers: None, rewrite_uri: None, + unstable_mirror: None, }), opts: opts_to_route_opts(&opts), ..std::default::Default::default() diff --git a/crates/bsnext_tracing/src/lib.rs b/crates/bsnext_tracing/src/lib.rs index 891945f5..c91586ae 100644 --- a/crates/bsnext_tracing/src/lib.rs +++ b/crates/bsnext_tracing/src/lib.rs @@ -77,8 +77,8 @@ pub enum OtelOption { #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] pub enum LogHttp { - #[default] On, + #[default] Off, } From 5d4ef621d86d649d8ba1b5342cd4a6db2aa74b88 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 18 Jul 2025 22:47:52 +0100 Subject: [PATCH 13/13] decompress when needed --- crates/bsnext_core/src/handler_stack.rs | 31 ++++++++++++++++------- crates/bsnext_core/src/handlers/proxy.rs | 21 --------------- crates/bsnext_core/src/optional_layers.rs | 12 +++++---- crates/bsnext_core/tests/delays.rs | 1 + 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index c14432d7..7bcf5fa6 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -14,6 +14,7 @@ use bsnext_guards::route_guard::RouteGuard; use bsnext_guards::{uri_extension, OuterUri}; use bsnext_input::route::{ListOrSingle, ProxyRoute, Route, RouteKind}; use bsnext_input::when_guard::{HasGuard, JsonGuard, JsonPropGuard, WhenBodyGuard, WhenGuard}; +use bsnext_resp::InjectHandling; use bytes::Bytes; use http::header::{ACCEPT, CONTENT_TYPE}; use http::request::Parts; @@ -162,11 +163,18 @@ pub async fn try_one( None }; + let injections = route.opts.inject.as_injections(); + let req_accepted = injections + .items + .iter() + .any(|item| item.accept_req(&req, &outer_uri)); + RouteCandidate { index, consume, route, mirror, + inject: req_accepted, } }) .collect::>(); @@ -192,15 +200,19 @@ pub async fn try_one( trace!(mirror = ?candidate.mirror); - let method_router = match &candidate.mirror { - None => to_method_router(&path, &candidate.route.kind, &ctx), - Some(mirror_path) => to_method_router(&path, &candidate.route.kind, &ctx) - .layer(DecompressionLayer::new()) - .layer(middleware::from_fn_with_state( - mirror_path.to_owned(), - mirror_handler, - )), - }; + let mut method_router = to_method_router(&path, &candidate.route.kind, &ctx); + + // decompress if needed + if candidate.mirror.is_some() || candidate.inject { + method_router = method_router.layer(DecompressionLayer::new()); + } + + if let Some(mirror_path) = &candidate.mirror { + method_router = method_router.layer(middleware::from_fn_with_state( + mirror_path.to_owned(), + mirror_handler, + )); + } let raw_out: MethodRouter = optional_layers(method_router, &candidate.route.opts); let req_clone = match candidate.route.kind { @@ -268,6 +280,7 @@ struct RouteCandidate<'a> { route: &'a Route, consume: bool, mirror: Option, + inject: bool, } impl RouteCandidate<'_> { diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index af23c3ae..7ecf0cda 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -127,27 +127,6 @@ pub async fn proxy_handler( } } - // decompress requests if needed - if let Some(h) = req.extensions().get::() { - let req_accepted = h.items.iter().any(|item| item.accept_req(&req, &outer_uri)); - tracing::trace!( - req.accepted = req_accepted, - req.accept.header = req - .headers() - .get("accept") - .map(|h| h.to_str().unwrap_or("")), - "will accept request + decompress?" - ); - if req_accepted { - let sv2 = any(serve_one_proxy_req.layer(DecompressionLayer::new())); - return Ok(sv2 - .oneshot(req) - .instrument(span.clone()) - .await - .into_response()); - } - } - let sv2 = any(serve_one_proxy_req); Ok(sv2 .oneshot(req) diff --git a/crates/bsnext_core/src/optional_layers.rs b/crates/bsnext_core/src/optional_layers.rs index 4517245f..b6aef97d 100644 --- a/crates/bsnext_core/src/optional_layers.rs +++ b/crates/bsnext_core/src/optional_layers.rs @@ -59,9 +59,11 @@ pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter { app = app.layer(cl); } - app.layer(Extension(InjectHandling { + app = app.layer(Extension(InjectHandling { items: injections.items, - })) + })); + + app } async fn delay_mw( @@ -78,10 +80,10 @@ async fn delay_mw( } } -async fn set_resp_headers_from_strs( +async fn set_resp_headers_from_strs( State(header_map): State>, - mut response: Response, -) -> Response { + mut response: Response, +) -> Response { let headers = response.headers_mut(); for (k, v) in header_map { let hn = HeaderName::from_bytes(k.as_bytes()); diff --git a/crates/bsnext_core/tests/delays.rs b/crates/bsnext_core/tests/delays.rs index 61db15ce..0462e96e 100644 --- a/crates/bsnext_core/tests/delays.rs +++ b/crates/bsnext_core/tests/delays.rs @@ -73,6 +73,7 @@ async fn test_proxy_delay() -> Result<(), anyhow::Error> { proxy: proxy.http_addr.clone(), rewrite_uri: None, proxy_headers: None, + unstable_mirror: None, }); let state = into_state(config);