This document closes the operational gaps around multi-transport middleware: how headers flow into Ashlar, how MediatR sees them, what is implemented in-process versus what belongs in AWS, and how to gate spend.
| Surface | Route / artifact | Purpose |
|---|---|---|
| Correlation | X-Correlation-Id (in + out), Activity tag ashlar.correlation_id |
End-to-end tracing. |
| Ingress envelope | X-Ashlar-Tenant, X-Ashlar-App-Id, X-Idempotency-Key, X-Ashlar-Payload-Version |
Adapter-agnostic metadata on HTTP requests. |
| Operator echo | GET /api/middleware/correlation-echo |
Quick sanity check. |
| Operator snapshot | GET /api/middleware/ingress-context |
JSON view of mapped ingress (tests + debugging). |
| Catalog | GET /api/middleware/ingress-catalog |
Static list of ingress seams (HTTP API, health/ready, OpenAPI, WS lab, SMS lab, AWS SNS, gRPC host, MCP, A2A). |
| OpenAPI | /swagger/v1/swagger.json, Swagger UI |
Contract visibility for integrators. Served only in Development or with Ashlar__Api__EnableSwagger=true (docs/Configuration.md, "Ashlar.API host"). |
| WebSocket lab | GET /ws/v1/echo |
Feature-flagged echo (JSON hello + text echo). |
| SMS lab | POST /api/ingress/sms/simulate |
Parses YES <token>; in-memory idempotent store. Not signed AWS callbacks. |
| AWS SNS webhook | POST /api/ingress/sms/sns |
Optional signed SNS → same approval store (Ashlar.Ingress.AwsSns helpers). |
Configuration: Ashlar:MiddlewareIngress (EnableWebSocketIngress, EnableSmsSimulationIngress, SmsSimulationAllowedAppIds, SmsIngressApprovalStore, EnableAwsSnsSmsWebhook, AwsSnsAllowedTopicArnPrefixes, AwsSnsAllowedAppIds, AwsSnsAutoConfirmSubscription, AwsSnsSkipSignatureVerification, AwsSnsSigningCertificateRevocationMode, IngressSmsPostRateLimitPermitLimit, IngressSmsPostRateLimitWindowSeconds, DisabledCapabilities, TenantCapabilityAllowlists) and Ashlar:SmsIngressDynamoDb (TableName when using DynamoDB).
Small helpers used by Ashlar.API (not AWS SDK–heavy):
SnsCanonicalStringBuilder— builds the SNS string-to-sign forNotificationand subscription handshake types.SnsRsaSignatureVerifier— downloads the PEM fromSigningCertURL(HTTPS +*.amazonaws.comhost allowlist), validates an Amazon-anchored X.509 chain (system trust first, then Amazon Root CA 1 embedded as a custom root fallback; revocation mode fromAwsSnsSigningCertificateRevocationMode), and verifiesSignatureVersion1 (SHA-1) or 2 (SHA-256).SnsSmsMessageExtractor— unwraps plain text or JSONMessagebodies (e.g.originationNumber+messageBody).
AwsSnsSkipSignatureVerification is honored only when IHostEnvironment.EnvironmentName is Testing (integration tests). Outside Testing, if EnableAwsSnsSmsWebhook is true, AwsSnsAllowedTopicArnPrefixes must be non-empty or the host fails ValidateOnStart (fail-closed against open-ended topic acceptance).
Optional AwsSnsAllowedAppIds enforces X-Ashlar-App-Id on the SNS route (parity with SMS simulation app allowlists).
Memory(default) — process-localConcurrentDictionaryfor labs and CI.DynamoDb— setSmsIngressApprovalStoretoDynamoDbandAshlar:SmsIngressDynamoDb:TableName. Table uses string keyspk(constantAshlarSmsIngress) andsk(idempotency key fromSmsIngressExternalIds). Grant the runtime identitydynamodb:PutItemanddynamodb:GetItem.UnsupportedSmsIngressApprovalStore— registered viaTryAddSingletoninAddAshlar()when no host-specific store is configured (e.g. CLI). Ashlar.API replaces this with Memory or DynamoDB.
Optional end-to-end check (DynamoDB Local in Docker): Ashlar.Tests.Infrastructure includes DynamoDbSmsIngressDockerTests (Testcontainers, trait DockerOptional). Set ASHLAR_RUN_DYNAMODB_CONTAINER=1 and run tests with Docker available to exercise DynamoDbSmsIngressApprovalStore against a real DynamoDB Local process (no AWS account). Default CI runs skip this (no env set).
The interface lives in Ashlar.Contracts so Lambda workers can share the contract without referencing Ashlar.API.
RecordSmsYesApprovalCommand / RecordSmsYesApprovalHandler (in Ashlar.API) record approvals through MediatR so pipeline behaviors (ingress logging, validation) apply consistently. POST /api/ingress/sms/simulate and SNS Notification handling both dispatch this command.
IngressSmsPostRateLimitPermitLimit/IngressSmsPostRateLimitWindowSeconds— per-IP fixed window on both SMS POST routes. When the permit limit is 0, an effectively unlimited partition is used (no practical throttling).AwsSnsSigningCertificateRevocationMode—NoCheck(default),Online, orOfflinefor SNS signing certificate chain builds (X509Chain.ChainPolicy.RevocationMode).Onlinecan add latency or fail closed if OCSP/CRL endpoints are unreachable.
infra/terraform/ashlar-sms-ingress/ provisions the DynamoDB table and an optional regional WAFv2 Web ACL with a rate-based rule (create_waf + alb_arn). Tune limits for your traffic.
samples/approval-workflow/ contains a minimal ASL file and README for a callback-token style gate that could invoke GitHub from Lambda (replace placeholders).
See samples/aws-sns-ashlar-lambda/ — minimal Node.js Lambda that reshapes SNS subscription events into the HTTP JSON document Ashlar verifies, then POSTs to ASHLAR_SMS_URL.
IAshlarIngressAccessorexposes correlation, transport, tenant, app id, idempotency, and payload version. Ashlar.API registersHttpAshlarIngressAccessorbeforeAddAshlar()so it overrides the default no-op.IngressLoggingPipelineBehavior(registered inAddAshlar) wraps every MediatR request in a logging scope whenCorrelationIdis present, so handlers and validators inherit structured logging fields without changing individual commands.- CLI and non-web hosts keep
NoOpAshlarIngressAccessorviaTryAddSingletonwhen no HTTP implementation is registered.
Handlers that need full detail can also call HttpContext.GetIngressEnvelope() in the API layer.
The lab endpoint exists to parse and unit-test approval keywords. A production path typically looks like:
- End User Messaging (SMS) two-way number → inbound publishes to SNS topic.
- Lambda (SNS subscription) validates sender, rate limits, parses
YES <run-id>, writes idempotent state (DynamoDB or SSM), optionally calls Step FunctionsSendTaskSuccessor the GitHub API (fine-grained PAT or GitHub App) to satisfy an environment approval. - Outbound estimates use SNS Publish to the operator handset; enforce spend limits and region support.
- IAM: OIDC from GitHub Actions to a least-privilege role; never long-lived keys in the repo.
- Cost: AWS Budgets + billing alarms; fixed EC2 or Lambda memory/timeouts for workers that apply approvals.
Wire contract: treat Lambda’s normalized payload as the same conceptual shape as SmsInboundSimulationRequest (from, body, stable external id for idempotency) and map into your approval store interface in application code outside Ashlar.API if you want strict network boundaries.
For HTTP(S) subscriptions directly to Ashlar.API, POST /api/ingress/sms/sns reuses ISmsIngressApprovalStore after signature verification. Prefer a dedicated Lambda (see samples/aws-sns-ashlar-lambda/) or private networking in front of Ashlar when the API is not meant to be Internet-exposed.
Use GitHub Environments with required reviewers on the job that assumes AWS OIDC or starts expensive resources. SMS can mirror that approval for on-call, but the environment gate is the simplest hard stop.
From repo root against a running API:
ASHLAR_BASE_URL=http://127.0.0.1:8080 ./scripts/middleware-ingress-smoke.shSet RUN_SMS_SMOKE=1 only when EnableSmsSimulationIngress is true on the server. The Swagger step needs the server started in Development or with Ashlar__Api__EnableSwagger=true.
Integration coverage lives in Ashlar.Tests.Infrastructure → MiddlewareIngressIntegrationTests (net8.0 WebApplicationFactory host).
From repo root, full solution tests (matches make test):
ASHLAR_ALLOW_MOCK=1 dotnet test Ashlar.sln --blame-hang-timeout 120s --blame-hang-dump-type noneASHLAR_ALLOW_MOCK=1 is required for mock-provider integration tests; make test sets this automatically.