Capability Matrix — what this port does, deliberately doesn't, and how to fill the gap

Benzene is honest about its boundaries. This page is the single place that states, for the TypeScript port, what is provided (with the package that provides it), what is deliberately not done and why, and how to solve the rest outside Benzene. Nothing here is a hidden gap — a deliberate "no" is a design decision stated with its reasoning; a plain "not implemented" is an honest omission, and the two are never conflated. Where this port does less than the .NET original, the row says so.

Refreshed 2026-10-11 for spec-0.2.0 (Phase 1): the settlement breakdown and its summary row, the Kafka, Versioning, Validation, Health checks, HTTP hosting, Spec endpoint and Conformance rows, and the new Envelope over HTTP / Default paths / Reserved topics rows were rewritten from source. Other rows were not re-verified in this pass. Refreshed again for spec-0.3.0 (Phase 3a, one self-description): the Spec endpoint, Codegen & clients and Conformance rows. Refreshed for spec-0.4.0 (Phase 3b, the Contract Document is the register body): the Reserved topics, Mesh — service side, Mesh — collector, Spec endpoint and Conformance rows. Refreshed for Phase 4 (the profile is R1–R5 and R7; the meshed-service claim is M1 and M2): the Mesh — service side, Spec endpoint / Cloud Service Profile and Distributed tracing rows. Refreshed for the spec 1.1 follow-ups: the gRPC, Mesh — service side, Codegen & clients, Validation, Outbound HTTP and Conformance rows. Refreshed 2026-10-14 for spec 1.2 (issue error detail, a flow's status): the Mesh — service side, Mesh — collector and Conformance rows.

The one idea behind every row

Benzene abstracts at the business-logic boundary — you write a message handler once and host it anywhere — and never at the transport or storage boundary. Wrapping SQS or Service Bus behind a generic "queue" interface would hide the transport-specific capabilities that were the reason to choose it. So the answer to "does it abstract X?" is usually a deliberate no: you keep full, direct access to the underlying SDK, and when Benzene doesn't ship an adapter, rolling your own is a first-class, supported path — a small middleware or custom pipe, not an escape hatch. See Middleware and Common Middleware.

Two corollaries you'll see below:

Packages and areas

Area What this port provides What it deliberately does NOT do (and why) How to solve the rest
Core pipeline Middleware pipeline, @message handler routing, BenzeneResult statuses, DI container seam (src/Benzene.Core*, src/Benzene.Abstractions*, src/Benzene.Results) Lock you into its built-in container — the resolver is a seam Bring your own DI container; concepts and wire contracts are in the cross-language spec
HTTP hosting Transport-agnostic HTTP core (src/Benzene.Http) hosted on Express (src/Benzene.Express), AWS API Gateway (src/Benzene.Aws.Lambda.ApiGateway), Azure Functions HTTP (src/Benzene.Azure.Function.Http), Google Cloud Functions HTTP (src/Benzene.GoogleCloud.Functions.Http) Ship its own HTTP server — it adapts the host you chose (Express, the platform runtime) Getting Started, Hosting
Envelope over HTTP useBenzeneMessage (src/Benzene.Http) serves the envelope at POST /benzene/invoke by default: HTTP 200 for every envelope it processed, whatever its status (deleted, unauthorized, not-found included), the response envelope as the body and any problem document inside it without status; only a malformed envelope is non-2xx (400, an application/problem+json document). It runs the same pipeline as the other transports, and topicFilter restricts the reachable topics (a restricted topic is a processed not-found). Envelope-over-HTTP clients (HttpMeshServiceDispatcher, MeshAnnouncer, the k8s-mesh example client) read the envelope whatever the status; an envelope without isSuccessful is read as unsuccessful, never re-classified from its status text Host-enforced authentication before the envelope is read (the 401/403 carrier case) — compose your host's auth in front of the route Clients
Default paths Every framework HTTP surface defaults under /benzene/ and is configurable: /benzene/invoke (envelope), /benzene/spec, /benzene/health (useBenzeneCloudService), /benzene/spec-ui, /benzene/mesh-ui. Liveness/readiness routes stay the application's choice No /benzene/fleet-ui or /benzene/mcp — this port has no fleet UI page or MCP binding —
Reserved topics benzene:healthcheck/liveness/readiness (health interception), benzene:spec, the collector topics, and benzene:ping (there is no benzene:mesh topic since spec-0.4.0; it falls to the router's ok-with-no-payload like any unclaimed reserved id) — answered ok with no payload by the router itself when no handler claims it, so a reachability probe into a one-way transport is acknowledged Rejecting an application topic under benzene: (the spec makes it a MAY) —
gRPC Server binding with method discovery, status mapping, streaming (src/Benzene.Grpc) + client binding (src/Benzene.Grpc.Client) + test helpers. A failed result's errors ride in grpc-status-details-bin as one google.rpc.BadRequest.FieldViolation each (any errored status, not only validation-error): message → description, field → field (dotted), code → reason; the client reads all three back (wire-contracts §4.2). A call that fails before any reply is a result, not an exception: no listener is UNAVAILABLE → service-unavailable, an elapsed deadline DEADLINE_EXCEEDED → timeout, anything else thrown service-unavailable (transport-bindings §1) Hide @grpc/grpc-js — the native call is on the context Getting started with gRPC
Kafka Self-hosted consumer-group worker with health check (src/Benzene.Kafka.Core), AWS Lambda MSK trigger with per-partition batchItemFailures (src/Benzene.Aws.Lambda.Kafka), Azure Functions Kafka trigger (src/Benzene.Azure.Function.Kafka). All three route a record on its topic header (configurable: topicHeaderKey), falling back to the Kafka topic it was consumed from, and consume that header; benzene-version selects the handler version on all three At-least-once by default on the self-hosted worker is deliberately NOT the default — a stream has no per-message ack, so commitOnlyOnSuccess defaults false (skip-and-continue, documented as the spec allows); halting on every poison record is too drastic a default commitOnlyOnSuccess: true (with catchHandlerExceptions: false) for at-least-once: a thrown exception or a failure result (raiseOnFailureStatus, default on) then stops the worker uncommitted. See Getting started with Kafka and the settlement breakdown below
RabbitMQ Self-hosted worker with Explicit/AutoAck settlement (default Explicit — safe), requeue-on-failure, health check, test helpers (src/Benzene.RabbitMq, src/Benzene.RabbitMq.TestHelpers) — No dedicated doc page yet (an omission, not a decision) — mentions in Hosting and Health Checks; the package source and its index.ts are the current reference
AWS Lambda adapters: API Gateway, SQS, SNS, EventBridge, DynamoDB Streams, Kinesis, S3, Kafka/MSK, X-Ray (src/Benzene.Aws.Lambda.*); self-hosted SQS consumer (src/Benzene.Aws.Sqs); outbound clients for SQS/SNS/EventBridge/Lambda/Step Functions (src/Benzene.Clients.Aws.*); test helpers Wrap the AWS SDK — the native Lambda event/record is on every context Getting started on AWS; SDK calls live in your handler
Azure Functions triggers: HTTP, Service Bus, Event Hub, Event Grid, Queue Storage, Blob Storage, Cosmos DB change feed, Kafka, Timer (src/Benzene.Azure.Function.*); self-hosted Service Bus and Event Hub workers (src/Benzene.Azure.ServiceBus, src/Benzene.Azure.EventHub); Cosmos DB change-feed processing (src/Benzene.Azure.CosmosDb); outbound clients (src/Benzene.Clients.Azure.*) Same — the native trigger payload is on the context Getting started on Azure, Azure Functions Setup
Google Cloud Cloud Functions HTTP + Pub/Sub adapters with test helpers (src/Benzene.GoogleCloud.Functions.*); Pub/Sub outbound client (src/Benzene.Clients.GoogleCloud.PubSub) — Getting started on Google Cloud
Mesh — service side One self-description (spec-0.4.0): the register body is the Contract Document benzene:spec serves, byte for byte — MeshAnnouncer (src/Benzene.CloudService) registers it (benzene:mesh:register) at host boot (a cloud-service-registration start-up hook attaches the host container; a host that runs no start-up checks registers on its first invocation), retrying until the collector answers, then heartbeats { service, serviceVersion?, instanceId, registrationHash, sentAt (ISO-8601), health }. The document carries runtime: "typescript", placement (configured, else self-hosted), the Cloud Service profile and degraded, outside both hashes, and no instanceId. Wire shapes and the trace feed (src/Benzene.Mesh.Wire: MeshRegistration, MeshHeartbeat, MeshOutboundRegistry, useMeshTrace, HttpMeshTraceExporter — startedAt on the wire as ISO-8601), src/Benzene.Mesh.Contracts, src/Benzene.Mesh.Dispatch, per-platform sources (src/Benzene.Mesh.Aws.Lambda). No ServiceDescriptor, benzene:mesh topic or descriptorHash (removed). Trace context (M2, mesh.md §3/§7.1): useMeshTrace joins an inbound traceparent and carries the caller's tracestate; useMeshTraceContext (src/Benzene.Mesh.Wire), put on an outbound route, forwards both from MeshSpan.current on every message the route sends — sends (one-way or with a reply) and published events alike, since @benzenejs/response-events publishes through the same IBenzeneMessageSender routes No issue emitter: the benzene:mesh:issues feed's wire shapes are here (MeshIssue, MeshIssueBatch, MeshIssueError/MeshIssueErrors, classification, fingerprint), but not the .NET emitter (UseMeshIssues, HttpMeshIssueExporter) — a service on this port sends no issues and so no errors detail; a collector then marks its feed missing only when it has failing traffic (mesh.md §4.1 is optional on both sides). placement.cloud is not detected from the platform environment: it is what withPlacement configures, else self-hosted, and region only when configured. M2 gap: outbound forwarding is not automatic. The cloud service builds no outbound routes — addOutboundRouting is called by the app, which builds each route's pipeline itself, and a raw IBenzeneMessageClient is constructed by the app too — so there is no route it could wire useMeshTraceContext onto for you (and its self-check cannot see whether you did); a raw IBenzeneMessageClient call outside IBenzeneMessageSender routes forwards nothing unless the caller sets traceparent from MeshSpan.current. useW3CTraceContext forwards the OpenTelemetry span, not the mesh span. degraded names events and sends whenever no outbound registry (withOutbound/addMeshOutbound, even empty) and no response-event/sender feed is registered — the port cannot know what an undeclared service sends The mesh contracts are normative in the spec
Mesh — collector, fleet & UI Collector + read models (src/Benzene.Mesh.Collector, src/Benzene.Mesh.Aggregator, src/Benzene.Mesh.Reporting). The collector reads a registration as a Contract Document: only service is required (no dialect check; registrationHash trusted, never recomputed, never a reason to reject); the graph is requests (consumers) and events ∪ sends (providers), benzene: entries ignored; an absent member, or one degraded names, is unknown — no edges, and no undeclared-edge contract-drift on it; a benzene: topic is never an undeclared edge; a heartbeat's registrationHash is compared with the carried value of the service's one registration (this collector keys by service; no Service Versions). Read-model shapes are unchanged (topics counts requests; the descriptor feed and the service view's descriptor field are the registration) except the fleet/service rows' binding, now transports, and the instance view's descriptorHash, now registrationHash (+ serviceVersion). Trace startedAt and issue firstSeen/lastSeen ingest as ISO-8601 or epoch ms. An issue's errors (mesh.md §4.1) are merged by (field, code) identity (count += delta) and bounded to the 10 highest counts (then field, then code, ordinal, absent first — an empty member is an absent one), and reported on the fleet view's issue when it holds any; an entry that is not an object, has neither member, or whose count is not a positive integer is skipped, the issue still accepted. A fleet view's flow (TraceSummary) carries status (mesh.md §4): its earliest-started failing event's when it failed, else its earliest-started event's, equal starts in receipt order — from the in-memory store, the Jaeger source and enriched X-Ray rows (omitted when X-Ray's error flag and the spans disagree); Tempo and X-Ray summary-plane rows omit it, their search summaries holding no span status. No legacy topics/produces body is read; discovery for AWS/Azure/Kubernetes (src/Benzene.Mesh.Discovery.*); fleet tracing sources for X-Ray/Jaeger/Tempo (src/Benzene.Mesh.Fleet.*, src/Benzene.Mesh.Tracing.Tempo); usage feeds for CloudWatch/Application Insights (src/Benzene.Mesh.Usage.*); storage on S3/Blob/GCS (src/Benzene.Mesh.{Aws.S3,Azure.Blob,GoogleCloud.Storage}); served UI (src/Benzene.Mesh.Ui) — Doc pages for the mesh UI and usage feed are still being ported (an omission, not a decision) — the runnable examples/mesh-service is the current reference
Health checks The health topics answer ok whenever the checks ran, the verdict in isHealthy (wire-contracts §5); an HTTP route mapped to a health topic answers 503 when isHealthy is false, the report as the body either way (a warning stays 200). Builder + processor core (src/Benzene.HealthChecks, src/Benzene.HealthChecks.Core); disk/HTTP/TCP/DynamoDB/TypeORM/schema/Service Bus checks (src/Benzene.HealthChecks.*); transport-embedded checks (Kafka, RabbitMQ); external probe (src/Benzene.CloudService.Probe); client-side checks (src/Benzene.Clients.HealthChecks) Ship a check for every dependency — the check interface is the seam Health Checks, Kubernetes Health Checks
Spec endpoint / Cloud Service Profile The profile report and reserved paths (src/Benzene.CloudService), plus a served spec UI (src/Benzene.Spec.Ui, default /benzene/spec-ui, reading /benzene/spec?type=benzene; shows service/serviceVersion, requests, events and sends). benzene:spec serves the spec-0.3.0 Contract Document (src/Benzene.Schema.OpenApi): schemaDialect: "openapi-3.0", service/serviceVersion/versionScheme/description from the application info, runtime/placement/profile from IServiceDeploymentInfo, degraded for a member with no source, requests (no benzene: entries; response absent for a handler that returns no payload; httpMappings per (topic, version)), events (response events and { kind: 'event' } declarations) and sends (sender definitions and { kind: 'send' } declarations, MeshOutboundRegistry, withOutbound), both hashes, and components hoisted from Zod/Joi/Yup/bring-your-own schemas in the OpenAPI 3.0 dialect. required is every always-emitted property, a defaulted one included (contract-document §4.2): Zod .default()/.prefault()/.catch(), Joi .default() and a declared Yup .default() all make a property required. Component names: declared (Zod .meta({ id }), Joi .id(), Yup .meta({ id }), MapTypeJsonSchemaSource { name }) else the class name; a collision, or a topic declared as both an event and a send, fails the start-up checks. benzene:spec takes { type?, format? }: absent/empty/null/{} is the Contract Document; benzene, openapi, asyncapi × json are served; any other type or format is not-implemented (HTTP 501). GET /benzene/spec runs the pipeline, its type/format query copied onto the request; a cloud service also answers benzene:spec over /benzene/invoke with the same document (R5: wherever the transport carries a reply). CloudServiceProfileReport (the wiring-time self-check) evaluates R1–R5 and R7 into the document's profile.missing (omitted when none); the meshed-service claim is a separate MeshedServiceReport on its mesh — M1 (registration, heartbeats, traces: needs a collector) and M2 (inbound trace join: needs a collector or a trace exporter), with isMeshed requiring the profile too — and M1/M2 never appear in missing. CloudServiceProbe (src/Benzene.CloudService.Probe) audits R1–R5 and R7 over HTTP (R5: GET /benzene/spec and the benzene:spec topic over /benzene/invoke) and reports M1 and M2 separately on mesh, always Inconclusive (not observable from outside one service). The asyncapi document reads handlers, response events, sender definitions and the MeshOutboundRegistry declarations (an event: a send operation on its own channel; a send: a send onto another service's channel, with its declared reply as AsyncAPI's reply) No yaml (or other) format. No messageEndpoint in the document. No synthesized examples (only author-supplied ones are ever emitted, and nothing supplies one yet). Joi/Yup nested objects are inlined, not hoisted (Zod hoists .meta({ id }) types). Undeclared class names do not survive bundler renaming — declare names or keepNames Profile shape is normative in the spec
Codegen & clients Typed client generation from a Contract Document (src/Benzene.CodeGen.Client, with a CLI): rejects an unknown schemaDialect, and a document with none (contract-document §1; missing-schema-dialect-is-rejected), ignores benzene: entries, reads sends, types an absent response as void, and embeds the contractHash recomputed over the client's projection (whole document, include-list or topic-scoped). The hash — projection, RFC 8785, SHA-256 — is src/Benzene.CodeGen.Core (@benzenejs/codegen-core), shared with the runtime that stamps contractHash/registrationHash on the served document; the schema health check (src/Benzene.HealthChecks.Schema) and the mesh aggregator's drift flag read the served contractHash (no private hash). Outbound routing, retries, fan-out (src/Benzene.Clients); in-process transport for the modular monolith (src/Benzene.Clients.InProcess) Require another language's SDK to call a Benzene service — the contract document is enough. No --include-reserved (removed in spec-0.3.0) Clients, Generating a client from a Contract Document
Caching Cache abstraction (src/Benzene.Cache.Core) + Redis adapter (src/Benzene.Cache.Redis) Grow into a general state-store abstraction (a database is not a transport) Caching, Redis caching
Validation Adapters for Zod, Joi, Yup, and JSON Schema via ajv (src/Benzene.Zod, src/Benzene.Joi, src/Benzene.Yup, src/Benzene.Ajv) over a shared validation abstraction (src/Benzene.Abstractions.Validation). Every adapter emits errors[].field as an RFC 6901 JSON Pointer into the request's JSON form (/items/0/sku; Yup's property paths are converted, ajv's member keywords point at the member), omitting field for a rule on the whole request; gRPC carries it as the dotted FieldViolation.field (items[0].sku) and reads it back as a pointer, and errors[].code as FieldViolation.reason Port the .NET validation libraries — third-party integrations are re-created against the popular JS-ecosystem equivalents, by convention Validation; one small adapter package per library is the pattern to copy for others
Serialization JSON by default; XML, MessagePack, and Avro adapters (src/Benzene.Xml, src/Benzene.MessagePack, src/Benzene.Avro) — Serialization & Media Formats
Versioning Handler-version selection per core-concepts §2 (VersionSelector): an exact match; else a handler the version can be cast to (the declared default wins a tie, else the ordinally highest cast target); else not-found naming topic and version. No version → the unversioned handler, else the one declared @message(topic, { version, isDefault: true }), else not-found; a topic that is all-versioned with no default is warned about at start-up. One version header, benzene-version, renameable once (addMessageVersionHeaderName); a version/x-version header selects nothing. Payload version casting for requests, responses, and schemas (src/Benzene.Core.Versioning) A "highest available" fallback — it serves another version's behaviour to a caller that named one (opt in with HighestAvailableVersionSelector if you must). No HTTP /v{version} route segment: on HTTP the version is the benzene-version header Message versioning
AuthN / AuthZ OAuth2 bearer (JWT via jose) with a required, no-default signing-algorithm allowlist and scope claims (src/Benzene.Auth.OAuth2); Basic auth (src/Benzene.Auth.Basic); shared seams (src/Benzene.Auth.Core) Ship a policy-engine (OPA/Cedar) adapter Add an authorization middleware calling your policy engine on the Benzene.Auth.Core seams; see Auth patterns

Cross-cutting production concerns

Capability What this port provides What it deliberately does NOT do (and why) How to solve the rest
Transport features Full, direct access to the native SDK message/event on every adapter's context Hide transport-specific capabilities behind a generic interface (the anti-pattern above) Use the raw SDK feature directly — the context exposes the native message/event
Message routing Topic-based dispatch to @message handlers (src/Benzene.Core.MessageHandlers); the same handler runs behind every transport Impose a canonical envelope on transports that already carry routing (Kafka topic, Service Bus properties) For envelope-less transports use the Benzene envelope or a preset topic; otherwise route on the native key. See Message Handlers
Idempotency IIdempotencyStore seam + InMemoryIdempotencyStore (single-process) + atomic-claim middleware (src/Benzene.Idempotency) Cross-instance de-duplication — independent processes can't coordinate at runtime without external state, and shared state can relocate the race, not remove it An external store with an atomic conditional write (DynamoDB conditional put, Redis SET NX) keyed on message identity, plus naturally idempotent handlers. See Idempotency
Outbox (produce-side atomicity) No package — nothing shipped. The IResponseEventPublisher seam (src/Benzene.ResponseEvents) and the scoped IUnitOfWork middleware (src/Benzene.Core.Middleware) are the documented drop-in points for your own The port's docs state the position: "writing the outbox row inside your DB transaction is application territory" — a stated design decision here, but an honest divergence from the .NET port, which ships Benzene.Outbox (+ DynamoDB/EF store packages). This port has no equivalent; do not read this row as parity The full DIY pattern — outbox publisher behind IResponseEventPublisher, one-transaction commit, a relay — is documented step by step in Transactional Outbox. At-least-once delivery means consumers must dedup: pair it with Idempotency
Oversized payloads (claim check) Not implemented. No package, no doc page (the .NET port ships Benzene.ClaimCheck + S3/Blob stores; this port has no equivalent and no stated design reason — it is simply unbuilt) — Roll an offload/hydrate middleware pair over S3/Blob keyed by a header (the .NET benzene-claim-check contract is the shape to follow), or keep payloads under the transport limit
Resilience RetryMiddleware — retry with exponential backoff (src/Benzene.Resilience); the full Cockatiel toolkit (circuit breaker, timeout, bulkhead, jittered backoff) via src/Benzene.Cockatiel, which bridges a returned failure result to Cockatiel's outcome model Re-implement or hide Cockatiel — the adapter runs your policy, exposing its full surface. Note: RetryMiddleware has no jitter helper or max-delay cap (the .NET port has both; not implemented here) — use Cockatiel for jittered backoff Resilience, Cockatiel resilience
Rate limiting Fixed-window, token-bucket, and concurrency limiters plus payload-size limiting middleware (src/Benzene.RateLimiting) Distributed rate limiting — the same cross-instance-coordination constraint as idempotency Rate Limiting; for a fleet-wide limit, back a custom limiter with shared atomic state (e.g. Redis)
Sagas / workflows In-process, compensation-based saga with LIFO rollback and an ISagaStateStore for observability (src/Benzene.Saga) Durable crash-resume — the saga is in-memory closures that can't be re-hydrated after a process dies; the state store records progress, it doesn't recover it Use a durable orchestrator (Step Functions, Durable Functions, Temporal); src/Benzene.Clients.Aws.StepFunctions can start an execution from a handler. See Sagas
Schema evolution Confluent wire-format codec + ISchemaRegistryClient seam (src/Benzene.SchemaRegistry.Core) Structural backward-compatibility checking in-box — the shipped TextualSchemaCompatibilityChecker only accepts byte-identical schemas Point at a real schema-registry server, or supply your own ISchemaCompatibilityChecker. See Schema Registry
Database / state access (nothing — by design) Any database abstraction — a database is not a transport, so wrapping one would hide its capabilities Your handler uses its own SDK/ORM directly. See TypeORM integration and Per-request transactions
Configuration & secrets Provider-agnostic ISecretStore (env vars, files, in-memory, composed, cached) + fail-fast startup validation (src/Benzene.Configuration.Core) Ship maintained cloud secret-store adapters (Key Vault / Secrets Manager / SSM) Copy the small adapter from Secrets & Configuration and use the cloud SDK yourself
Outbound HTTP HttpClientMiddleware over an injectable fetch-like function, defaulting to the Node global fetch (src/Benzene.Clients.Http). Error fidelity (core-concepts §12): a non-success application/problem+json response rebuilds the result's errors with field and code intact and attaches the document (ProblemTypes.from(result)); the envelope reader behind IBenzeneMessageSender (asBenzeneResult) does the same for a failure envelope's body. Both take the result's status from the document's benzeneStatus (wire-contracts §4.1), so an application-defined status survives the hop — and a failure envelope whose statusCode this reader cannot map is still read from it; a failure without a document, or a document without benzeneStatus, takes the reverse HTTP table (or the envelope's statusCode). An application/json failure body is not read as a problem document (the §4.1 MAY is not taken). A call that fails before any reply (transport-bindings §1) is a result, not an exception: HttpClientMiddleware catches a rejected fetch and HttpContextConverter maps it to timeout for an abort (AbortError, AbortSignal.timeout's TimeoutError) or an undici/Node timeout code, otherwise service-unavailable (transportFailureResult in src/Benzene.Results); the mesh dispatchers (HttpMeshServiceDispatcher, AwsLambdaMeshServiceDispatcher) answer the same statuses instead of throwing. The HTTP client has no deadline of its own: bound it with an injected fetch that passes a signal. The other outbound clients (SQS, SNS, EventBridge, Step Functions, Service Bus, Event Hub, Event Grid, Queue Storage, Pub/Sub, Kafka, RabbitMQ) are one-way and receive no problem document. A one-way send whose SDK call rejects before the broker accepts the message is a result too (failedSendResult): timeout for a timed-out send (a …TimeoutError such as the AWS SDK's or kafkajs's, the Azure messaging ServiceTimeout, gRPC DEADLINE_EXCEEDED from Pub/Sub), otherwise service-unavailable; a rejection the broker itself answered (an AWS service exception's $metadata.httpStatusCode, an Azure RestError.statusCode) maps that status through the reverse HTTP table (403 → forbidden, 429 → too-many-requests). An Event Hubs event too large for a batch still throws - it is not a transport failure. The batch clients (*BatchMessageClient) never throw for a transport failure either: a failed chunk is reported per entry in BatchSendResult (each with the error's name and message), and a Service Bus createMessageBatch / Event Hub createBatch that rejects fails every entry that batch would have carried - the rest of the request collection for Service Bus, the rest of that partition-key group for Event Hub (other groups still send). The low-level AwsLambdaClient is not a result-returning sender (it returns the raw payload) and still throws (the result-returning Lambda message client is not ported); the mesh dispatchers relay the reply body verbatim Ship its own HTTP stack or manage connection pooling — you inject the fetch you want (undici agent, a test stub) Pass your configured fetch in; correlation/trace propagation is applied on the Benzene-message outbound path. See Clients
Distributed tracing W3C traceparent/tracestate propagation (W3CTraceContextMiddleware in src/Benzene.Clients, extraction in src/Benzene.Diagnostics; the mesh trace context's own outbound half is useMeshTraceContext in src/Benzene.Mesh.Wire, see Mesh — service side); OpenTelemetry via the standard API, exporter-agnostic Ship a tracing backend or its sampling config — Benzene exports via the OTel API; Jaeger/Tempo/App Insights wiring is yours Monitoring & Diagnostics, Distributed tracing with OpenTelemetry, Sampling Strategies
Retry on a returned failure result Settlement per transport-bindings §1, obligation 8, on every one-way binding (see the breakdown below): a failure result, a missing result and an uncaught error are redelivered by default; success (ignored included) is acknowledged. Every one-way binding has an atMostOnce override and an ignoreUnmatchedTopics option. Stream bindings skip-and-continue a missing result and an unmatched topic by default, as .NET does, and say so A single cross-transport reliability abstraction: retry semantics are transport-native; dead-lettering is the transport's own redrive configuration Handlers on one-way bindings must be idempotent. A middleware that ends a one-way pipeline must set a result, or the message is redelivered. See Message Results and the failure-handling cookbooks
Multi-tenancy (nothing as a framework feature today) — Roll a tenant-resolver middleware + a scoped tenant holder — the documented Multi-Tenancy pattern
Conformance test/Benzene.Core.Test/Conformance/ runs the vendored language-neutral fixtures under npm test (CI: build.yml). The fixtures are a snapshot of the main repo's docs/specification/conformance/, pinned to spec-1.2.0 (SPEC_VERSION, checked by ConformanceDriftTest) (its missing-schema-dialect-is-rejected parse case passes); all 13 canonical files are vendored (mesh-descriptor-cases.json and its runner were deleted with the descriptor). 11 are opened by a runner: envelope-cases (including version selection through the canonical conformance:versioned handler), http-status-mapping, grpc-status-mapping, status-vocabulary, problem-details-cases (including conformance:validate through the Zod adapter, and httpRules.envelopeEndpoint/healthEndpoint), transport-metadata-cases (topic, version and benzene-headers keys; every metadataCases and overrideCases case through each native-metadata inbound binding - Lambda SQS and SNS, the self-hosted SQS consumer, Pub/Sub, both Service Bus bindings, the Event Hub consumer, RabbitMQ, and the three Kafka bindings - which read the topic key case-insensitively and consume it, so it never reappears as a header; the topicSources fallbacks (RabbitMQ's routing key, Kafka's own topic); settlementDefaults for every shipped one-way binding), contract-document-cases (every group: the producer groups derivationCases on real Zod output, namingCases, specTopicCases, specHttpCases in ContractDocumentProducerConformanceTest; the client-generator groups in ContractDocumentConformanceTest), contract-hash-cases (every case: those without a projection through the runtime hash, all of them through the generator's projections), mesh-trace-cases, mesh-collector-cases (including the 3b cases: an event's publisher is a provider, benzene: entries ignored, a service-only body accepted; and spec 1.2's two flow-status cases), mesh-issue-cases (including spec 1.2's three error-detail cases). ConformanceCoverageTest fails if a vendored fixture has no load(...) runner and is not on its opt-out list. Not run: mesh-service-version-cases and mesh-version-order-cases (this port's collector has no service-version identity) Edit a fixture to make this port pass. The fixture is the neutral truth A mismatch is either a bug here or a deliberate spec change in the main repo. It is never fixed by patching the fixture

Returned-failure-result settlement — the per-transport breakdown

What each inbound binding does with a delivery, by outcome, as of spec-0.2.0 (transport-bindings §1, obligation 8). Each row names the class that makes the decision. The rule is written once, in src/Benzene.Core.MessageHandlers/Settlement.ts (isSettledAsFailure), and every binding calls it.

Every one-way binding's options/config carries the same two knobs (SettlementOptions):

The settlement words: redeliver (not acknowledged; the transport redelivers, subject to its own redrive / DLQ / max-delivery / retry-policy configuration), acknowledged, skip-and-continue (a stream position advances past it), n/a (no per-message result). transport-metadata-cases.json → settlementDefaults names each binding's shape; TransportMetadataConformanceTest asserts every binding below against it, reading the default from the binding's own options.

Binding (fixture id, shape) Package — where decided Failure result No result Unmatched topic Uncaught exception Knobs (default) Unit of settlement
AWS Lambda SQS (sqs, queue) src/Benzene.Aws.Lambda.Sqs — SqsApplication, SqsOptions redeliver ¹ redeliver ¹ redeliver ¹ redeliver ¹ batchFailureMode (PartialBatchFailure; FailWholeBatch throws); atMostOnce; ignoreUnmatchedTopics (false) per message (whole batch under FailWholeBatch)
Self-hosted SQS consumer (sqs, queue) src/Benzene.Aws.Sqs — SqsConsumerApplication, SqsConsumerOptions redeliver (not deleted) redeliver redeliver redeliver ackMode (PerMessage; WholeBatch); atMostOnce; ignoreUnmatchedTopics (false) per message (poll batch under WholeBatch)
Azure Functions Service Bus trigger (servicebus, queue) src/Benzene.Azure.Function.ServiceBus — ServiceBusBatchApplication, ServiceBusOptions redeliver redeliver redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) invocation (one message, or the delivered batch)
Self-hosted Service Bus worker (servicebus, queue) src/Benzene.Azure.ServiceBus — BenzeneServiceBusWorker.settleAsync redeliver (abandon) redeliver (abandon) redeliver (abandon) redeliver (abandon) ackMode (Explicit); atMostOnce; ignoreUnmatchedTopics (false); per message ServiceBusSettlementHolder.override wins per message
Azure Functions Queue Storage (queue-storage, queue) src/Benzene.Azure.Function.QueueStorage — QueueStorageBatchApplication, BenzeneMessageQueueStorageHandler redeliver redeliver (envelope and non-envelope paths alike) redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) invocation (one message, or the delivered batch)
Google Cloud Pub/Sub (pubsub, queue) src/Benzene.GoogleCloud.Functions.PubSub — PubSubMiddlewareApplication, PubSubOptions redeliver redeliver redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) per message
RabbitMQ worker (rabbitmq, queue) src/Benzene.RabbitMq — RabbitMqWorker.handleDeliveryAsync redeliver (nack) ² redeliver (nack) ² redeliver (nack) ² redeliver (nack) ² ackMode (Explicit; AutoAck acks on dispatch); requeueOnFailure (true); atMostOnce; ignoreUnmatchedTopics (false) per delivery
AWS Lambda SNS (sns, one-shot) src/Benzene.Aws.Lambda.Sns — SnsApplication, SnsOptions redeliver (fails the invocation) redeliver redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) invocation: a throw fails every record in the event
AWS Lambda EventBridge (eventbridge, one-shot) src/Benzene.Aws.Lambda.EventBridge — EventBridgeApplication, EventBridgeOptions redeliver redeliver redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) invocation (one event)
AWS Lambda S3 (s3, one-shot) src/Benzene.Aws.Lambda.S3 — S3Application, S3Options redeliver redeliver redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) invocation: a throw fails every record in the event
Azure Functions Event Grid (event-grid, one-shot) src/Benzene.Azure.Function.EventGrid — EventGridBatchApplication, EventGridOptions redeliver redeliver redeliver redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) invocation (one event, or the delivered batch)
Lambda envelope, asynchronous (envelope-async, one-shot) src/Benzene.Aws.Lambda.Core — BenzeneMessageLambdaHandler, BenzeneMessageLambdaOptions (invocationType: 'Event') redeliver (fails the invocation) redeliver redeliver redeliver invocationType ('RequestResponse': request/response, nothing settled); atMostOnce; ignoreUnmatchedTopics (false) invocation
Azure Functions Blob trigger (blob-storage, one-shot) src/Benzene.Azure.Function.BlobStorage — BlobStorageApplication, BlobStorageOptions n/a (useBlob, no routing) n/a n/a redeliver (fails the invocation) atMostOnce per blob
Azure Functions Timer (timer, one-shot) src/Benzene.Azure.Function.Timer — TimerApplication, TimerOptions fails the invocation ³ acknowledged ³ (a useTick tick records none) fails the invocation ³ fails the invocation ³ raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (false) per tick
AWS Lambda DynamoDB Streams (dynamodb-streams, stream) src/Benzene.Aws.Lambda.DynamoDb — DynamoDbApplication, DynamoDbOptions redeliver ¹ (stops at the record) redeliver ¹ (stops at the record) redeliver ¹ redeliver ¹ atMostOnce; ignoreUnmatchedTopics (false) position: sequence number of the first failed record (records run in order)
AWS Lambda Kinesis (kinesis, stream) src/Benzene.Aws.Lambda.Kinesis — KinesisApplication, KinesisOptions redeliver (fails the invocation) skip-and-continue ⁴ skip-and-continue ⁴ redeliver raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (true) invocation (whole batch). Records run concurrently, so shard order is not kept
AWS Lambda Kafka/MSK (kafka, stream) src/Benzene.Aws.Lambda.Kafka — KafkaApplication, KafkaOptions redeliver ¹ (stops the partition) skip-and-continue ⁴ skip-and-continue ⁴ redeliver ¹ (stops the partition) batchFailureMode (PartialBatchFailure; FailWholeBatch throws); maxDegreeOfParallelism; atMostOnce; ignoreUnmatchedTopics (true) position: { partition, offset } per topic-partition (records in a partition run in offset order)
Azure Functions Kafka trigger (kafka, stream) src/Benzene.Azure.Function.Kafka — KafkaBatchApplication, KafkaOptions redeliver ⁵ skip-and-continue ⁴ skip-and-continue ⁴ redeliver ⁵ raiseOnFailureStatus (true); catchExceptions (false); atMostOnce; ignoreUnmatchedTopics (true) invocation (whole batch)
Azure Functions Event Hub trigger (eventhub, stream) src/Benzene.Azure.Function.EventHub — EventHubBatchApplication, EventHubOptions redeliver ⁵ (envelope path too: the inner result is surfaced) skip-and-continue ⁴ (a non-envelope event included) skip-and-continue ⁴ redeliver ⁵ raiseOnFailureStatus (true); catchExceptions (false); maxDegreeOfParallelism; atMostOnce; ignoreUnmatchedTopics (true) invocation (the trigger batch, which the host checkpoints)
Self-hosted Event Hub worker (eventhub, stream) src/Benzene.Azure.EventHub — BenzeneEventHubWorker.onProcessEventsAsync skip-and-continue ⁶ skip-and-continue ⁴ (checkpointed) skip-and-continue ⁴ skip-and-continue ⁶ catchHandlerExceptions (true; false halts uncheckpointed — at-least-once); raiseOnFailureStatus (true); atMostOnce; ignoreUnmatchedTopics (true) position: partition checkpoint every checkpointInterval (1) events
Self-hosted Kafka worker (kafka, stream) src/Benzene.Kafka.Core — BenzeneKafkaWorker.onEachMessage skip-and-continue ⁶ skip-and-continue ⁴ skip-and-continue ⁴ skip-and-continue ⁶ commitOnlyOnSuccess (false) + catchHandlerExceptions (true) — set true/false for at-least-once on failure results and exceptions; raiseOnFailureStatus (true); atMostOnce; ignoreUnmatchedTopics (true) position: partition offset
Azure Functions Cosmos DB trigger (cosmosdb-changefeed, stream) src/Benzene.Azure.Function.CosmosDb — useCosmosDbChangeFeed, CosmosDbChangeFeedOptions n/a (fan-in, no routing) n/a n/a redeliver (the lease is not checkpointed) atMostOnce (checkpoints past the batch) batch (lease)
Self-hosted Cosmos DB change feed (cosmosdb-changefeed, stream) src/Benzene.Azure.CosmosDb — BenzeneCosmosChangeFeedWorker, BenzeneCosmosAllVersionsChangeFeedWorker n/a (fan-in) n/a n/a redeliver (the batch is not checkpointed) catchHandlerExceptions (false) or atMostOnce checkpoint past the batch; autoCheckpointOnSuccess (true, latest-version worker) batch (lease checkpoint)

