Lenso Web
General-purpose backend Web Interfaces and Plugins for Lenso.
This repository owns:
lenso.http.endpoint@1, provided by backend Plugins that own HTTP behavior;lenso.http.stream-endpoint@1, provided when a response must remain incremental;lenso-web-ingress-plugin, which assembles immutable routes and owns HTTP transport;lenso.http.client@1, required by backend Plugins that call upstream HTTP APIs;lenso-http-egress-plugin, which provides policy-bounded outbound HTTP transport;- credential-evidence extraction before a target-owned Auth decision; and
- protocol response mapping after the target Plugin completes.
It does not own portable Kernel semantics, authentication policy, business authorization, Console/UI behavior, Web Shell, Browser Adapter, or a global route registry.
This document makes no release, target, or production-maturity claim. Those facts require their exact CI, registry, or deployment receipts.
Current packages
lenso-capability-http-endpointlenso-capability-http-stream-endpointlenso-capability-http-clientlenso-http-authlenso-http-egress-pluginlenso-openapi-pluginlenso-web-ingress-pluginlenso-web-host
lenso-http-egress-plugin and lenso-openapi-plugin use the ordinary source-first native
authoring path: struct Plugins declare configuration, typed Capability Ports,
provided Capabilities, and lifecycle hooks; linked factory registration and
Provider endpoints are generated. App Composition still decides whether an
Instance exists and exactly which bindings it receives.
lenso-web-ingress-plugin remains the deliberate compatibility exception. It is an
endpoint-free consumer of many lenso.http.endpoint@1 and
many lenso.http.stream-endpoint@1; its public Factory also
accepts Host-owned middleware, listener observation, and lane-replica handles.
The current struct authoring macro cannot inject those Host-only values into a
consumer-only Plugin. Ingress routing nevertheless uses the generated typed
ManyPort<EndpointClient>; raw request handles are confined to generated
Capability projection code and test fixtures.
The Capability crates keep their generated Rust bindings. Bun consumers import
the matching TypeScript projections from @lenso/bun, which locks the source
revision and verifies each projection independently.
WebIngressConfig is immutable Plugin Instance configuration from the Resolved
App Plan and defaults to an ephemeral loopback listener. App Composition may
explicitly bind a fixed private or public address; deployment policy stays with
the host. Ingress serves HTTP/1.1 and cleartext HTTP/2 prior knowledge, accepts
one Authorization credential, replaces untrusted external request identifiers,
removes credential and hop-by-hop headers before dispatch, enforces body/head,
concurrency, and Endpoint deadline limits, and cooperatively cancels work on
client disconnect or App shutdown. Request bodies are collected frame-by-frame
under the configured limit and avoid a copy when Hyper supplies one data frame.
Customized configuration uses
crates/lenso-web-ingress-plugin/config.schema.json. All fields are optional:
{
"bind_address": "0.0.0.0:8080",
"max_request_body_bytes": 1048576,
"max_request_head_bytes": 16384,
"max_concurrent_requests": 128,
"max_connections": 1024,
"request_head_timeout_millis": 10000,
"request_body_timeout_millis": 30000,
"connection_idle_timeout_millis": 60000,
"shutdown_grace_timeout_millis": 30000,
"request_timeout_millis": 30000,
"session_cookie": {
"name": "__Host-lenso-session",
"csrf_cookie_name": "__Host-lenso-csrf",
"csrf_header_name": "x-csrf-token"
}
}
session_cookie is optional. Both configured Cookie names must use the
__Host- prefix. Ingress maps the exact session Cookie to protocol-neutral
credential scheme session. Unsafe Cookie-authenticated
methods require the configured CSRF Cookie and request Header to match; missing
or mismatched evidence is rejected before Endpoint dispatch. Authorization
continues to work when no session Cookie is selected. Supplying both credentials
is ambiguous and fails closed. Cookie and CSRF protocol headers are never
forwarded to the Endpoint Plugin.
An existing composition with empty Ingress configuration continues to receive these defaults without a schema. Only customized Plan configuration needs the schema.
max_connections is a hard listener-group budget. The idle deadline is
suspended while any parsed request is active and starts at the final request's
completion, so it limits keep-alive connections without imposing an absolute
connection lifetime. request_body_timeout_millis is a total body-read
deadline. request_head_timeout_millis configures Hyper's
HTTP/1 header deadline; an HTTP/2 connection with no active parsed stream is
still bounded by the connection idle deadline. HTTP/2 advertises
max_concurrent_requests as its per-connection stream ceiling; a peer that
opens excess simultaneous streams receives REFUSED_STREAM and may retry,
while the same semaphore remains the global execution ceiling across
connections. On App shutdown, admitted connections receive
shutdown_grace_timeout_millis to drain after Hyper begins graceful shutdown;
the default matches the Endpoint request deadline, after which the socket and
its live-connection permit are released.
Ingress admission and Kernel Endpoint admission are separate resolved-Plan
boundaries. A Host should explicitly apply the Ingress execution ceiling to
every lenso.http.endpoint@1 binding it creates; the helper returns the
copyable queue/concurrency pair without moving policy into the Capability
Descriptor:
let (queue_capacity, max_concurrency) = ingress_config.endpoint_admission_limits();
let binding = HostBinding::new(
PluginInstanceId::new("lenso.web-ingress", "default"),
CAPABILITY_ID,
"http-endpoints",
)
.with_admission(RequestAdmissionPlan::new(queue_capacity, max_concurrency));
A Host may deliberately choose a stricter Endpoint binding. Omitting the override inherits the generic source-first provider default of one execution slot and no queue, which is usually inappropriate for an HTTP provider.
Route providers are resolved through immutable many bindings. They describe
their method/path table during activation, before the App Ready Gate opens;
route collisions fail startup instead of changing behavior at runtime.
Streaming providers use distinct describe_stream and handle_stream
operations so one cohesive Plugin can provide both HTTP capabilities without
Rust method-name collisions. handle_stream first emits exactly one head
frame containing status and headers, then zero or more chunk frames containing
body bytes, followed by a terminal event. Ingress never buffers those chunks.
Malformed ordering or transport-owned response headers fail closed. Global
Ingress middleware can inspect and modify the response head; it does not buffer
or rewrite the stream body.
Endpoint providers can put route attributes directly on handlers. The outer
#[endpoint] attribute collects them at compile time and generates both the
Capability description and handler dispatch:
#[lenso::plugin]
#[derive(Clone, Debug)]
struct OrdersHttp {}
#[derive(serde::Deserialize)]
struct CreateOrder {
total_cents: u64,
}
#[derive(serde::Serialize)]
struct CreatedOrder<'a> {
id: &'a str,
}
#[endpoint]
impl OrdersHttp {
#[post("orders.create", "/orders")]
async fn create(
&self,
Json(_order): Json<CreateOrder>,
) -> Result<(StatusCode, Json<CreatedOrder<'static>>), Problem> {
Ok((StatusCode::CREATED, Json(CreatedOrder { id: "order-42" })))
}
}
#[lenso::plugin] declares the Plugin boundary. #[endpoint] publishes its
HTTP Endpoint Capability and generates route description and dispatch from the
same handler declarations. Json<T> and (StatusCode, T) implement
IntoResponse; Problem is the typed intentional-error response. Lower-level
response and header helpers remain available for unusual responses.
The supported method attributes are get, post, put, patch, delete,
head, options, and query. Each handler declares its stable route ID and path.
http_endpoint! remains available for existing providers and generated source
that prefers one explicit route table.
Attribute-authored handlers may use Path<T>, QueryParams<T>, Json<T>,
Option<Json<T>>, Body, Headers, and RequestId extractors. Body preserves
raw bytes, while Headers::get performs case-insensitive lookup. Middleware on
the impl applies to every route;
route-owned middleware follows it, before extraction:
#[endpoint]
#[middleware(trace_all)]
impl OrdersHttp {
#[middleware(trace_request)]
#[get("orders.read", "/orders/{order_id}")]
async fn read(
&self,
context: InvocationContext,
Path(path): Path<OrderPath>,
) -> Result<HandleResponse, EndpointHandleInvocationError> {
self.read_authenticated(context, path.order_id).await
}
}
The named async middleware method receives InvocationContext and
HandleRequest, then returns MiddlewareOutcome::next with an enriched
context/request or MiddlewareOutcome::response to short-circuit. Multiple
provider-wide and route middleware declarations run in order, with
provider-wide middleware first. Custom protocol-local extractors can implement
FromRequest<Provider>; extractors are asynchronous, can access the provider's
activation-time clients, and can enrich the context seen by later extractors
and the handler.
The macro rejects empty or malformed routes, duplicate route identifiers, and duplicate exact method/path pairs during compilation. Web Ingress still owns method/path matching, path-parameter extraction, semantic path-shape and cross-provider collision detection, and transport limits. Endpoint handlers own authentication orchestration, request decoding, business Capability calls, and intentional HTTP responses.
A linked native Host that only needs Ingress plus the Endpoint Plugins in the binary can skip a handwritten Plan:
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), lenso_web_host::WebHostError> {
lenso_web_host::NativeWebHost::new()
.plugin::<GreetingsHttp>()
.bind("127.0.0.1:8080".parse().unwrap())
.run()
.await
}
cargo run -p lenso-web-greetings-app-example
NativeWebHost is a Web Host preset. Inventory admits linked Factories.
.plugin::<T>() selects a default Instance; .plugin_with / .instance pass
configuration; .plugin_on_lane, .plugin_with_on_lane, and
.instance_on_lane record explicit Execution Lane placement;
.resolve_plan() exposes the exact immutable Plan for an advanced Runner
integration; .factory installs a hand-written native Factory for the single-lane path;
.with_ingress_config supplies limits, deadlines, cookies, and WebSocket policy;
.with_middleware adds one Ingress middleware chain; .with_diagnostics adds
one Host-owned observer for Endpoint failures; .with_tower_middleware /
.with_tower_layer adapt a Tower pre-dispatch policy; .with_adapter adds
Bun/WASI/process Adapters. .with_replicated_ingress creates lane-local
Ingress customization; .with_replicated_lane creates the complete
lane-local native registry and Adapter catalog; .with_replicated_ready_timeout
sets the replicated Ready deadline; .start_replicated binds one shared listener
and starts one Kernel per declared lane; .run_replicated adds the private
LocalSet and Ctrl-C lifecycle. Ingress is a Host default.
Enabled HTTP, stream, and WebSocket Endpoint providers are bound to it.
The middleware chain and diagnostics observer are shared by native and
start_event Hosts. Middleware runs after transport normalization and unwinds
response hooks in reverse order. A diagnostics observer sees only trusted
request/route/provider identity and the internal Runtime Failure; it cannot
change routing or the HTTP response. A Tower policy receives a cloned normalized
request and returns Continue or Respond;
it does not replace Ingress's streaming/WebSocket service. Linking a crate
without .plugin does not start it. Unique Capability
requirements still derive. Call start from an existing LocalSet when a
test needs the bound address. For route-contract tests, start_event exposes
the same ingress behavior without opening a TCP socket:
let app = NativeWebHost::new().plugin::<GreetingsHttp>().start_event().await?;
let response = app.handle(Request::get("/missing").body(Bytes::new())?).await?;
assert_eq!(response.status(), 404);
app.shutdown().await?;
handle_response is the streaming/WebSocket-capable variant when a test or
embedded Host must preserve the response body kind instead of buffering it.
Both running Host handles expose route_manifest() after activation, so route
contract tests can assert the exact immutable dispatch surface.
The Host deliberately inherits Lenso's portable local execution lane: native
Plugin state and Endpoint futures may be !Send/!Sync, so start runs on a
Tokio current_thread runtime and LocalSet. start_replicated preserves that
boundary by letting ReplicatedNativeApp own one current-thread Kernel per
Plan-declared lane. It builds the Ingress factory and inventory-linked native
registry inside each lane, then uses WebIngressListenerCoordinator for one
shared listener. Lane-local custom middleware, hand-written Factories, and
non-native Adapters must be created through the with_replicated_* builders;
ordinary single-lane Rc values are rejected rather than shared unsafely.
For authenticated HTTP, Ingress selects one Authorization credential into
HandleRequest::credential; it does not decide identity or permission. The
lenso-http-auth integration crate handles Auth invocation, stable 401/403
responses, and assertion attachment while the application owns meaningful
actor types:
struct UserActor { subject: String }
impl AuthenticatedHttpActor for UserActor {
const KIND: &'static str = "user";
fn from_assertion(assertion: &ActorAssertion) -> Self {
Self { subject: assertion.subject().to_owned() }
}
}
impl FromRequest<OrdersHttp> for UserActor {
fn from_request<'a>(
provider: &'a OrdersHttp,
context: &'a mut InvocationContext,
request: &'a HandleRequest,
) -> ExtractorFuture<'a, Self> {
extract_authenticated_actor(provider, context, request)
}
}
The Endpoint provider implements AuthClientSource to return its explicitly
bound activation-time AuthClient. A handler can then request UserActor
directly; an endpoint for a distinct credential actor kind can define
AdminActor in the same way. These AuthenticatedHttpActor types are edge
authentication projections, not
permission shortcuts: roles, tenant access, resource ownership, and every final
authorization decision remain in the target business Plugin, which verifies
the attached assertion. Release clients during deactivation rather than
resolving bindings per request.
lenso-http-auth is optional integration glue. Neither portable HTTP
Capability crate nor the Web Ingress library has a normal dependency on it;
removing the helper and its application integration leaves unauthenticated or
otherwise authenticated Endpoint providers usable without a Kernel branch.
The SDK is additive: existing providers that implement EndpointProvider
directly continue to work through the same immutable activation and dispatch
path.
Optional OpenAPI 3.1 documents
OpenAPI is an opt-in Plugin, not an Ingress mode. Linking lenso-openapi-plugin does
not add a route or change an App. App Composition enables it by selecting one
lenso.openapi Instance, binding the Endpoint providers to document to that
Instance, and binding the Instance's own HTTP Endpoint to Web Ingress. Removing
that package selection, Instance, and those bindings removes the document
without changing the business Endpoints.
For a generated local Rust Web App, lenso app add @lenso/openapi --root .
creates a pinned shared source link and an explicit
plugins/lenso.openapi/default.toml selection. Its generated Host binds the
document Plugin's many Endpoint requirement to the web Slot; the document
Endpoint itself and Endpoint providers in other Slots are not document inputs.
After lenso app build, lenso app check --root dist, and
lenso app start --from dist, fetch the actual /openapi.json from that App
for lenso-web-client generate openapi.json src/generated/lenso-api.ts.
lenso plugins disable lenso.openapi default --root dist changes the built
App's Plugin Root; lenso app start --from dist --root dist uses that external
Root instead of the immutable build snapshot and no longer serves the document
route. This is a native local-Host path; it does not imply
Workers or Wasm Endpoint qualification.
The generated HTML home route remains an undocumented-response fallback in
this document; client generation does not provide a typed HTML response for it.
Endpoint authors may attach an OpenAPI 3.1 Operation Object while retaining the
stable route declaration as the source of operationId, method, and path:
#[endpoint]
impl OrdersHttp {
#[get("orders.read", "/orders/{order_id}")]
#[openapi({
summary: "Read an order",
responses: {
"200": {
description: "Order",
content: {
"application/json": {
schema: {
"type": "object",
required: ["id"],
properties: { id: { "type": "string" } }
}
}
}
}
}
})]
async fn read(&self) -> Result<HandleResponse, EndpointHandleInvocationError> {
// ...
}
}
The JSON-like syntax accepts ordinary identifier keys without quotes. Keys such
as "type", "application/json", and "$ref" stay quoted. String, number,
boolean, null, array, and nested object values are supported. The previous
JSON string form remains compatible.
http_endpoint! accepts the same syntax through
openapi = openapi_operation!({ ... }). The authoring macros validate the DSL
at compile time and reject a separately declared operationId. A route without
metadata remains valid and receives an Undocumented response. fallback only
when an OpenAPI document is assembled.
Strict public API contracts
An Endpoint that is deliberately published for generated clients can opt into a
strict check without changing App Composition or creating a second request/
response DTO. Add #[openapi_contract] beside the existing #[openapi(...)]
metadata. The typed values already used by the handler remain the source of the
checked representation:
use lenso_capability_http_endpoint::{Json, JsonSchema, Path, endpoint};
use lenso_capability_http_endpoint::response::{Problem, StatusCode};
#[derive(serde::Deserialize, JsonSchema)]
struct CreateOrder { id: String }
#[derive(serde::Serialize, JsonSchema)]
struct CreatedOrder { id: String }
#[endpoint]
impl OrdersHttp {
#[post("orders.create", "/orders/{account_id}")]
#[openapi({ /* the complete authored OpenAPI Operation Object */ })]
#[openapi_contract(
success = 201,
errors = [(422, "invalid_order")]
)]
async fn create(
&self,
Path(path): Path<OrderPath>,
Json(input): Json<CreateOrder>,
) -> Result<(StatusCode, Json<CreatedOrder>), Problem> {
# todo!()
}
}
The Plugin package must make the schemars derive available (for example,
schemars = "1.2"), then derive JsonSchema for each public Path,
QueryParams, Json request body, and JSON response value. At OpenAPI Plugin
activation, the existing generated Endpoint description carries a temporary
type-derived fragment. The document assembler compares it with the author's
Operation Object and rejects drift in the route ID, method, path, path/query
parameters, JSON request body, JSON success response, and known RFC 9457
Problem responses. The fragment is removed before /openapi.json is served.
The comparison also rejects non-default path/query serialization settings, so
the emitted operation remains compatible with an ordinary OpenAPI-generated
client rather than only looking superficially similar.
Json<T> has an implicit 200 success response. For
(StatusCode, Json<T>), declare the exact success = 2xx status. A Problem
handler error must enumerate its stable error codes through
errors = [(4xx_or_5xx, "code")]; JSON, path, and query extractor failures
are included automatically. Strict contracts intentionally support the typed
extractors Path<T>, QueryParams<T>, Json<T>, and Option<Json<T>>, plus
RequestId and InvocationContext. Raw bodies, headers, requests, and custom
extractors stay outside this first representation-level check rather than being
silently documented incorrectly.
#[openapi_contract] is opt-in. Existing Endpoint providers, private routes,
and routes with ordinary #[openapi] metadata continue to assemble exactly as
before; they are not forced to supply complete public-client contracts.
The selected lenso.openapi Instance uses immutable configuration validated by
crates/lenso-openapi-plugin/config.schema.json:
{
"title": "Orders API",
"version": "1.0.0",
"description": "Public order operations",
"document_path": "/openapi.json",
"servers": [{"url": "https://api.example.com"}],
"components": {
"securitySchemes": {
"bearer": {"type": "http", "scheme": "bearer"}
}
}
}
The Plugin never infers a public server address from its listener and never
discovers Endpoint providers globally. Only providers explicitly bound to its
many lenso.http.endpoint@1 requirement appear in the document. Swagger UI,
Redoc, pages, assets, and navigation remain with the application or Console UI
owner rather than this repository.
Ingress-owned failures have stable JSON codes. Missing routes return 404,
method mismatches return 405 with Allow, malformed evidence returns 400,
body/head limits return 413/431, invalid or rejected Endpoint responses
return 502, unavailable Endpoint execution returns 503, and Endpoint
deadlines return 504. Every Ingress-produced response carries a generated
x-request-id and x-content-type-options: nosniff.
Hosts may install WebIngressDiagnostics on the concrete Ingress factory to
observe an Endpoint Runtime Failure together with its trusted request ID, route
ID, and bound provider index before the failure is mapped to a generic 503 or
504. Diagnostics receive neither credentials nor request bodies and cannot
change routing or the HTTP response:
#[derive(Debug)]
struct DevelopmentDiagnostics;
impl WebIngressDiagnostics for DevelopmentDiagnostics {
fn endpoint_runtime_failure(&self, event: WebIngressEndpointFailure<'_>) {
tracing::warn!(
request_id = event.request_id(),
route_id = event.route_id(),
provider_index = event.provider_index(),
failure = ?event.failure(),
"HTTP Endpoint failed",
);
}
}
let ingress = WebIngressFactory::new().with_diagnostics(DevelopmentDiagnostics);
Hosts can install transport-wide policy on one concrete Ingress factory. Global Ingress middleware runs after request-head/body limits, request-ID replacement, hop-by-hop filtering, and credential isolation. Request steps run in declaration order before route matching; response steps run in reverse order, including for short-circuited responses:
use futures::future::LocalBoxFuture;
use lenso_kernel::RuntimeFailure;
use lenso_web_ingress_plugin::{
WebIngressFactory, WebIngressMiddleware, WebIngressMiddlewareOutcome,
WebIngressRequest, WebIngressResponse,
};
#[derive(Debug)]
struct AccessLog;
impl WebIngressMiddleware for AccessLog {
fn identity(&self) -> &'static str {
// Include immutable settings so replicated lanes can compare policy.
"access-log:json:v1"
}
fn before_request<'a>(
&'a self,
request: &'a mut WebIngressRequest,
) -> LocalBoxFuture<'a, Result<WebIngressMiddlewareOutcome, RuntimeFailure>> {
tracing::info!(method = %request.method(), path = %request.uri().path());
Box::pin(async { Ok(WebIngressMiddlewareOutcome::Continue) })
}
fn after_response<'a>(
&'a self,
_request: &'a WebIngressRequest,
response: &'a mut WebIngressResponse,
) -> LocalBoxFuture<'a, Result<(), RuntimeFailure>> {
tracing::info!(status = response.status().as_u16());
Box::pin(async { Ok(()) })
}
}
let ingress = WebIngressFactory::new().with_middleware(AccessLog);
This seam is suitable for transport-wide access logging, CORS, compression,
and coarse network admission policy. Middleware can mutate the normalized
method, URI, headers, and body, or return an intentional response. It cannot
override Ingress-owned response headers, reintroduce credential or hop-by-hop
headers to an Endpoint, bypass immutable transfer limits, or access Hyper's
connection body. Middleware failures map to 503.
Authentication remains Endpoint orchestration: use UserActor or AdminActor
extractors backed by the Auth Plugin so the target Plugin can still make the
final authorization decision. Do not turn global Ingress middleware into a
business identity or permission registry.
Hosts that replicate one App across Runner lanes can bind once and create one
Ingress factory per lane through WebIngressListenerCoordinator. The
coordinator opens the Ready Gate only after every replica publishes the same
canonical route manifest and ordered middleware identity sequence, distributes
accepted sockets round-robin, keeps connection and request budgets global to
the listener group, and gives each lane a fair local request bound as a second
containment boundary. Acceptor failures are propagated to every lane instead
of silently stopping the listener. Every identity must include its immutable
configuration:
let coordinator = WebIngressListenerCoordinator::bind(config, lane_count).await?;
let factories = (0..lane_count)
.map(|lane_index| {
WebIngressFactory::replicated_at(&coordinator, lane_index).map(|factory| {
factory.with_middleware(AccessLog)
})
})
.collect::<Result<Vec<_>, _>>()?;
replicated_at is preferred when a Runner builds lane-local factories: the
explicit slot keeps a lane mapped to the same replica even when lane threads
start in a different order. replicated remains available for sequential
factory construction and allocates the next free slot.
The coordinator is only the transport fan-out. NativeWebHost::start_replicated()
now packages the native Runner composition around it: it resolves one Plan,
creates one deterministic replica slot per declared lane, builds each
lane-local NativePluginRegistry from inventory, and starts
lenso_runner::ReplicatedNativeApp with one ExecutionAdapterCatalog per
lane. Use the ordinary NativeWebHost::start() for a single Kernel; never call
it once per lane, because that would create independent listeners and bypass
the Runner's cross-lane Plan validation.
For a lane-local custom Adapter or hand-written Factory, extend the complete
catalog supplied to with_replicated_lane rather than constructing a second
Runner. The callback runs in the target lane, so it may safely create
!Send/!Sync Plugin state there:
let app = NativeWebHost::new()
.plugin::<GreetingsHttp>()
.plugin_on_lane::<OrdersHttp>("orders")
.with_replicated_lane(|_lane, registry| {
let mut catalog = ExecutionAdapterCatalog::single(registry);
catalog = catalog
.with_adapter(MyLaneLocalAdapter)
.map_err(|error| error.to_string())?;
Ok(catalog)
})
.start_replicated()
.await?;
The default lane builder already supplies the native catalog, so this callback
is only needed for additional lane-local implementations. The single-lane
.factory(...) installer is intentionally rejected by start_replicated; add
the hand-written factory inside this callback once per lane instead.
HttpEgressConfig requires at least one exact http or https origin. The
binding and immutable origin list are the caller's outbound authority. Egress
rejects origin changes, user info, URL fragments, authority/hop-by-hop request
headers, and oversized transfer evidence. It does not follow redirects, use
system proxies, set a Referer, store cookies, or retry automatically. Total and
connect timeouts plus concurrency and head/body limits are owned by each Egress
Instance. Egress negotiates HTTP/2 over TLS by default while retaining HTTP/1.1
compatibility. An Instance may instead require HTTP/1.1 or HTTP/2 prior
knowledge, including cleartext h2c for trusted internal origins:
{
"allowed_origins": ["http://inventory.internal:8080"],
"http_version": "http2_prior_knowledge"
}
HTTP/3 is not currently shipped: Reqwest 0.13 exposes its HTTP/3 client only
behind the upstream reqwest_unstable configuration, while Ingress still needs
a Host-owned TLS credential source and QUIC/UDP listener lifecycle.
Verify
cargo fmt --all -- --check
cargo check --locked --workspace --all-targets
cargo test --locked --workspace
cargo clippy --locked --workspace --all-targets -- -D warnings
For a bounded portable HTTP Endpoint proof, run
cargo test --locked -p lenso-web-ingress-plugin --test portable_http_component.
The fixture compiles the same business handler into a native provider and a
real Wasm Component, then sends six loopback HTTP requests through Web Ingress
to each: method/path matching, non-UTF-8 request and response bytes, 404/405,
domain rejection, and runtime failure. This is local Native Host plus Wasmtime
evidence. It does not qualify workerd or deployed Workers, outbound HTTP,
streaming, WebSocket, authentication, or production resource ceilings.
The Component above runs through the Native Wasmtime Adapter. The workspace
also has a bounded lenso app build --target workers path for one verified
V4/V6 Workers Component variant. It packages a locked Plan, Jco bindings, the
pinned @lenso/workers-runtime HTTP event adapter, and a workers-build.json
build receipt. See crates/lenso-engine-app/assets/workers-app-README.md for
the request-only local-workerd limits. A build receipt or Node handler test is
not a real workerd run: the exact output needs its own HTTP corpus. Neither
this Native test nor a local workerd pass qualifies deployed Workers, streams,
WebSockets, outbound HTTP, cancellation, or production resource ceilings.
The dispatch-only release workflow publishes an explicitly authorized package
set from a landed main SHA through configured crates.io Trusted Publishers
and GitHub OIDC. Dependency-aware
publication releases the Capability crates before the dependent
lenso-web-ingress-plugin and lenso-http-egress-plugin Plugins, and workspace CI fully
verifies every package archive.
The independent-process benchmark compares transport-only Axum, the bridge, and the complete Lenso path without sharing a Tokio runtime between client and server. Filters keep short local runs reproducible:
LENSO_HTTP_BENCH_BODY_BYTES=0 \
LENSO_HTTP_BENCH_CONNECTIONS=8 \
cargo bench -p lenso-web-ingress-plugin --bench http_ingress_process
The Web execution profile adds the decision evidence needed before introducing a different Web scheduler:
cargo bench -p lenso-web-ingress-plugin --bench web_execution_profile
It runs the same echo and delayed handlers through bare Axum, policy-equivalent Axum transport, the bounded bridge, and the complete Lenso native path. The report includes repeated throughput, unloaded p50/p99 latency, sampled server CPU and RSS, and classified saturation outcomes. A two-lane Lenso fixture also reports per-lane request counts for one, two, and eight HTTP/1.1 keep-alive connections.
LENSO_HTTP_BENCH_REQUESTS, LENSO_HTTP_BENCH_CONNECTIONS, and
LENSO_HTTP_BENCH_SERVER can reduce the matrix. Process CPU/RSS sampling uses
the host ps command and prints unavailable when the host cannot provide it.
Treat results as host-specific evidence: a work-stealing Web profile is only
justified if repeated runs show a material user workload problem that cannot be
fixed at the Ingress routing or operation-granularity boundary. The benchmark
does not change the portable Kernel or imply live Instance migration.
Event hosts
lenso-web-ingress-plugin also exposes WebIngressEventFactory for an event-owned
App using an event Driver. Disable the default native feature to compile the
shared HTTP path for wasm32-unknown-unknown. The event factory uses the same
Plugin descriptor, Plan-bound generated Endpoint clients, routing, credentials
and middleware as the native listener. See the event ingress seam
for the host integration contract, buffered-only scope and qualification limits.
lenso-http-egress-plugin exposes HttpEgressEventFactory for an explicitly
injected event transport, with a workers feature and the JS-owned abortable
Fetch bridge. It reuses the native exact-origin and transfer policy. See
event HTTP Egress for supported configuration,
per-event injection, cancellation and target qualification requirements.