Request/response bindings settle nothing; the result goes back to the caller (http, grpc, envelope): HTTP (Express, API Gateway, Azure Functions HTTP, Cloud Functions HTTP), gRPC, the envelope endpoint (/benzene/invoke) and a synchronous Lambda invoke of the envelope.

¹ Partial reporting needs ReportBatchItemFailures on the event source mapping. Without it, Lambda ignores the returned failures and the batch is acknowledged. ² Requeued once; a failure on an already-redelivered message goes to the dead-letter exchange, or is dropped if there is none. ³ The Timer trigger follows .NET: a failure result fails the invocation (the host's retry policy decides whether the tick is retried; otherwise the failure is visible as a failed invocation and the next tick runs on schedule). A tick consumed with useTick(...) never records a result, so a missing result is not a failure here. ⁴ The documented stream carve-out. A stream binding may skip-and-continue (transport-bindings §1). As in .NET, a missing result is acknowledged on these bindings, and an unmatched topic is too (ignoreUnmatchedTopics defaults true — in .NET an unrouted record is a missing result): Kafka and Event Hubs have no per-record dead-letter path, so retaining such a record would replay its partition or batch forever. Set ignoreUnmatchedTopics: false to redeliver unmatched records instead. A failure result is still redelivered. ⁵ Benzene's part ends at failing the invocation. Whether the Functions host then redelivers a failed Event Hubs or Kafka trigger batch depends on the host's retry-policy configuration, which the application sets. ⁶ The self-hosted stream workers default to skip-and-continue (logged; the position advances), as .NET does: a stream has no per-message ack, so the safe-by-default alternative would halt the worker on every poison record. Opt into at-least-once: Event Hub catchHandlerExceptions: false; Kafka commitOnlyOnSuccess: true with catchHandlerExceptions: false (enforced at startup) — a failure result or an exception then stops the worker without committing, so a restart redelivers it.

Where this port and .NET still differ. The defaults above match .NET's 1.0 settlement contract on every binding .NET has. The remaining differences are shapes, recorded in the README "Porting conventions" ledger: the uniform atMostOnce/ignoreUnmatchedTopics knobs (spec-0.2.0 requirements .NET had not added yet); Kinesis is a per-record fan-out here, settled by invocation, where .NET runs a fan-in stream with a checkpointer; and the TypeScript-only options types (KinesisOptions, DynamoDbOptions, BlobStorageOptions, CosmosDbChangeFeedOptions, BenzeneMessageLambdaOptions).

Binding with no fixture id. The in-process fan-out client (@benzenejs/clients-in-process, InProcessFanOutClientMiddleware) dispatches to in-process consumers one-way, logs a consumer's failure and never redelivers (there is no transport to redeliver from). settlementDefaults names no such binding.

Why "we don't do that" is a feature

Every deliberate "no" above buys you something: you keep the full power of the tool you chose, you're never blocked by a leaky abstraction, and the surface you depend on stays small and stable. When you need something this port doesn't ship, the extension model (custom middleware, custom pipes, the getter/setter mapper pattern) is a supported, documented path — and where this port does less than the .NET original (outbox, claim check, retry jitter), the row above says so plainly rather than papering over it.