diff --git a/README.md b/README.md index 67e6a5cc5..afd137756 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ A headless web framework for .NET. Build a more robust frontend: whatever goes wrong, the backend gets it right, and every run is recorded in your Postgres. -[Docs](https://traxsharp.net/docs) · [Getting started](https://traxsharp.net/docs/getting-started) · +[What Trax does](https://traxsharp.net/docs/what-trax-does) · [Docs](https://traxsharp.net/docs) · +[Getting started](https://traxsharp.net/docs/getting-started) · [Samples](Trax.Samples) · [NuGet](https://www.nuget.org/profiles/Theauxm) · [npm](https://www.npmjs.com/package/@traxsharp/state-machine) @@ -52,7 +53,7 @@ folder depends only on folders above it in this table; take the layers you need, | [Trax.Scheduler](Trax.Scheduler) | Cron and interval schedules, retries, dead letters, and workers on other machines or in Lambda | | [Trax.Api](Trax.Api) | GraphQL generated from your trains and machines: a mutation per machine event, authorization checked on the server, persisted operations, audit, live subscriptions and the operations console your host serves | | [Trax.Cli](Trax.Cli) | The `trax` tool: scaffold a hub and trains from an OpenAPI or GraphQL schema, and state-machine codegen | -| [Trax.Samples](Trax.Samples) | **Start here.** Complete sample apps, and the `trax-api`, `trax-scheduler` and `trax-hub` templates | +| [Trax.Samples](Trax.Samples) | Complete sample apps, and the `trax-api`, `trax-scheduler` and `trax-hub` templates | | [Trax.Api.StateMachine](Trax.Api.StateMachine) | `@traxsharp/state-machine`, the TypeScript twin of the state-machine engine | | [Trax.Docs](Trax.Docs) | The documentation published at [traxsharp.net/docs](https://traxsharp.net/docs), and the decision records | | [Trax.Website](Trax.Website) | The source of [traxsharp.net](https://traxsharp.net) | diff --git a/SECURITY.md b/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Api/SECURITY.md b/Trax.Api/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Api/SECURITY.md +++ b/Trax.Api/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Api/src/AUTH-PACKAGE-README.md b/Trax.Api/src/AUTH-PACKAGE-README.md index 854d76ca7..c6e6b40b9 100644 --- a/Trax.Api/src/AUTH-PACKAGE-README.md +++ b/Trax.Api/src/AUTH-PACKAGE-README.md @@ -21,7 +21,7 @@ The full disclaimer, including the consumer responsibility checklist, ships in t | `Trax.Api.Auth.Jwt.Cognito.Issuer` | Mints Cognito-shaped RS256 tokens, with a refresh-token store contract. | [Cognito issuer](https://traxsharp.net/docs/sdk-reference/api-auth/cognito-token-issuer) | | `Trax.Api.Auth.Jwt.Testing` | A self-hosted JWKS server and token minters for integration tests. | [JWT testing](https://traxsharp.net/docs/sdk-reference/api-auth/test-jwks-server) | | `Trax.Api.Auth.Oidc` | OpenID Connect code flow with PKCE and a session cookie, for browser sign-in. | [AddTraxOidcAuth](https://traxsharp.net/docs/sdk-reference/api-auth/add-trax-oidc-auth) | -| `Trax.Api.GraphQL.Audit` | Records each GraphQL request to your `ITraxAuditSink` from a bounded channel and a background writer. | [API Security](https://traxsharp.net/docs/api-security) | +| `Trax.Api.GraphQL.Audit` | Records each GraphQL request to your `ITraxAuditSink` from a bounded channel and a background writer. | [API Security](https://traxsharp.net/docs/security/secure-a-graphql-host) | ## Installation @@ -85,11 +85,11 @@ public sealed class MyAuditSink : ITraxAuditSink } ``` -Per-train authorization uses `[TraxAuthorize]` on the train class; see [Authorization](https://traxsharp.net/docs/authorization). Subscriptions carry credentials in the `connection_init` payload; see [API Security](https://traxsharp.net/docs/api-security). +Per-train authorization uses `[TraxAuthorize]` on the train class; see [Authorization](https://traxsharp.net/docs/security/authorization-rules). Subscriptions carry credentials in the `connection_init` payload; see [API Security](https://traxsharp.net/docs/security/secure-a-graphql-host). ## Documentation -- [API Security](https://traxsharp.net/docs/api-security): every scheme, subscription auth, auditing and hardening defaults +- [API Security](https://traxsharp.net/docs/security/secure-a-graphql-host): every scheme, subscription auth, auditing and hardening defaults - [API Auth reference](https://traxsharp.net/docs/sdk-reference/api-auth) - [Trax documentation](https://traxsharp.net/docs) - Source: [github.com/TraxSharp/Trax/tree/main/Trax.Api](https://github.com/TraxSharp/Trax/tree/main/Trax.Api) diff --git a/Trax.Api/src/Trax.Api.Dashboard/Extensions/ApiDashboardEndpointExtensions.cs b/Trax.Api/src/Trax.Api.Dashboard/Extensions/ApiDashboardEndpointExtensions.cs index 678930f6c..701a44ff6 100644 --- a/Trax.Api/src/Trax.Api.Dashboard/Extensions/ApiDashboardEndpointExtensions.cs +++ b/Trax.Api/src/Trax.Api.Dashboard/Extensions/ApiDashboardEndpointExtensions.cs @@ -97,7 +97,8 @@ public static IEndpointConventionBuilder UseTraxApiDashboard( + "of the dashboard reads, and AddTraxGraphQL() does not expose it. Call " + "ExposeOperationQueries() on the GraphQL builder, and " + "ExposeOperationMutations() for the dashboard's actions, with the gate the " - + "builder then asks for (RequireAuthorization() or GateOperations(...))." + + "builder then asks for (RequireAuthorization() or GateOperations(...)). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#usetraxapidashboard-requires-the-graphql-operations-namespace" ); var assets = services.GetService() ?? DashboardAssets.Embedded; diff --git a/Trax.Api/src/Trax.Api.GraphQL/Extensions/GraphQLServiceExtensions.cs b/Trax.Api/src/Trax.Api.GraphQL/Extensions/GraphQLServiceExtensions.cs index 54197911d..0df0dfd36 100644 --- a/Trax.Api/src/Trax.Api.GraphQL/Extensions/GraphQLServiceExtensions.cs +++ b/Trax.Api/src/Trax.Api.GraphQL/Extensions/GraphQLServiceExtensions.cs @@ -111,7 +111,8 @@ Func configure if (!services.Any(sd => sd.ServiceType == typeof(TraxMarker))) throw new InvalidOperationException( "AddTraxGraphQL() requires AddTrax() to be called first. " - + "Call services.AddTrax(trax => ...) before services.AddTraxGraphQL()." + + "Call services.AddTrax(trax => ...) before services.AddTraxGraphQL(). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#addtraxgraphql-requires-addtrax-to-be-called-first" ); var builder = new TraxGraphQLBuilder(services); diff --git a/Trax.Cli/SECURITY.md b/Trax.Cli/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Cli/SECURITY.md +++ b/Trax.Cli/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Cli/tests/Trax.Cli.Tests/Fixtures/HubTemplate/Program.cs.txt b/Trax.Cli/tests/Trax.Cli.Tests/Fixtures/HubTemplate/Program.cs.txt index 4a79168f9..4e06a1296 100644 --- a/Trax.Cli/tests/Trax.Cli.Tests/Fixtures/HubTemplate/Program.cs.txt +++ b/Trax.Cli/tests/Trax.Cli.Tests/Fixtures/HubTemplate/Program.cs.txt @@ -48,7 +48,7 @@ var builder = WebApplication.CreateBuilder(args); // operator key holds. Both exist only in Development: Trax.Api refuses to start a host outside // Development with a key containing "do-not-use-in-production" registered. Anywhere else, register real // credentials (AddHashed keys from a secret store, or AddTraxJwtAuth) before removing the -// IsDevelopment() check: https://traxsharp.net/docs/api-security. +// IsDevelopment() check: https://traxsharp.net/docs/security/secure-a-graphql-host. // AddAuthentication() stays outside the check. AddTraxApiKeyAuth registers authentication // itself, so without this line a start outside Development fails in UseAuthentication() // with "Unable to resolve service for type IAuthenticationSchemeProvider". AddAuthorization() diff --git a/Trax.Core/SECURITY.md b/Trax.Core/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Core/SECURITY.md +++ b/Trax.Core/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Caching/CachedPriceTests.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Caching/CachedPriceTests.cs new file mode 100644 index 000000000..0baddf3d6 --- /dev/null +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Caching/CachedPriceTests.cs @@ -0,0 +1,82 @@ +using AwesomeAssertions; +using Trax.Core.Functional; +using Trax.Core.Junction; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Integration.Examples.Caching; + +// Shown on https://traxsharp.net/docs/sdk-reference/train-methods/short-circuit +// +// A price lookup with a cache in front. A cache hit becomes the result, but the steps after it +// still run. A cache miss throws, and the train carries on as if the step were not there. + +public sealed record Product(string Sku); + +public sealed record Price(decimal Amount); + +public interface IPriceCache +{ + Dictionary Prices { get; } +} + +public sealed class PriceCache : IPriceCache +{ + public Dictionary Prices { get; } = []; +} + +public interface IAudit +{ + List Lines { get; } +} + +public sealed class Audit : IAudit +{ + public List Lines { get; } = []; +} + +public class CheckCache(IPriceCache cache) : Junction +{ + public override Task Run(Product product) => + cache.Prices.TryGetValue(product.Sku, out var amount) + ? Task.FromResult(new Price(amount)) + : throw new KeyNotFoundException($"{product.Sku} is not cached"); +} + +public class LookUpPrice(IAudit audit) : Junction +{ + public override Task Run(Product product) + { + audit.Lines.Add($"looked up {product.Sku}"); + return Task.FromResult(new Price(10m)); + } +} + +public class PriceTrain(IPriceCache cache, IAudit audit) : Train +{ + protected override Task> Junctions() => + AddServices(cache, audit).ShortCircuit().Chain().Resolve(); +} + +public class CachedPriceTests +{ + [Test] + public async Task Run_CacheHit_ReturnsTheCachedPriceButStillRunsTheLookUp() + { + var cache = new PriceCache(); + cache.Prices["tea"] = 4m; + var audit = new Audit(); + + var price = await new PriceTrain(cache, audit).Run(new Product("tea")); + + price.Should().Be(new Price(4m)); + audit.Lines.Should().Equal("looked up tea"); + } + + [Test] + public async Task Run_CacheMiss_IgnoresTheFailureAndReturnsTheLookUp() + { + var price = await new PriceTrain(new PriceCache(), new Audit()).Run(new Product("tea")); + + price.Should().Be(new Price(10m)); + } +} diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/Moderation.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/Moderation.cs index 105a19d94..d67e2ef15 100644 --- a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/Moderation.cs +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/Moderation.cs @@ -5,6 +5,8 @@ namespace Trax.Core.Tests.Integration.Examples.Decisions; +// Shown on https://traxsharp.net/docs/core/decisions +// // Moderating a post. Three questions about the same post are asked in one call: is it a threat, // what should happen to it, and how severe is it. A threat is taken down whatever the verdict; the // rest is routed by the verdict, and posts sent for review are routed again by severity. diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/SupportDesk.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/SupportDesk.cs index 3974fa123..000be1132 100644 --- a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/SupportDesk.cs +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Decisions/SupportDesk.cs @@ -5,6 +5,8 @@ namespace Trax.Core.Tests.Integration.Examples.Decisions; +// Shown on https://traxsharp.net/docs/core/decisions +// // A support desk triages each incoming message. A decider (in production, a model that returns // typed decisions) reads the parsed ticket and picks a track. Refunds move money, so they need more // confidence than the other tracks; anything the decider is unsure about goes to a person. diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/Checkout.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/Checkout.cs new file mode 100644 index 000000000..1f1c3791f --- /dev/null +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/Checkout.cs @@ -0,0 +1,48 @@ +using Trax.Core.Functional; +using Trax.Core.Junction; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Integration.Examples.Failures; + +// Shown on https://traxsharp.net/docs/cross-cutting/handle-errors-in-trains +// Shown on https://traxsharp.net/docs/sdk-reference/trains-and-junctions/train +// +// A checkout whose card is always declined. The tests beside this file show what a failing +// junction does to the rest of the train, and what the caller gets back. + +public sealed record Order(decimal Total); + +public sealed record Payment(decimal Amount); + +public sealed record Receipt(decimal Amount); + +public interface IOutbox +{ + List Sent { get; } +} + +public sealed class Outbox : IOutbox +{ + public List Sent { get; } = []; +} + +public class ChargeCard : Junction +{ + public override Task Run(Order order) => + throw new InvalidOperationException("Card declined"); +} + +public class SendReceipt(IOutbox outbox) : Junction +{ + public override Task Run(Payment payment) + { + outbox.Sent.Add($"receipt for {payment.Amount}"); + return Task.FromResult(new Receipt(payment.Amount)); + } +} + +public class CheckoutTrain(IOutbox outbox) : Train +{ + protected override Task> Junctions() => + AddServices(outbox).Chain().Chain().Resolve(); +} diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/FailedJunctionTests.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/FailedJunctionTests.cs new file mode 100644 index 000000000..c4b347536 --- /dev/null +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/FailedJunctionTests.cs @@ -0,0 +1,20 @@ +using AwesomeAssertions; + +namespace Trax.Core.Tests.Integration.Examples.Failures; + +// Shown on https://traxsharp.net/docs/cross-cutting/handle-errors-in-trains + +public class FailedJunctionTests +{ + [Test] + public async Task Run_ChargeCardThrows_SendsNoReceiptAndThrowsTheSameException() + { + var outbox = new Outbox(); + + var checkout = () => new CheckoutTrain(outbox).Run(new Order(20m)); + + var declined = await checkout.Should().ThrowAsync(); + declined.Which.Message.Should().Be("Card declined"); + outbox.Sent.Should().BeEmpty(); + } +} diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/RunEitherTests.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/RunEitherTests.cs new file mode 100644 index 000000000..3f8b4a356 --- /dev/null +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/RunEitherTests.cs @@ -0,0 +1,18 @@ +using AwesomeAssertions; + +namespace Trax.Core.Tests.Integration.Examples.Failures; + +// Shown on https://traxsharp.net/docs/sdk-reference/trains-and-junctions/train + +public class RunEitherTests +{ + [Test] + public async Task RunEither_ChargeCardThrows_ReturnsTheExceptionInsteadOfThrowing() + { + var result = await new CheckoutTrain(new Outbox()).RunEither(new Order(20m)); + + result.IsLeft.Should().BeTrue(); + var message = result.Match(Right: receipt => "", Left: failure => failure.Message); + message.Should().Be("Card declined"); + } +} diff --git a/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Memory/OneValuePerTypeTests.cs b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Memory/OneValuePerTypeTests.cs new file mode 100644 index 000000000..eb56f5fd7 --- /dev/null +++ b/Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Memory/OneValuePerTypeTests.cs @@ -0,0 +1,40 @@ +using AwesomeAssertions; +using Trax.Core.Functional; +using Trax.Core.Junction; +using Trax.Core.Train; + +namespace Trax.Core.Tests.Integration.Examples.Memory; + +// Shown on https://traxsharp.net/docs/core/memory +// +// Memory keeps one value per type. Both junctions return a string, so the second one replaces +// the first, and the train returns the last name only. + +public sealed record Person(string FirstName, string LastName); + +public class GetFirstName : Junction +{ + public override Task Run(Person person) => Task.FromResult(person.FirstName); +} + +public class GetLastName : Junction +{ + public override Task Run(Person person) => Task.FromResult(person.LastName); +} + +public class NameTrain : Train +{ + protected override Task> Junctions() => + Chain().Chain().Resolve(); +} + +public class OneValuePerTypeTests +{ + [Test] + public async Task Run_TwoJunctionsReturnAString_ReturnsTheSecond() + { + var name = await new NameTrain().Run(new Person("Ada", "Lovelace")); + + name.Should().Be("Lovelace"); + } +} diff --git a/Trax.Docs/SECURITY.md b/Trax.Docs/SECURITY.md index b38dca0fe..b66c04068 100644 --- a/Trax.Docs/SECURITY.md +++ b/Trax.Docs/SECURITY.md @@ -20,4 +20,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Docs/about-these-docs.md b/Trax.Docs/about-these-docs.md index 275f37616..1c8d4f7f8 100644 --- a/Trax.Docs/about-these-docs.md +++ b/Trax.Docs/about-these-docs.md @@ -16,7 +16,7 @@ it links to the page that has it rather than repeating it. | Kind | What it is for | In Trax | |---|---|---| -| Tutorial | Learning by doing. One path from an empty folder to something that runs, with a visible result at each step. | [Getting Started](/docs/getting-started) builds a server with a train, a schedule, the dashboard and a GraphQL API. | +| Tutorial | Learning by doing. One path from an empty folder to something that runs, with a visible result at each step. | [Getting Started](/docs/getting-started) builds a server with a train, a schedule, the operations console and a GraphQL API. | | How-to guide | Getting a specific job done when you already know what you want. Steps, no teaching. | Setting up the scheduler, resolving a dead letter, securing a GraphQL host, testing a train. | | Reference | Looking something up. Describes what exists and what it does, in a consistent shape, without advice. | Every public type and method under [SDK Reference](/docs/sdk-reference), the CLI, the packages, the glossary, the schema migrations. | | Explanation | Understanding. Why a part of Trax works the way it does, what it trades off, how its pieces fit. | How a train runs its chain, what an effect is, how the scheduler decides when a manifest runs. | diff --git a/Trax.Docs/api.md b/Trax.Docs/api.md index afcd89d48..62eba0f0e 100644 --- a/Trax.Docs/api.md +++ b/Trax.Docs/api.md @@ -8,7 +8,7 @@ nav_order: 1 # The GraphQL API -> **Security.** Trax ships authentication (`Trax.Api.Auth.*`) and audit (`Trax.Api.GraphQL.Audit`) packages alongside this library. NO WARRANTY. See [Secure a GraphQL host](/docs/api-security) before deploying to production. +> **Security.** Trax ships authentication (`Trax.Api.Auth.*`) and audit (`Trax.Api.GraphQL.Audit`) packages alongside this library. NO WARRANTY. See [Secure a GraphQL host](/docs/security/secure-a-graphql-host) before deploying to production. `Trax.Api.GraphQL` puts a GraphQL endpoint in front of the trains a host registers. A train opts in with [`[TraxQuery]` or `[TraxMutation]`](/docs/sdk-reference/graphql-api/trax-graphql-attribute) @@ -81,8 +81,8 @@ says whether it is gated or public, and a host where one does not is refused at exposes it, and exposing it requires a gate. Authentication is pluggable (API keys, JWTs, OIDC), and every scheme feeds the same principal. -- [Secure a GraphQL host](/docs/api-security) puts the pieces together in one `Program.cs`. -- [Authorization rules](/docs/authorization) lists what gates each surface. +- [Secure a GraphQL host](/docs/security/secure-a-graphql-host) puts the pieces together in one `Program.cs`. +- [Authorization rules](/docs/security/authorization-rules) lists what gates each surface. - [Startup refusals](/docs/security/startup-refusals) lists what stops a host. ## A named schema @@ -113,8 +113,8 @@ which capabilities to enable. ## See also -- [Call another GraphQL server](/docs/api-graphql-client), for the other direction. -- [Persisted operations](/docs/persisted-operations), to serve only documents the host knows. +- [Call another GraphQL server](/docs/api/call-another-graphql-server), for the other direction. +- [Persisted operations](/docs/api/persisted-operations), to serve only documents the host knows. - [Add the dashboard to a host](/docs/dashboard), for a monitoring UI over the same data. ## SDK Reference diff --git a/Trax.Docs/api/add-a-subscription-field.md b/Trax.Docs/api/add-a-subscription-field.md index 15595ba4d..20420e8e6 100644 --- a/Trax.Docs/api/add-a-subscription-field.md +++ b/Trax.Docs/api/add-a-subscription-field.md @@ -65,7 +65,7 @@ builder.Services.AddTraxGraphQL(graphql => graphql.AddTypeExtension - `GateManagement` serves the management fields, `persistedOperations { ... }` at the root, to a role or policy. Without it they are not in the schema. It is a gate of its own: the uploader needs this role and nothing under `operations`, which `UsePersistedOperations` does not expose. - The other choices are in [Authorization rules](/docs/authorization). + The other choices are in [Authorization rules](/docs/security/authorization-rules). - `RequirePersisted(false)` without `LogNonPersistedRequests(true)` does nothing, and the host refuses to start. @@ -161,7 +161,7 @@ builder.Services.AddTraxGraphQL(graphql => ``` The allowlist matches the name the caller sends, not the document, so keep it to networks you -trust; see [Allowlist and dev carve-outs](/docs/persisted-operations#allowlist-and-dev-carve-outs). +trust; see [Allowlist and dev carve-outs](/docs/api/persisted-operations#allowlist-and-dev-carve-outs). ## 6. Run more than one node diff --git a/Trax.Docs/api/typed-requests.md b/Trax.Docs/api/typed-requests.md index 9d14e54c2..ee8b57654 100644 --- a/Trax.Docs/api/typed-requests.md +++ b/Trax.Docs/api/typed-requests.md @@ -114,7 +114,7 @@ The same naming applies to arguments, so `URLPath` binds `$urlPath`. ## See also -- [Call another GraphQL server](/docs/api-graphql-client) +- [Call another GraphQL server](/docs/api/call-another-graphql-server) - [How the GraphQL client checks queries](/docs/api/graphql-client-validation) - [IGraphQLClientRequest](/docs/sdk-reference/graphql-client/i-graphql-client-request) diff --git a/Trax.Docs/book.md b/Trax.Docs/book.md index 93dc4ea0b..be6aeb9ab 100644 --- a/Trax.Docs/book.md +++ b/Trax.Docs/book.md @@ -17,8 +17,33 @@ The [quick start](/docs/getting-started) is the short version: one train, record served in an afternoon. The book covers the same ground more slowly and then keeps going. Every C# block in the book compiles in CI against the release the site was published from, as in the -quick start, and each chapter compiles as the whole project so far. Chapters without a link are not -written yet. +quick start, and each chapter compiles as the whole project so far. + +## The chapters at a glance + +| Part | Chapter | What Shopfront gains | Trax feature | +|---|---|---|---| +| Trains | [1. An order is a train](/docs/book/an-order-is-a-train) | placing an order | trains, junctions, Memory | +| | [2. Testing PlaceOrder](/docs/book/testing-place-order) | a test project | junction, train and host tests, architecture guards | +| | [3. Every order is recorded](/docs/book/every-order-is-recorded) | Postgres, a failing payment | the run record, failure classes | +| | [4. The shop's own data](/docs/book/the-shops-own-data) | products, stock, orders | domain data contexts | +| | [5. Does this order look like fraud](/docs/book/does-this-order-look-like-fraud) | a fraud check | decisions and deciders | +| The API | [6. Customers and staff](/docs/book/customers-and-staff) | sign-in | authentication and authorization | +| | [7. Serve it over GraphQL](/docs/book/serve-it-over-graphql) | the API | queries, mutations, query models | +| | [8. The operations console](/docs/book/the-operations-console) | a staff view of every run | the console | +| Running it for real | [9. Scheduled work](/docs/book/scheduled-work) | a nightly stock report | schedules, dependent jobs | +| | [10. Failures, retries and dead letters](/docs/book/failures-retries-and-dead-letters) | a supplier sync | retries, dead letters, lifecycle hooks | +| | [11. A separate worker](/docs/book/a-separate-worker) | fulfilment on its own process | queued work, workers, recovery | +| Machines | [12. Checkout is a state machine](/docs/book/checkout-is-a-state-machine) | checkout | machines, guards, timers | +| | [13. The machine places the order](/docs/book/the-machine-places-the-order) | checkout runs the order | invoking a train, child machines | +| Composition | [14. Checks in parallel, and resuming](/docs/book/checks-in-parallel-and-resuming) | courier rates side by side | `Parallel`, checkpoints | +| The storefront | [15. A generated client](/docs/book/a-generated-client) | a React storefront | generated clients | +| | [16. Checkout in the browser](/docs/book/checkout-in-the-browser) | checkout in React | the TypeScript twin | +| | [17. Require persisted operations](/docs/book/require-persisted-operations) | only known documents run | persisted operations | +| Shipping it | [18. Deploy](/docs/book/deploy) | a hub and two workers in containers | deployment, health checks | + +[What Trax does](/docs/what-trax-does) has the same features grouped by capability, with the page +that explains each. ## What we build diff --git a/Trax.Docs/book/an-order-is-a-train.md b/Trax.Docs/book/an-order-is-a-train.md index 89ba148c5..41738d3d7 100644 --- a/Trax.Docs/book/an-order-is-a-train.md +++ b/Trax.Docs/book/an-order-is-a-train.md @@ -11,7 +11,8 @@ nav_order: 11 In this chapter we start Shopfront and write its most important piece of work, placing an order, as a train. By the end, `POST /orders` prices a cart, reserves stock, takes a payment and confirms the -order, and the log shows each of those steps as it happens. +order, and the log shows each of those steps as it happens. When a Trax name is new, the +[Glossary](/docs/reference/glossary) gives its plain equivalent. Every C# block on this page is compiled in CI against the release this site was published from. diff --git a/Trax.Docs/book/an-order-is-a-train/the-train.md b/Trax.Docs/book/an-order-is-a-train/the-train.md index 4acf39cb9..ebd5d869d 100644 --- a/Trax.Docs/book/an-order-is-a-train/the-train.md +++ b/Trax.Docs/book/an-order-is-a-train/the-train.md @@ -46,7 +46,9 @@ The train is only the order of its junctions. Nothing here passes a value from o next: the train carries a store called Memory, which holds the input when the train starts and each junction's output as it finishes, one value per type. Every junction takes its input from there. That is how `TakePaymentJunction` gets the `PlaceOrderInput` even though the junction just -before it made a `StockReservation`. +before it made a `StockReservation`. One value per type also means a junction that returns a type +already in Memory replaces the value there, which is why each junction here returns a type of its +own ([Memory](/docs/core/memory#storage-by-type)). Nothing reads the `StockReservation` yet. In chapter 3 the payment starts failing, and `TakePaymentJunction` takes the reservation too, to give the stock back before the order stops. diff --git a/Trax.Docs/book/an-order-is-a-train/when-the-chain-does-not-fit.md b/Trax.Docs/book/an-order-is-a-train/when-the-chain-does-not-fit.md index c171b52a9..0308cc494 100644 --- a/Trax.Docs/book/an-order-is-a-train/when-the-chain-does-not-fit.md +++ b/Trax.Docs/book/an-order-is-a-train/when-the-chain-does-not-fit.md @@ -43,8 +43,8 @@ small junctions that each do one thing. A junction that fails stops the order wh chain in the wrong order never starts. What we cannot do yet is look back. The runs are recorded, but in memory, and they are gone when the -application stops. In chapter 2 we move them to Postgres, make the payment provider fail now and -then, and read back what happened to each order. +application stops. In chapter 2 we test the train, and in chapter 3 we move the runs to Postgres, +make the payment provider fail now and then, and read back what happened to each order. ## SDK Reference diff --git a/Trax.Docs/book/require-persisted-operations.md b/Trax.Docs/book/require-persisted-operations.md index c1a2f644e..4223f10f8 100644 --- a/Trax.Docs/book/require-persisted-operations.md +++ b/Trax.Docs/book/require-persisted-operations.md @@ -26,7 +26,7 @@ This chapter starts from the end of [chapter 16](/docs/book/checkout-in-the-brow ## SDK Reference -> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) +> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/api/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) --- diff --git a/Trax.Docs/book/require-persisted-operations/make-the-hub-refuse.md b/Trax.Docs/book/require-persisted-operations/make-the-hub-refuse.md index 2e53e6bdb..fda792cd8 100644 --- a/Trax.Docs/book/require-persisted-operations/make-the-hub-refuse.md +++ b/Trax.Docs/book/require-persisted-operations/make-the-hub-refuse.md @@ -213,7 +213,7 @@ What changed: ## SDK Reference -> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) +> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/api/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) --- diff --git a/Trax.Docs/book/require-persisted-operations/test-it.md b/Trax.Docs/book/require-persisted-operations/test-it.md index ae1320e54..7e6d9f15d 100644 --- a/Trax.Docs/book/require-persisted-operations/test-it.md +++ b/Trax.Docs/book/require-persisted-operations/test-it.md @@ -188,7 +188,7 @@ The last chapter deploys Shopfront. ## SDK Reference -> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) +> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/api/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) --- diff --git a/Trax.Docs/book/require-persisted-operations/what-a-caller-sees.md b/Trax.Docs/book/require-persisted-operations/what-a-caller-sees.md index 46319fd2a..abd7d6385 100644 --- a/Trax.Docs/book/require-persisted-operations/what-a-caller-sees.md +++ b/Trax.Docs/book/require-persisted-operations/what-a-caller-sees.md @@ -51,7 +51,7 @@ entry page names the ids: ## SDK Reference -> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) +> [Turn on persisted operations](/docs/api/turn-on-persisted-operations) | [Persisted operations](/docs/api/persisted-operations) | [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) --- diff --git a/Trax.Docs/contributing/api-conventions.md b/Trax.Docs/contributing/api-conventions.md index 5de59419f..b6f442412 100644 --- a/Trax.Docs/contributing/api-conventions.md +++ b/Trax.Docs/contributing/api-conventions.md @@ -129,7 +129,7 @@ compiler then reports CS0619 with the instruction as its text: ``` error CS0619: 'BuilderOrderExtensions.AddMediator(TraxBuilder, params Assembly[])' is obsolete: -'Call AddEffects(...) before AddMediator(...).' +'Call AddEffects(...) before AddMediator(...). See https://traxsharp.net/docs/cross-cutting/troubleshooting#call-addeffects-before-addmediator' ``` Trax.Mediator's are in `BuilderOrderExtensions`: diff --git a/Trax.Docs/contributing/releases.md b/Trax.Docs/contributing/releases.md index d899f3025..05fd85e85 100644 --- a/Trax.Docs/contributing/releases.md +++ b/Trax.Docs/contributing/releases.md @@ -64,7 +64,7 @@ release is intended; describe a breaking change in the pull request instead. carries that one version. It uploads the packages as an artifact. 5. The `publish` job, also gated on the `release` environment, attests the packages and pushes them to nuget.org with a short-lived Trusted Publishing key. See - [Supply Chain Security](/docs/supply-chain-security). + [Supply Chain Security](/docs/security/supply-chain). 6. The site is not part of the release. Dispatch **Website** with the new tag (`v1.61.0`); its job, gated on the `website-deploy` environment, moves the `website` branch to the tag, which publishes the site for that release. diff --git a/Trax.Docs/contributing/writing-docs.md b/Trax.Docs/contributing/writing-docs.md index df07a097e..91429a637 100644 --- a/Trax.Docs/contributing/writing-docs.md +++ b/Trax.Docs/contributing/writing-docs.md @@ -345,6 +345,19 @@ them. Trailing whitespace and the indentation a chunk's lines share are not comp lifted out of a nested block drops its outer indentation. Region marker lines are ignored on both sides. A failure names the page and line, the file, and the first chunk that did not match. +A page that states a behaviour can show the test that proves it: an excerpt of a file under a +library folder's `tests/`, usually a small example in `Examples/` written to be read (one train, +one test, names a reader follows without the rest of the suite). The test file names each page that +shows it, in a comment near its top, so whoever edits the test knows a page depends on it: + +```csharp fragment +// Shown on https://traxsharp.net/docs/core/memory +``` + +`TestExcerptsNameTheirPagesTests` checks both directions: an excerpt of a library test needs the +line in the file, and the line needs the page to excerpt the file. Samples and templates are not +covered. + ### Package versions Every Trax `PackageReference` with a `Version` on a page, and every Trax `PackageVersion` in a diff --git a/Trax.Docs/core.md b/Trax.Docs/core.md index ebb4e6d6e..b55c1b245 100644 --- a/Trax.Docs/core.md +++ b/Trax.Docs/core.md @@ -1,5 +1,5 @@ --- -title: Trax.Core +title: Trains (Trax.Core) description: "Trax.Core overview: trains, junctions, Memory and railway error propagation with no database or DI container, and when to use Core on its own." source: - Trax.Core/src/Trax.Core @@ -8,9 +8,17 @@ area: trains nav_order: 1 --- -# Trax.Core +# Trains (Trax.Core) -`Trax.Core` is the foundation: trains, junctions, and railway error propagation. No database, no DI container, no infrastructure required. +A piece of business work, such as placing an order, is rarely one call. It prices a cart, reserves +stock, takes a payment and confirms, and when the payment fails the steps after it must not run. +Written by hand, that is a method full of `if` checks between service calls. In Trax it is a +**train**: a class that lists its steps (**junctions**) in order, hands each step's output to the +next by its type, and stops at the first one that throws. + +Everything else in Trax runs, records, schedules or serves a train, so this is the place to start. +[What Trax does](/docs/what-trax-does) shows how the rest attaches to it. This page covers +`Trax.Core`, the package with the train itself: no database, no DI container, no infrastructure. ```bash dotnet add package Trax.Core diff --git a/Trax.Docs/core/memory.md b/Trax.Docs/core/memory.md index 86fcae80d..1e233d1b0 100644 --- a/Trax.Docs/core/memory.md +++ b/Trax.Docs/core/memory.md @@ -43,6 +43,34 @@ Memory stores one value per type. If two junctions both return `string`, the sec .Chain() // Returns LastName ``` +This test, from Trax.Core's own suite, shows it happening: both junctions return `string`, and the train ends up with the last name only. + +```csharp from=Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Memory/OneValuePerTypeTests.cs +public class GetFirstName : Junction +{ + public override Task Run(Person person) => Task.FromResult(person.FirstName); +} + +public class GetLastName : Junction +{ + public override Task Run(Person person) => Task.FromResult(person.LastName); +} + +public class NameTrain : Train +{ + protected override Task> Junctions() => + Chain().Chain().Resolve(); +} +// ... + [Test] + public async Task Run_TwoJunctionsReturnAString_ReturnsTheSecond() + { + var name = await new NameTrain().Run(new Person("Ada", "Lovelace")); + + name.Should().Be("Lovelace"); + } +``` + A junction's output is stored under its declared output type only, not its runtime type or interfaces, so a later junction that takes an interface finds the value only when the producing junction declares that interface as its output. This is why Trax encourages specific types (records, value objects) over primitives. A junction signature like `Junction` tells you exactly what goes in and what comes out, while `Junction` doesn't. diff --git a/Trax.Docs/cross-cutting/handle-errors-in-trains.md b/Trax.Docs/cross-cutting/handle-errors-in-trains.md index 5d3550a17..1e0d5db1b 100644 --- a/Trax.Docs/cross-cutting/handle-errors-in-trains.md +++ b/Trax.Docs/cross-cutting/handle-errors-in-trains.md @@ -51,6 +51,47 @@ public class ChargeCardJunction(IPaymentGateway gateway) : Junction +{ + public override Task Run(Order order) => + throw new InvalidOperationException("Card declined"); +} + +public class SendReceipt(IOutbox outbox) : Junction +{ + public override Task Run(Payment payment) + { + outbox.Sent.Add($"receipt for {payment.Amount}"); + return Task.FromResult(new Receipt(payment.Amount)); + } +} + +public class CheckoutTrain(IOutbox outbox) : Train +{ + protected override Task> Junctions() => + AddServices(outbox).Chain().Chain().Resolve(); +} +``` + +```csharp from=Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/FailedJunctionTests.cs + [Test] + public async Task Run_ChargeCardThrows_SendsNoReceiptAndThrowsTheSameException() + { + var outbox = new Outbox(); + + var checkout = () => new CheckoutTrain(outbox).Run(new Order(20m)); + + var declined = await checkout.Should().ThrowAsync(); + declined.Which.Message.Should().Be("Card declined"); + outbox.Sent.Should().BeEmpty(); + } +``` + ## React to a failure in the train On a `ServiceTrain`, override `OnFailed`. It runs once, after the run's failure is recorded, with diff --git a/Trax.Docs/cross-cutting/troubleshooting.md b/Trax.Docs/cross-cutting/troubleshooting.md index 56d453b44..e239ecab8 100644 --- a/Trax.Docs/cross-cutting/troubleshooting.md +++ b/Trax.Docs/cross-cutting/troubleshooting.md @@ -12,7 +12,8 @@ redirect_from: Each error entry is headed by the message Trax raises, quoted as it appears in the source, with the names it fills in shown as `X`, `Y` and `Z`. Search this page for a distinctive phrase from -the message you have. Problems that raise no error are at the end. +the message you have; a startup error ends with a link to its entry here. Problems that raise no +error are at the end. ## "Could not find train with input type (X)" diff --git a/Trax.Docs/dashboard/api-admin.md b/Trax.Docs/dashboard/api-admin.md index ccc9284cc..b6e2426ef 100644 --- a/Trax.Docs/dashboard/api-admin.md +++ b/Trax.Docs/dashboard/api-admin.md @@ -11,7 +11,7 @@ source: # API admin -Shown only when the host serves [persisted operations](/docs/persisted-operations) management to +Shown only when the host serves [persisted operations](/docs/api/persisted-operations) management to this caller: it turns on persisted operations, declares the management gate (`GateManagement`), and the caller meets it. Every read and write goes through the `persistedOperations` fields, behind that gate, so the console refuses what the API refuses, with the same message. Turning it on is in diff --git a/Trax.Docs/effect.md b/Trax.Docs/effect.md index d34453143..0f97e9be7 100644 --- a/Trax.Docs/effect.md +++ b/Trax.Docs/effect.md @@ -1,5 +1,5 @@ --- -title: Trax.Effect +title: Recording runs (Trax.Effect) description: "How Trax.Effect works: ServiceTrain, the record it writes for every run, junctions resolved from DI, and the effect pattern behind its providers." type: explanation area: effects @@ -9,9 +9,10 @@ source: - Trax.Effect/src/Trax.Effect/Services/EffectRunner/EffectRunner.cs --- -# Trax.Effect +# Recording runs (Trax.Effect) -`Trax.Effect` gives every train run a record: a row with its state, start and end time, the junction that failed and the exception it threw, stored in PostgreSQL, SQLite or memory. It also resolves junctions from dependency injection and runs pluggable effect providers around each run. +When a run fails in production, the first questions are which run, with what input, at which step, +and why. `Trax.Effect` answers them with no logging code of yours. It gives every train run a record: a row with its state, start and end time, the junction that failed and the exception it threw, stored in PostgreSQL, SQLite or memory. It also resolves junctions from dependency injection and runs pluggable effect providers around each run. It adds to [Core](/docs/core): diff --git a/Trax.Docs/getting-started.md b/Trax.Docs/getting-started.md index 12b1d3b70..05f9adbd5 100644 --- a/Trax.Docs/getting-started.md +++ b/Trax.Docs/getting-started.md @@ -13,6 +13,8 @@ snippet_context: none In this tutorial we build one application from an empty folder: a train that greets someone, run in-process first, then recorded in Postgres, scheduled, and called over GraphQL and watched from the operations console. Each step shows every file it changes in full, and what you should see when you run it. +The [Glossary](/docs/reference/glossary) says what each Trax name means in plain terms: a train is a +workflow, a junction one of its steps. Every C# block on this page is compiled in CI against the package versions in the project file below. @@ -503,7 +505,7 @@ serves it over GraphQL and shows it in the operations console. - [Put a sign-in in front of the console](/docs/dashboard/add-the-console-to-a-host) before it runs outside Development. -- [Secure a GraphQL host](/docs/api-security) with real credentials, and expose a train as a +- [Secure a GraphQL host](/docs/security/secure-a-graphql-host) with real credentials, and expose a train as a mutation with [TraxMutation](/docs/sdk-reference/graphql-api/trax-graphql-attribute). - [Start from a project template](/docs/reference/templates): `trax-hub` scaffolds this same shape with a test project. diff --git a/Trax.Docs/index.md b/Trax.Docs/index.md index ccc1d3348..08ecfc4d6 100644 --- a/Trax.Docs/index.md +++ b/Trax.Docs/index.md @@ -16,6 +16,34 @@ a worker in another process or in AWS Lambda, and every run leaves a record of w it ended, and which junction failed with what. Trax is MIT licensed and targets .NET 10. The [Glossary](/docs/reference/glossary) defines the names the docs use. +Everything in Trax is a way to use a train. The database records its runs, the mediator, the +scheduler and workers decide when and where it runs, the GraphQL API and the operations console let +a client or an operator call it and watch it, and a state machine walks a user through a longer +process and runs trains along the way. [What Trax does](/docs/what-trax-does) maps every capability +to the chapter that teaches it and the page that explains it. + +## Start here + +New to Trax, read these in order. Everything else on this site is for when you need it. + +1. [What Trax does](/docs/what-trax-does): every capability on one page, and how they fit. +2. [Quick start](/docs/getting-started): one app from an empty folder, in-process to GraphQL. +3. [Trains and junctions](/docs/core/trains-and-junctions): what a train is and how it runs. +4. [Memory](/docs/core/memory): how a junction gets its input. +5. [How a chain runs](/docs/core/building-chains): the steps a train can declare. +6. [Handle errors in trains](/docs/cross-cutting/handle-errors-in-trains): what a failing junction + does to the rest. +7. [Test a train](/docs/cross-cutting/test-a-train): with in-memory effects, no database. +8. [Recording runs](/docs/effect): what gets recorded for each run, and where. +9. [Calling a train by its input](/docs/mediator): how every caller starts a train. +10. [Schedule a train](/docs/scheduler/setup): cron, intervals and one-off runs. +11. [The GraphQL API](/docs/api): serving trains to a client. +12. [Authorization rules](/docs/security/authorization-rules): nothing is served until a rule allows it. +13. [Operations console](/docs/dashboard): finding and requeuing runs. +14. [Train or machine](/docs/statemachine/train-or-machine): when a process needs a state machine. +15. [Troubleshooting](/docs/cross-cutting/troubleshooting): startup and runtime errors, verbatim. +16. [Packages](/docs/reference/packages): which ones to install. + ## Where to go [How these docs are organised](/docs/about-these-docs) explains the four kinds of page below. @@ -25,7 +53,7 @@ it ended, and which junction failed with what. Trax is MIT licensed and targets - [The Trax book](/docs/book): build one shop from an empty folder to a deployed hub, workers and a React storefront, one feature per chapter. - [Getting Started](/docs/getting-started): build a server with one train, record its runs in - Postgres, schedule it, open the dashboard and call it over GraphQL. + Postgres, schedule it, open the operations console and call it over GraphQL. ### Do @@ -33,7 +61,7 @@ it ended, and which junction failed with what. Trax is MIT licensed and targets - [Run a job once, later](/docs/scheduler/delayed-jobs): delayed and one-off runs. - [Handle dead letters](/docs/scheduler/dead-letters-and-cleanup): retry or acknowledge runs that ran out of retries. -- [Secure a GraphQL host](/docs/api-security): authentication, authorization and hardening, end to +- [Secure a GraphQL host](/docs/security/secure-a-graphql-host): authentication, authorization and hardening, end to end. - [Add a domain data context](/docs/effect/effect-providers/domain-data-contexts): your own EF Core tables next to Trax's. @@ -44,13 +72,27 @@ it ended, and which junction failed with what. Trax is MIT licensed and targets ### Understand -- [Core](/docs/core): trains, junctions, and how a chain runs. -- [Effect](/docs/effect): what an effect is and how runs get recorded. -- [Mediator](/docs/mediator): running a train by its input type, and nested trains. +- [What Trax does](/docs/what-trax-does): every capability, with where to learn and read about it. +- [Trains](/docs/core): trains, junctions, and how a chain runs. +- [Decisions](/docs/core/decisions): a branch in a chain as a typed question a rule or a model answers. +- [Checkpoints and branches](/docs/core/checkpoints-and-branches): steps side by side, and resuming a + failed run. +- [Recording runs](/docs/effect): what an effect is and how runs get recorded. +- [Calling a train by its input](/docs/mediator): the train bus, queued work and nested trains. - [Scheduling](/docs/scheduler): manifests, the work queue, and how jobs are dispatched. +- [How retries work](/docs/scheduler/how-retries-work): backoff, dead letters and requeues. +- [Choosing where trains run](/docs/scheduler/remote-execution): in the host, on worker + processes, over SQS or in Lambda. +- [The GraphQL API](/docs/api): trains as queries and mutations, and query models. +- [Persisted operations](/docs/api/persisted-operations): a server that runs only the documents you + registered. +- [Operations console](/docs/dashboard): what an operator sees and can do. - [State Machines](/docs/statemachine): snapshots and definitions, the guarantees, and the matching C# and TypeScript runtimes. -- [Authorization](/docs/authorization): where Trax checks who may run what. +- [Train or machine](/docs/statemachine/train-or-machine): which one a piece of work needs. +- [Authorization](/docs/security/authorization-rules): where Trax checks who may run what. +- [Architecture guards](/docs/reference/architecture-guards): tests that keep an application in + the shape Trax expects. - [Deployment topologies](/docs/reference/deployment-topologies): the ways to split an application across processes, and the sample that runs each. - [Trax compared with Quartz.NET and Hangfire](/docs/reference/comparison): what each is for. diff --git a/Trax.Docs/mediator.md b/Trax.Docs/mediator.md index 8df8cda5e..dffca8751 100644 --- a/Trax.Docs/mediator.md +++ b/Trax.Docs/mediator.md @@ -1,5 +1,5 @@ --- -title: Mediator +title: Calling a train by its input (Trax.Mediator) description: "Trax.Mediator and the train bus: dispatching a train by its input type instead of injecting it, nested trains, and the scope each run gets." type: explanation area: mediator @@ -9,10 +9,12 @@ source: - Trax.Mediator/src/Trax.Mediator/Services/TrainRegistry/TrainRegistry.cs --- -# Mediator +# Calling a train by its input (Trax.Mediator) +A controller, a GraphQL mutation, the scheduler and a worker all need to start the same trains, +and none of them should have to reference each train's class. `Trax.Mediator` provides `ITrainBus`, which runs a train chosen by the type of the input it is -given, so a caller does not need a reference to the train it runs. [AddMediator](/docs/sdk-reference/configuration/add-mediator) +given. [AddMediator](/docs/sdk-reference/configuration/add-mediator) registers it, scans the assemblies it is given for trains, and registers each train under its interface. How the scan and the lookup work in detail is in [Train discovery](/docs/mediator/train-discovery). diff --git a/Trax.Docs/mediator/dispatch-limits.md b/Trax.Docs/mediator/dispatch-limits.md index 5b2072b22..5d4ea9874 100644 --- a/Trax.Docs/mediator/dispatch-limits.md +++ b/Trax.Docs/mediator/dispatch-limits.md @@ -94,7 +94,7 @@ The hook itself is described under ## See also -- [Secure a GraphQL host](/docs/api-security): where these limits sit among a host's other guards. +- [Secure a GraphQL host](/docs/security/secure-a-graphql-host): where these limits sit among a host's other guards. - [MediatorConfiguration](/docs/sdk-reference/mediator-api/mediator-configuration): the properties. - [Concurrency limiting](/docs/sdk-reference/mediator-api/concurrency-limiting): per-train and global limits. diff --git a/Trax.Docs/migration-guides/hotchocolate-16.md b/Trax.Docs/migration-guides/hotchocolate-16.md index 20833fb0b..7d25b1746 100644 --- a/Trax.Docs/migration-guides/hotchocolate-16.md +++ b/Trax.Docs/migration-guides/hotchocolate-16.md @@ -134,7 +134,7 @@ resolver with `[Parent(requires: ...)]`, which is enforced by the A surface that declares its posture with `HotChocolate.Authorization.Authorize` or `HotChocolate.Authorization.AllowAnonymous` refuses host startup, with a message naming the Trax attribute to use instead. Trax reads only its own attributes; why is in -[Authorization](/docs/authorization). +[Authorization](/docs/security/authorization-rules). | Replace | With | |---|---| diff --git a/Trax.Docs/reference.md b/Trax.Docs/reference.md index 73b8b5dfe..5fc609ebd 100644 --- a/Trax.Docs/reference.md +++ b/Trax.Docs/reference.md @@ -57,12 +57,12 @@ Pages to look things up in. Per-type and per-method pages are in the ## GraphQL API and dashboard -- [GraphQL Client](/docs/api-graphql-client): the outbound GraphQL client. +- [GraphQL Client](/docs/api/call-another-graphql-server): the outbound GraphQL client. - [Operations console](/docs/dashboard): the operations console and its pages. ## Security -- [Authorization](/docs/authorization): endpoint policies, `TraxAuthorize`, `TraxAllowAnonymous` and startup validation. +- [Authorization](/docs/security/authorization-rules): endpoint policies, `TraxAuthorize`, `TraxAllowAnonymous` and startup validation. - [Security policy](/docs/SECURITY): reporting a vulnerability and supported versions. ## Tooling diff --git a/Trax.Docs/reference/glossary.md b/Trax.Docs/reference/glossary.md index c13d3b723..668731cee 100644 --- a/Trax.Docs/reference/glossary.md +++ b/Trax.Docs/reference/glossary.md @@ -8,16 +8,20 @@ nav_order: 4 # Glossary -The names Trax uses, in the code and in these pages. Each entry says where the type lives; [Packages](/docs/reference/packages) says which package to install for it. Each term is a heading, so a link can name it, as the operations console's help links do: `/docs/reference/glossary#manifest-group`. +The names Trax uses, in the code and in these pages. Where a term has a familiar equivalent, its entry starts with it (*Roughly:*); the rest of the entry is the exact meaning. Each entry says where the type lives; [Packages](/docs/reference/packages) says which package to install for it. Each term is a heading, so a link can name it, as the operations console's help links do: `/docs/reference/glossary#manifest-group`. ## Trains and junctions ### Train +*Roughly:* a workflow, or one use case. + `Train` in `Trax.Core.Train`. A class that takes one input, runs the chain its `Junctions()` method declares, and returns either the output or the exception that stopped it. See [Trains and Junctions](/docs/core/trains-and-junctions). ### ServiceTrain +*Roughly:* a workflow whose every run is recorded. + `ServiceTrain` in `Trax.Effect.Services.ServiceTrain`. A train that records each run as a **Metadata** row and runs the registered **effects** around it. It implements `IServiceTrain`, and its `Run` is sealed, so what runs is always the declared chain. The base class for trains the mediator, scheduler and API can run. ### Route @@ -26,6 +30,8 @@ The names Trax uses, in the code and in these pages. Each entry says where the t ### Junction +*Roughly:* one step of a workflow. + `Junction` in `Trax.Core.Junction`. One step of a chain: its `Run(TIn)` returns `Task`. Trax builds a junction through its single public constructor, taking each argument from **Memory** first and the DI container second. ### EffectJunction @@ -34,6 +40,8 @@ The names Trax uses, in the code and in these pages. Each entry says where the t ### Chain +*Roughly:* the steps, in order. + The sequence of junctions a train declares in `Junctions()`, written with `Chain()`, `ShortCircuit()` and `Resolve()`. See [Building Chains](/docs/core/building-chains). ### Chain verification @@ -42,28 +50,40 @@ The check `AddMediator` registers: at host startup it replays every registered t ### Memory +*Roughly:* the values a run has so far, looked up by type. + The values a running train holds, keyed by type. It starts with the train's input, and each junction's output is added to it. A junction's input, and each constructor argument Memory can supply, is taken from it by type. See [Memory](/docs/core/memory). ### Right track / left track +*Roughly:* the success path and the failure path. + A chain's two outcomes, as Trax.Core's `Either`. Right carries the value on to the next junction; left carries the exception past every remaining junction to the train's result. ### Checkpoint +*Roughly:* a save point a later run resumes from. + `Checkpoint()` in a chain: stores a state the chain declares, so a later run of the same input resumes after it instead of running every step before it again. See [Checkpoint](/docs/sdk-reference/train-methods/checkpoint). ### Decider +*Roughly:* whatever answers a branching question: a rule, a lookup, a model. + `IDecider` in `Trax.Core.Decisions`: what answers a train's typed questions (`Decide`) about a value in Memory, such as a model. With decision recording on, each answer is stored, and a requeued run replays it rather than asking again. See [Decisions](/docs/core/decisions). ### ShortCircuit +*Roughly:* an early return. + A chain step that ends the train early with its output when it succeeds, and lets the chain continue when it throws. See [ShortCircuit](/docs/sdk-reference/train-methods/short-circuit). ## Effects and recording ### Effect +*Roughly:* middleware around every run. + Something that runs alongside every ServiceTrain run without being part of its chain. An effect provider (`IEffectProvider` in `Trax.Effect.Services.EffectProvider`) tracks the run's models and persists them when the train saves. Effects are added on the `AddEffects` builder: a data provider, `AddJson()`, `SaveTrainParameters()`. See [Effect Providers](/docs/effect/effect-providers). ### Junction effect @@ -76,6 +96,8 @@ The effect that stores runs: `UsePostgres(...)`, `UseSqlite(...)` or `UseInMemor ### Metadata +*Roughly:* the record of one run. + `Metadata` in `Trax.Effect.Models.Metadata`, a row of `trax.metadata`. One per ServiceTrain run: the train's name, its `TrainState` (`Pending`, `InProgress`, `Completed`, `Failed`, `Cancelled`), start and end times, input and output when `SaveTrainParameters()` stores them, and on failure the junction, exception, reason and stack trace. `ParentId` links a run to the run that started it. See [Metadata](/docs/effect/metadata). ### Run @@ -102,6 +124,8 @@ A row of `trax.log`: one `ILogger` message captured by `AddDataContextLogging`. ### Mediator +*Roughly:* a dispatcher from an input type to the train that handles it. + What `AddMediator(...)` registers: it discovers the trains in the assemblies you name, so callers can run a train without referencing its class. See [Mediator](/docs/mediator). ### TrainBus @@ -112,6 +136,8 @@ What `AddMediator(...)` registers: it discovers the trains in the assemblies you ### Manifest +*Roughly:* a saved schedule entry, like one line of a crontab. + `Manifest` in `Trax.Effect.Models.Manifest`, a row of `trax.manifest`. A job definition: which train to run, the input to run it with, when (cron, interval, once, after a parent manifest succeeds, or only when triggered), and its retry and timeout policy. Each run of a manifest creates a Metadata row. See [Scheduling](/docs/scheduler). ### IManifestProperties @@ -120,10 +146,14 @@ The marker interface in `Trax.Effect.Models.Manifest` (package `Trax.Effect`, no ### Manifest group +*Roughly:* a concurrency pool for scheduled jobs. + A row of `trax.manifest_group`: settings shared by the manifests in it, `MaxActiveJobs` (how many may run at once), `Priority` (dispatch order between groups) and `IsEnabled`. See [Concurrency](/docs/scheduler/concurrency). ### Work queue +*Roughly:* runs waiting to start. + `trax.work_queue`: runs that have been asked for but not yet dispatched. Every source of a run (a due manifest, a trigger, a dashboard re-run) adds an entry, and the job dispatcher takes entries from it. An entry is `Queued`, `Dispatched` or `Cancelled`. ### Background job @@ -160,6 +190,8 @@ Where a dispatched job runs. Local workers are threads in the scheduler's own pr ### Dependent train +*Roughly:* a job that runs after another one succeeds. + A manifest with `ScheduleType.Dependent`: rather than running on a timer, it runs after its parent manifest's latest success. See [Dependent Trains](/docs/scheduler/dependent-trains). ## API and state machines @@ -174,4 +206,6 @@ An EF entity marked `[TraxQueryModel]` (in `Trax.Effect.Attributes`), which `Add ### State machine +*Roughly:* a long-lived record that moves between named states. + A machine deriving `Machine` in `Trax.Effect.StateMachine.Persistence`: states, triggers and a context record, stored as a language-neutral snapshot that the C# engine and its TypeScript twin both read. See [State Machines](/docs/statemachine). diff --git a/Trax.Docs/reference/packages.md b/Trax.Docs/reference/packages.md index 19b9b8a8a..ad374a64d 100644 --- a/Trax.Docs/reference/packages.md +++ b/Trax.Docs/reference/packages.md @@ -115,11 +115,11 @@ dotnet add package Trax.Api.Dashboard | [Trax.Api.GraphQL](https://www.nuget.org/packages/Trax.Api.GraphQL) | `AddTraxGraphQL(...)` and `UseTraxGraphQL()`: a HotChocolate schema with queries, mutations and subscriptions generated from your trains. See [API](/docs/api) and [AddTraxGraphQL](/docs/sdk-reference/graphql-api/add-trax-graphql). | `Trax.Api.GraphQL.Extensions` | A data provider and an auth package below. It brings `Trax.Api`, `Trax.Mediator` and `Trax.Scheduler` with it. | | [Trax.Api.Dashboard](https://www.nuget.org/packages/Trax.Api.Dashboard) | `UseTraxApiDashboard(...)`: serves the operations dashboard, a React app built into the package, at `/trax/dashboard`, behind the authorization posture you choose. It reads and changes everything through the Trax GraphQL endpoint. See [API Dashboard](/docs/sdk-reference/api-dashboard). | `Trax.Api.Dashboard.Extensions` | `Trax.Api.GraphQL`, mapped with `UseTraxGraphQL()`. | | [Trax.Api.GraphQL.Audit](https://www.nuget.org/packages/Trax.Api.GraphQL.Audit) | `AddAudit()` on the GraphQL builder: an audit entry for each operation, written to your sink. See [AddAudit](/docs/sdk-reference/api-audit/add-audit). | `Trax.Api.GraphQL.Audit` | `Trax.Api.GraphQL`. | -| [Trax.Api.GraphQL.PersistedOperations](https://www.nuget.org/packages/Trax.Api.GraphQL.PersistedOperations) | `UsePersistedOperations(...)` on the GraphQL builder and `AddPersistedOperationStore`: an allow-list of operations that callers run by id. See [Persisted Operations](/docs/persisted-operations). | `Trax.Api.GraphQL.PersistedOperations.Extensions` | `Trax.Api.GraphQL`. | +| [Trax.Api.GraphQL.PersistedOperations](https://www.nuget.org/packages/Trax.Api.GraphQL.PersistedOperations) | `UsePersistedOperations(...)` on the GraphQL builder and `AddPersistedOperationStore`: an allow-list of operations that callers run by id. See [Persisted Operations](/docs/api/persisted-operations). | `Trax.Api.GraphQL.PersistedOperations.Extensions` | `Trax.Api.GraphQL`. | | [Trax.Api.GraphQL.Testing](https://www.nuget.org/packages/Trax.Api.GraphQL.Testing) | Cross-schema GraphQL guards through `CrossSchemaGuards` and `CrossSchemaGuardFixture`. See [Architecture Guards](/docs/reference/architecture-guards). | `Trax.Api.GraphQL.Testing` | `Trax.Core.Testing`, in a test project. | -| [Trax.Api.GraphQL.Client](https://www.nuget.org/packages/Trax.Api.GraphQL.Client) | A typed client for a Trax GraphQL API: `AddTraxGraphQLClient(uri)`, `AddKeyedTraxGraphQLClient`, and `ValidateGraphQLClientAssembliesAsync` to check your request types against the schema. See [GraphQL Client](/docs/api-graphql-client). | `Trax.Api.GraphQL.Client` | Any .NET caller; it needs no other Trax package. | -| [Trax.Api.GraphQL.Client.Typed](https://www.nuget.org/packages/Trax.Api.GraphQL.Client.Typed) | `TypedRequest`, which generates the query string from attributed request and result types. See [GraphQL Client](/docs/api-graphql-client). | `Trax.Api.GraphQL.Client.Typed` | `Trax.Api.GraphQL.Client`. | -| [Trax.Api.GraphQL.Client.Trax](https://www.nuget.org/packages/Trax.Api.GraphQL.Client.Trax) | `UseAssemblySchema(...)` and `UseStartupValidation(...)` on the client builder: validate against the server's schema built in-process. See [GraphQL Client](/docs/api-graphql-client). | `Trax.Api.GraphQL.Client.Trax` | `Trax.Api.GraphQL.Client`, in a .NET process that can reference the server's schema configuration. | +| [Trax.Api.GraphQL.Client](https://www.nuget.org/packages/Trax.Api.GraphQL.Client) | A typed client for a Trax GraphQL API: `AddTraxGraphQLClient(uri)`, `AddKeyedTraxGraphQLClient`, and `ValidateGraphQLClientAssembliesAsync` to check your request types against the schema. See [GraphQL Client](/docs/api/call-another-graphql-server). | `Trax.Api.GraphQL.Client` | Any .NET caller; it needs no other Trax package. | +| [Trax.Api.GraphQL.Client.Typed](https://www.nuget.org/packages/Trax.Api.GraphQL.Client.Typed) | `TypedRequest`, which generates the query string from attributed request and result types. See [GraphQL Client](/docs/api/call-another-graphql-server). | `Trax.Api.GraphQL.Client.Typed` | `Trax.Api.GraphQL.Client`. | +| [Trax.Api.GraphQL.Client.Trax](https://www.nuget.org/packages/Trax.Api.GraphQL.Client.Trax) | `UseAssemblySchema(...)` and `UseStartupValidation(...)` on the client builder: validate against the server's schema built in-process. See [GraphQL Client](/docs/api/call-another-graphql-server). | `Trax.Api.GraphQL.Client.Trax` | `Trax.Api.GraphQL.Client`, in a .NET process that can reference the server's schema configuration. | | [Trax.Api.Auth](https://www.nuget.org/packages/Trax.Api.Auth) | The principal model the auth packages share: `TraxPrincipal`, `ITraxPrincipalResolver`, `AddTraxPrincipalAccessor`. See [TraxPrincipal](/docs/sdk-reference/api-auth/trax-principal). | `Trax.Api.Auth` | Comes with each auth package below. | | [Trax.Api.Auth.ApiKey](https://www.nuget.org/packages/Trax.Api.Auth.ApiKey) | `AddTraxApiKeyAuth(...)`: API-key authentication. See [AddTraxApiKeyAuth](/docs/sdk-reference/api-auth/add-trax-api-key-auth). | `Trax.Api.Auth.ApiKey` | `Trax.Api.GraphQL`. | | [Trax.Api.Auth.Jwt](https://www.nuget.org/packages/Trax.Api.Auth.Jwt) | `AddTraxJwtAuth(...)`, the `AddTraxCognitoJwtAuth`, `AddTraxEntraJwtAuth` and `AddTraxGoogleJwtAuth` presets, and `AddTraxJwtDispatcher` for several issuers on one endpoint. See [AddTraxJwtAuth](/docs/sdk-reference/api-auth/add-trax-jwt-auth). | `Trax.Api.Auth.Jwt` | `Trax.Api.GraphQL`. | diff --git a/Trax.Docs/reference/prepare-a-template-for-deployment.md b/Trax.Docs/reference/prepare-a-template-for-deployment.md index 5bdb57fb1..d1d4ff270 100644 --- a/Trax.Docs/reference/prepare-a-template-for-deployment.md +++ b/Trax.Docs/reference/prepare-a-template-for-deployment.md @@ -48,4 +48,4 @@ than a 401 or 403. 3. Keep `[TraxAuthorize]` on the trains you add. Mark one `[TraxAllowAnonymous]` only when anyone who can reach the host may call it. -[Secure a GraphQL host](/docs/api-security) covers the rest of a production posture. +[Secure a GraphQL host](/docs/security/secure-a-graphql-host) covers the rest of a production posture. diff --git a/Trax.Docs/samples/chat-service.md b/Trax.Docs/samples/chat-service.md index 46a32fc4a..1456bbbd6 100644 --- a/Trax.Docs/samples/chat-service.md +++ b/Trax.Docs/samples/chat-service.md @@ -8,7 +8,7 @@ nav_order: 2 # Chat Service -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). `samples/ChatService` is a chat server whose messages reach the other participants over GraphQL subscriptions. Chat mutations are ordinary Trax trains. When one completes, a lifecycle hook diff --git a/Trax.Docs/samples/content-shield.md b/Trax.Docs/samples/content-shield.md index 86a87480f..e4168fa7a 100644 --- a/Trax.Docs/samples/content-shield.md +++ b/Trax.Docs/samples/content-shield.md @@ -8,7 +8,7 @@ nav_order: 11 # Content Shield -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). `samples/EphemeralWorkers` in [Trax.Samples](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples) is a content moderation API whose trains all run somewhere else: on a runner that is an AWS Lambda function in diff --git a/Trax.Docs/samples/graphql-client.md b/Trax.Docs/samples/graphql-client.md index dab91f4b6..af8684071 100644 --- a/Trax.Docs/samples/graphql-client.md +++ b/Trax.Docs/samples/graphql-client.md @@ -111,7 +111,7 @@ by introspection, and a Trax server answers introspection only in Development un and so the two servers it starts in-process, in Development; started in Production, the servers refuse introspection and the Gateway stops with `GraphQLSchemaIntrospectionException`. Against a production server the client uses `UseFileSchema` or `UseAssemblySchema` instead, or the server admits only the -callers it trusts. See [GraphQL Client: Schema Providers](/docs/api-graphql-client#schema-providers). +callers it trusts. See [GraphQL Client: Schema Providers](/docs/api/call-another-graphql-server#schema-providers). ### Two keyed clients in one container diff --git a/Trax.Docs/samples/signalr-broadcaster.md b/Trax.Docs/samples/signalr-broadcaster.md index 7649fce86..059ed9636 100644 --- a/Trax.Docs/samples/signalr-broadcaster.md +++ b/Trax.Docs/samples/signalr-broadcaster.md @@ -8,7 +8,7 @@ nav_order: 12 # SignalR Broadcaster -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). `samples/SignalRBroadcaster` in [Trax.Samples](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples) pushes each train's lifecycle events to a browser as they happen. It is one process with one train, a plain HTML diff --git a/Trax.Docs/scheduler/remote-execution.md b/Trax.Docs/scheduler/remote-execution.md index 43b6f0df4..f53cce63e 100644 --- a/Trax.Docs/scheduler/remote-execution.md +++ b/Trax.Docs/scheduler/remote-execution.md @@ -167,7 +167,7 @@ must not kill running trains, and Kubernetes or ECS services scaled on their own Two things stay on the scheduler. A standalone worker has no API to register an authorization service on, and only runs work that passed the check where it was submitted, so a worker with `[TraxAuthorize]` trains declares that with `AllowMissingAuthorizationService()` (see -[Authorization: Opting Out for Scheduler-Only Hosts](/docs/authorization#opting-out-for-scheduler-only-hosts)). +[Authorization: Opting Out for Scheduler-Only Hosts](/docs/security/authorization-rules#opting-out-for-scheduler-only-hosts)). And synchronous work is still answered by the process that receives the request: a `[TraxQuery]` or a `RUN` mutation does not reach a worker, so mutations that should always run on one are declared `GraphQLOperation.Queue`. The [Energy Hub sample](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples/samples/DistributedWorkers) runs every diff --git a/Trax.Docs/sdk-reference/api-audit.md b/Trax.Docs/sdk-reference/api-audit.md index 34d46f597..73694b11e 100644 --- a/Trax.Docs/sdk-reference/api-audit.md +++ b/Trax.Docs/sdk-reference/api-audit.md @@ -10,9 +10,9 @@ nav_order: 9 # API Audit -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). -Reference for the request-level audit pipeline of a Trax GraphQL host. How it fits together is in [Audit pipeline](/docs/api-security#3-write-the-audit-sink). +Reference for the request-level audit pipeline of a Trax GraphQL host. How it fits together is in [Audit pipeline](/docs/security/secure-a-graphql-host#3-write-the-audit-sink). ## Trax.Api.GraphQL.Audit diff --git a/Trax.Docs/sdk-reference/api-audit/add-audit.md b/Trax.Docs/sdk-reference/api-audit/add-audit.md index e5c8a64ff..7b632e11b 100644 --- a/Trax.Docs/sdk-reference/api-audit/add-audit.md +++ b/Trax.Docs/sdk-reference/api-audit/add-audit.md @@ -14,7 +14,7 @@ source: # AddAudit -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension method on `TraxGraphQLBuilder`, declared on `TraxGraphQLBuilderAuditExtensions`, that registers the GraphQL audit pipeline: a bounded channel, a background writer, a HotChocolate diagnostic listener, the sink `TSink` and a redactor. @@ -78,7 +78,7 @@ services.AddTraxGraphQL(graphql => ## See also -- [Audit pipeline](/docs/api-security#3-write-the-audit-sink): what an entry records, drops and shutdown. +- [Audit pipeline](/docs/security/secure-a-graphql-host#3-write-the-audit-sink): what an entry records, drops and shutdown. - [Tune the audit pipeline](/docs/security/tune-the-audit-pipeline) - [TraxAuditOptions](/docs/sdk-reference/api-audit/trax-audit-options), [ITraxAuditSink](/docs/sdk-reference/api-audit/i-trax-audit-sink), [ITraxAuditRedactor](/docs/sdk-reference/api-audit/i-trax-audit-redactor) - The [Auth sample](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples/samples/Auth) stores entries in a table and serves them to auditors as a gated query model. diff --git a/Trax.Docs/sdk-reference/api-audit/i-trax-audit-redactor.md b/Trax.Docs/sdk-reference/api-audit/i-trax-audit-redactor.md index f8963b64a..679c05f01 100644 --- a/Trax.Docs/sdk-reference/api-audit/i-trax-audit-redactor.md +++ b/Trax.Docs/sdk-reference/api-audit/i-trax-audit-redactor.md @@ -14,7 +14,7 @@ source: # ITraxAuditRedactor -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Decides which GraphQL variables, if any, an audit entry records. `AddAudit` registers `DefaultAuditRedactor`, which records none, unless the host has registered its own. @@ -93,5 +93,5 @@ A removal list keeps any field it does not name. An allow-list, copying across o ## See also -- [What an entry records](/docs/api-security#what-an-entry-records) +- [What an entry records](/docs/security/secure-a-graphql-host#what-an-entry-records) - [AddAudit](/docs/sdk-reference/api-audit/add-audit), [TraxAuditEntry](/docs/sdk-reference/api-audit/trax-audit-entry) diff --git a/Trax.Docs/sdk-reference/api-audit/i-trax-audit-sink.md b/Trax.Docs/sdk-reference/api-audit/i-trax-audit-sink.md index 29bac14d7..1d65b56de 100644 --- a/Trax.Docs/sdk-reference/api-audit/i-trax-audit-sink.md +++ b/Trax.Docs/sdk-reference/api-audit/i-trax-audit-sink.md @@ -14,7 +14,7 @@ source: # ITraxAuditSink -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The destination for audit entries, implemented by the host (a database table, a log pipeline, object storage) and registered with `AddAudit()`. diff --git a/Trax.Docs/sdk-reference/api-audit/trax-audit-channel.md b/Trax.Docs/sdk-reference/api-audit/trax-audit-channel.md index b24399626..34931e258 100644 --- a/Trax.Docs/sdk-reference/api-audit/trax-audit-channel.md +++ b/Trax.Docs/sdk-reference/api-audit/trax-audit-channel.md @@ -14,7 +14,7 @@ source: # TraxAuditChannel -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The bounded queue between the audit listener, which offers entries, and the background writer, which drains them. `AddAudit` registers it as a singleton. It owns the `Trax.Audit` meter and its `trax.audit.dropped` counter. diff --git a/Trax.Docs/sdk-reference/api-audit/trax-audit-entry.md b/Trax.Docs/sdk-reference/api-audit/trax-audit-entry.md index 074eeb09a..95cd082bf 100644 --- a/Trax.Docs/sdk-reference/api-audit/trax-audit-entry.md +++ b/Trax.Docs/sdk-reference/api-audit/trax-audit-entry.md @@ -14,7 +14,7 @@ source: # TraxAuditEntry -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Immutable record of one audited GraphQL request, including one the endpoint policy refused and a subscription refused when it subscribed. Two more kinds of entry describe something other than a whole request: a socket refused at `connection_init`, and one failed event of an accepted subscription. The listener builds entries and the writer passes them in batches to [ITraxAuditSink.WriteAsync](/docs/sdk-reference/api-audit/i-trax-audit-sink). @@ -63,5 +63,5 @@ A host that replaces Trax's socket interceptor through `ConfigureSchema` gets no ## See also -- [What an entry records](/docs/api-security#what-an-entry-records) +- [What an entry records](/docs/security/secure-a-graphql-host#what-an-entry-records) - [AddAudit](/docs/sdk-reference/api-audit/add-audit), [TraxAuditOptions](/docs/sdk-reference/api-audit/trax-audit-options), [ITraxAuditRedactor](/docs/sdk-reference/api-audit/i-trax-audit-redactor) diff --git a/Trax.Docs/sdk-reference/api-audit/trax-audit-options.md b/Trax.Docs/sdk-reference/api-audit/trax-audit-options.md index 56a0b330f..79fa6f231 100644 --- a/Trax.Docs/sdk-reference/api-audit/trax-audit-options.md +++ b/Trax.Docs/sdk-reference/api-audit/trax-audit-options.md @@ -15,7 +15,7 @@ source: # TraxAuditOptions -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The options of the audit pipeline, set through `AddAudit(opts => ...)`. diff --git a/Trax.Docs/sdk-reference/api-auth.md b/Trax.Docs/sdk-reference/api-auth.md index 1c52a817a..4144bf438 100644 --- a/Trax.Docs/sdk-reference/api-auth.md +++ b/Trax.Docs/sdk-reference/api-auth.md @@ -10,9 +10,9 @@ nav_order: 8 # API Auth -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). -Reference for the Trax authentication packages. How they fit into a secured host is in [API Security](/docs/api-security). +Reference for the Trax authentication packages. How they fit into a secured host is in [API Security](/docs/security/secure-a-graphql-host). ## Trax.Api.Auth diff --git a/Trax.Docs/sdk-reference/api-auth/add-trax-api-key-auth.md b/Trax.Docs/sdk-reference/api-auth/add-trax-api-key-auth.md index c8e583d29..b76bdb3ac 100644 --- a/Trax.Docs/sdk-reference/api-auth/add-trax-api-key-auth.md +++ b/Trax.Docs/sdk-reference/api-auth/add-trax-api-key-auth.md @@ -17,7 +17,7 @@ source: # AddTraxApiKeyAuth -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension methods on `IServiceCollection`, declared on `ApiKeyAuthServiceCollectionExtensions`, that register the Trax API-key authentication scheme. One overload takes a static key set built with `ApiKeyBuilder`; the other takes a resolver from DI. @@ -198,7 +198,7 @@ public sealed class DirectoryApiKeyResolver(IUserDirectory users) : ITraxPrincip ## See also -- [API-key authentication](/docs/api-security#1-write-the-host) +- [API-key authentication](/docs/security/secure-a-graphql-host#1-write-the-host) - [HashedApiKeyResolver](/docs/sdk-reference/api-auth/hashed-api-key-resolver), [ApiKeyAuthenticationOptions](/docs/sdk-reference/api-auth/api-key-authentication-options), [ApiKeyDefaults](/docs/sdk-reference/api-auth/api-key-defaults) -- [Authorization](/docs/authorization#combining-with-endpoint-level-auth) +- [Authorization](/docs/security/authorization-rules#combining-with-endpoint-level-auth) - The [Auth sample](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples/samples/Auth) registers keys conditionally. diff --git a/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-auth.md b/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-auth.md index 710d9c1c8..300093805 100644 --- a/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-auth.md +++ b/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-auth.md @@ -18,7 +18,7 @@ source: # AddTraxJwtAuth -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension methods on `IServiceCollection`, declared on `JwtAuthServiceCollectionExtensions`, that register a Trax JWT bearer authentication scheme. `JwtBearerHandler` validates the token's signature, issuer, audience and lifetime; an `ITraxPrincipalResolver` then maps the validated token to a `TraxPrincipal`. @@ -194,6 +194,6 @@ app.MapGet("/internal", () => Results.Ok()).RequireAuthorization(TraxAuthClaimTy ## See also -- [JWT bearer authentication](/docs/api-security#1-write-the-host) +- [JWT bearer authentication](/docs/security/secure-a-graphql-host#1-write-the-host) - [Write a JWT principal resolver](/docs/security/write-a-jwt-principal-resolver), [Sign development JWTs](/docs/security/sign-development-jwts), [Test JWT authentication](/docs/security/test-jwt-authentication) - [JwtProviderExtensions](/docs/sdk-reference/api-auth/jwt-provider-extensions), [AddTraxJwtDispatcher](/docs/sdk-reference/api-auth/add-trax-jwt-dispatcher), [UseCognito](/docs/sdk-reference/api-auth/use-cognito), [JwtDefaults](/docs/sdk-reference/api-auth/jwt-defaults) diff --git a/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-dispatcher.md b/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-dispatcher.md index e154a5f76..65319e604 100644 --- a/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-dispatcher.md +++ b/Trax.Docs/sdk-reference/api-auth/add-trax-jwt-dispatcher.md @@ -17,7 +17,7 @@ source: # AddTraxJwtDispatcher -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension method on `IServiceCollection`, declared on `JwtAuthServiceCollectionExtensions`, that registers a policy authentication scheme. It reads the `iss` claim of the request's Bearer token and forwards authentication to the JWT scheme, registered with [AddTraxJwtAuth](/docs/sdk-reference/api-auth/add-trax-jwt-auth), that the issuer maps to. diff --git a/Trax.Docs/sdk-reference/api-auth/add-trax-oidc-auth.md b/Trax.Docs/sdk-reference/api-auth/add-trax-oidc-auth.md index 980a6649a..73816fa42 100644 --- a/Trax.Docs/sdk-reference/api-auth/add-trax-oidc-auth.md +++ b/Trax.Docs/sdk-reference/api-auth/add-trax-oidc-auth.md @@ -17,7 +17,7 @@ source: # AddTraxOidcAuth -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension methods on `IServiceCollection`, declared on `OidcAuthServiceCollectionExtensions`, that register the Trax OpenID Connect authorization-code scheme (`OidcDefaults.SchemeName`) and its session cookie (`OidcDefaults.CookieSchemeName`). A challenge against the OIDC scheme redirects the browser to the identity provider; on the callback the handler validates the id-token, an `ITraxPrincipalResolver` maps it to a `TraxPrincipal`, and the user is signed into the cookie scheme. For API clients that send tokens, the scheme is [AddTraxJwtAuth](/docs/sdk-reference/api-auth/add-trax-jwt-auth). @@ -128,7 +128,7 @@ Returns `null`, failing the sign-in, when the id-token has no subject. ### Policies -`OidcDefaults.PolicyName` authenticates against the cookie scheme and is satisfied only by a caller the OIDC session signed in. The cookie scheme joins `TraxAuthClaimTypes.TraxAuthPolicy`. On the Trax GraphQL endpoint, the builder's `RequireAuthorization(...)` is the gate the startup checks for the `operations` namespace and `[TraxAllowAnonymous]` see; a convention on the route is an extra layer those checks do not count. See [Authorization](/docs/authorization#combining-with-endpoint-level-auth). +`OidcDefaults.PolicyName` authenticates against the cookie scheme and is satisfied only by a caller the OIDC session signed in. The cookie scheme joins `TraxAuthClaimTypes.TraxAuthPolicy`. On the Trax GraphQL endpoint, the builder's `RequireAuthorization(...)` is the gate the startup checks for the `operations` namespace and `[TraxAllowAnonymous]` see; a convention on the route is an extra layer those checks do not count. See [Authorization](/docs/security/authorization-rules#combining-with-endpoint-level-auth). ### Subscriptions @@ -190,5 +190,5 @@ public sealed class EntraOidcResolver : ITraxPrincipalResolver ## See also -- [OpenID Connect authentication](/docs/api-security#1-write-the-host) +- [OpenID Connect authentication](/docs/security/secure-a-graphql-host#1-write-the-host) - [OidcDefaults](/docs/sdk-reference/api-auth/oidc-defaults), [ITraxPrincipalResolver](/docs/sdk-reference/api-auth/i-trax-principal-resolver) diff --git a/Trax.Docs/sdk-reference/api-auth/add-trax-principal-accessor.md b/Trax.Docs/sdk-reference/api-auth/add-trax-principal-accessor.md index 90f79e7c7..4b681d437 100644 --- a/Trax.Docs/sdk-reference/api-auth/add-trax-principal-accessor.md +++ b/Trax.Docs/sdk-reference/api-auth/add-trax-principal-accessor.md @@ -15,7 +15,7 @@ source: # AddTraxPrincipalAccessor -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension method on `IServiceCollection`, declared on `TraxAuthServiceCollectionExtensions`, that registers the current request's [TraxPrincipal](/docs/sdk-reference/api-auth/trax-principal) and [TraxCaller](/docs/sdk-reference/api-auth/trax-caller) as injectable services. Every `AddTrax*Auth` call runs it. diff --git a/Trax.Docs/sdk-reference/api-auth/api-key-authentication-options.md b/Trax.Docs/sdk-reference/api-auth/api-key-authentication-options.md index ffb4b9263..84ea3ba5e 100644 --- a/Trax.Docs/sdk-reference/api-auth/api-key-authentication-options.md +++ b/Trax.Docs/sdk-reference/api-auth/api-key-authentication-options.md @@ -14,7 +14,7 @@ source: # ApiKeyAuthenticationOptions -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The options of the API-key scheme, set through the `configureOptions` argument of [AddTraxApiKeyAuth](/docs/sdk-reference/api-auth/add-trax-api-key-auth). It derives from `AuthenticationSchemeOptions` and has all of its members. diff --git a/Trax.Docs/sdk-reference/api-auth/api-key-defaults.md b/Trax.Docs/sdk-reference/api-auth/api-key-defaults.md index c5682baf7..66818e29a 100644 --- a/Trax.Docs/sdk-reference/api-auth/api-key-defaults.md +++ b/Trax.Docs/sdk-reference/api-auth/api-key-defaults.md @@ -14,7 +14,7 @@ source: # ApiKeyDefaults -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The constants of the Trax API-key scheme that [AddTraxApiKeyAuth](/docs/sdk-reference/api-auth/add-trax-api-key-auth) registers. diff --git a/Trax.Docs/sdk-reference/api-auth/cognito-defaults.md b/Trax.Docs/sdk-reference/api-auth/cognito-defaults.md index 79938168f..c8e231d4e 100644 --- a/Trax.Docs/sdk-reference/api-auth/cognito-defaults.md +++ b/Trax.Docs/sdk-reference/api-auth/cognito-defaults.md @@ -14,7 +14,7 @@ source: # CognitoDefaults -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The claim names and values of Amazon Cognito user pool tokens, as [UseCognito](/docs/sdk-reference/api-auth/use-cognito), `CognitoJwtPrincipalResolver` and [CognitoTokenIssuer](/docs/sdk-reference/api-auth/cognito-token-issuer) use them. diff --git a/Trax.Docs/sdk-reference/api-auth/cognito-token-issuer.md b/Trax.Docs/sdk-reference/api-auth/cognito-token-issuer.md index c1cfbdbba..a52947a35 100644 --- a/Trax.Docs/sdk-reference/api-auth/cognito-token-issuer.md +++ b/Trax.Docs/sdk-reference/api-auth/cognito-token-issuer.md @@ -20,7 +20,7 @@ redirect_from: # CognitoTokenIssuer -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Mints RS256 access and ID tokens with the claims Amazon Cognito user pools emit, for a local Cognito stand-in or for tests. It is the minting counterpart of `Trax.Api.Auth.Jwt.Cognito`, which validates such tokens; a host that only validates does not reference this package. Refresh tokens are on [IRefreshTokenStore](/docs/sdk-reference/api-auth/i-refresh-token-store). diff --git a/Trax.Docs/sdk-reference/api-auth/hashed-api-key-resolver.md b/Trax.Docs/sdk-reference/api-auth/hashed-api-key-resolver.md index 62894c17e..badea9942 100644 --- a/Trax.Docs/sdk-reference/api-auth/hashed-api-key-resolver.md +++ b/Trax.Docs/sdk-reference/api-auth/hashed-api-key-resolver.md @@ -14,7 +14,7 @@ source: # HashedApiKeyResolver -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). An `ITraxPrincipalResolver` over a fixed set of salted SHA-256 key hashes. `AddTraxApiKeyAuth(keys => ...)` builds one from the keys `ApiKeyBuilder` collected and registers it as a singleton. diff --git a/Trax.Docs/sdk-reference/api-auth/i-refresh-token-store.md b/Trax.Docs/sdk-reference/api-auth/i-refresh-token-store.md index 383bfb1a5..2b8089ab0 100644 --- a/Trax.Docs/sdk-reference/api-auth/i-refresh-token-store.md +++ b/Trax.Docs/sdk-reference/api-auth/i-refresh-token-store.md @@ -15,7 +15,7 @@ source: # IRefreshTokenStore -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The operations a Cognito-compatible auth server needs for opaque refresh tokens: issue, validate, rotate and revoke. Tokens belong to rotation chains bound to a `(sub, clientId)` pair. `InMemoryRefreshTokenStore` implements it in process memory. diff --git a/Trax.Docs/sdk-reference/api-auth/i-trax-principal-resolver.md b/Trax.Docs/sdk-reference/api-auth/i-trax-principal-resolver.md index 4ba55aa11..e454aae6a 100644 --- a/Trax.Docs/sdk-reference/api-auth/i-trax-principal-resolver.md +++ b/Trax.Docs/sdk-reference/api-auth/i-trax-principal-resolver.md @@ -14,7 +14,7 @@ source: # ITraxPrincipalResolver -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Maps a credential to a [TraxPrincipal](/docs/sdk-reference/api-auth/trax-principal). Generic over the input, so one abstraction serves every Trax scheme. diff --git a/Trax.Docs/sdk-reference/api-auth/jwt-defaults.md b/Trax.Docs/sdk-reference/api-auth/jwt-defaults.md index dcfdd56cd..986dccf7c 100644 --- a/Trax.Docs/sdk-reference/api-auth/jwt-defaults.md +++ b/Trax.Docs/sdk-reference/api-auth/jwt-defaults.md @@ -14,7 +14,7 @@ source: # JwtDefaults -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The constants of the Trax JWT bearer scheme that [AddTraxJwtAuth](/docs/sdk-reference/api-auth/add-trax-jwt-auth) registers, and of the dispatcher [AddTraxJwtDispatcher](/docs/sdk-reference/api-auth/add-trax-jwt-dispatcher) registers. diff --git a/Trax.Docs/sdk-reference/api-auth/jwt-provider-extensions.md b/Trax.Docs/sdk-reference/api-auth/jwt-provider-extensions.md index b7b627021..e2bcc3c50 100644 --- a/Trax.Docs/sdk-reference/api-auth/jwt-provider-extensions.md +++ b/Trax.Docs/sdk-reference/api-auth/jwt-provider-extensions.md @@ -14,7 +14,7 @@ source: # JwtProviderExtensions -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension methods on `IServiceCollection` that call [AddTraxJwtAuth](/docs/sdk-reference/api-auth/add-trax-jwt-auth)`(authority, audience)` with the authority URL of a common identity provider. Each has a `` overload that passes the resolver through. diff --git a/Trax.Docs/sdk-reference/api-auth/oidc-defaults.md b/Trax.Docs/sdk-reference/api-auth/oidc-defaults.md index 7420cc624..4a24d59a8 100644 --- a/Trax.Docs/sdk-reference/api-auth/oidc-defaults.md +++ b/Trax.Docs/sdk-reference/api-auth/oidc-defaults.md @@ -14,7 +14,7 @@ source: # OidcDefaults -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The constants of the Trax OpenID Connect scheme that [AddTraxOidcAuth](/docs/sdk-reference/api-auth/add-trax-oidc-auth) registers. diff --git a/Trax.Docs/sdk-reference/api-auth/test-jwks-server.md b/Trax.Docs/sdk-reference/api-auth/test-jwks-server.md index 0f2d4c1db..c84f98de3 100644 --- a/Trax.Docs/sdk-reference/api-auth/test-jwks-server.md +++ b/Trax.Docs/sdk-reference/api-auth/test-jwks-server.md @@ -17,7 +17,7 @@ redirect_from: # TestJwksServer -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). An in-process Kestrel host that serves an OIDC discovery document and a JWKS, for tests of Trax JWT authentication that do not reach a real identity provider. It generates an RSA key pair at startup and publishes the public key. The package is for test projects. Tokens are minted with [TestTokenIssuer](/docs/sdk-reference/api-auth/test-token-issuer). diff --git a/Trax.Docs/sdk-reference/api-auth/test-token-issuer.md b/Trax.Docs/sdk-reference/api-auth/test-token-issuer.md index abdb12465..920df31a3 100644 --- a/Trax.Docs/sdk-reference/api-auth/test-token-issuer.md +++ b/Trax.Docs/sdk-reference/api-auth/test-token-issuer.md @@ -14,7 +14,7 @@ source: # TestTokenIssuer -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Mints signed JWTs for tests: RS256 through [TestJwksServer.CreateIssuer](/docs/sdk-reference/api-auth/test-jwks-server), HS256 through `Symmetric`, or any `SigningCredentials` through the constructor. `TestTokenBuilder` sets each token's claims. diff --git a/Trax.Docs/sdk-reference/api-auth/trax-auth-claim-types.md b/Trax.Docs/sdk-reference/api-auth/trax-auth-claim-types.md index 62c101dd5..690408648 100644 --- a/Trax.Docs/sdk-reference/api-auth/trax-auth-claim-types.md +++ b/Trax.Docs/sdk-reference/api-auth/trax-auth-claim-types.md @@ -14,7 +14,7 @@ source: # TraxAuthClaimTypes -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The claim types every Trax authentication scheme writes, and the name of the policy every scheme joins. diff --git a/Trax.Docs/sdk-reference/api-auth/trax-caller.md b/Trax.Docs/sdk-reference/api-auth/trax-caller.md index c6bcba92b..2ad3fc7d6 100644 --- a/Trax.Docs/sdk-reference/api-auth/trax-caller.md +++ b/Trax.Docs/sdk-reference/api-auth/trax-caller.md @@ -14,7 +14,7 @@ source: # TraxCaller -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The current caller, as a scoped service that never throws. Where an injected [TraxPrincipal](/docs/sdk-reference/api-auth/trax-principal) throws `TraxPrincipalNotAvailableException` for an anonymous caller, `TraxCaller` reports `null`, so it fits code that runs for anonymous and authenticated callers alike: row-level query filters, subscription resolvers, junctions shared by gated and ungated trains. @@ -45,7 +45,7 @@ public sealed class TraxCaller On a subscription the socket's principal is set on the connection's `HttpContext.User` at `connection_init`, so a `TraxCaller` injected into a subscribe resolver sees the subscriber. -`IsTrusted` is true for whatever a scheduler runner's posture admits, so a filter that lets trusted execution see every row treats that caller as the scheduler. See [The scheduler and remote workers are trusted](/docs/authorization#the-scheduler-and-remote-workers-are-trusted). +`IsTrusted` is true for whatever a scheduler runner's posture admits, so a filter that lets trusted execution see every row treats that caller as the scheduler. See [The scheduler and remote workers are trusted](/docs/security/authorization-rules#the-scheduler-and-remote-workers-are-trusted). `TraxCaller` is registered scoped by [AddTraxPrincipalAccessor](/docs/sdk-reference/api-auth/add-trax-principal-accessor), which every `AddTrax*Auth` call runs. A host with no scheme registered in some environment calls `AddTraxPrincipalAccessor()` itself, or `TraxCaller` does not resolve there. diff --git a/Trax.Docs/sdk-reference/api-auth/trax-principal.md b/Trax.Docs/sdk-reference/api-auth/trax-principal.md index add248224..66a462590 100644 --- a/Trax.Docs/sdk-reference/api-auth/trax-principal.md +++ b/Trax.Docs/sdk-reference/api-auth/trax-principal.md @@ -16,7 +16,7 @@ source: # TraxPrincipal -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). The identity record an [ITraxPrincipalResolver](/docs/sdk-reference/api-auth/i-trax-principal-resolver) returns, independent of any authentication framework. `TraxPrincipalExtensions` projects it to an ASP.NET Core `ClaimsPrincipal` and back; `TraxPrincipalId` builds the scheme-qualified id the claims carry. diff --git a/Trax.Docs/sdk-reference/api-auth/use-cognito.md b/Trax.Docs/sdk-reference/api-auth/use-cognito.md index c7ffef792..eb9f058da 100644 --- a/Trax.Docs/sdk-reference/api-auth/use-cognito.md +++ b/Trax.Docs/sdk-reference/api-auth/use-cognito.md @@ -16,7 +16,7 @@ source: # UseCognito -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [API Security](/docs/security/secure-a-graphql-host). Extension method on `JwtBuilder`, declared on `CognitoJwtBuilderExtensions`, that configures an [AddTraxJwtAuth](/docs/sdk-reference/api-auth/add-trax-jwt-auth) scheme to validate tokens from an Amazon Cognito user pool. The package also ships `CognitoJwtPrincipalResolver`, a resolver for Cognito's claim shapes. diff --git a/Trax.Docs/sdk-reference/attributes/trax-allow-anonymous.md b/Trax.Docs/sdk-reference/attributes/trax-allow-anonymous.md index d1fcd11d3..62118db05 100644 --- a/Trax.Docs/sdk-reference/attributes/trax-allow-anonymous.md +++ b/Trax.Docs/sdk-reference/attributes/trax-allow-anonymous.md @@ -12,7 +12,7 @@ grand_parent: SDK Reference nav_order: 2 --- -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/security/secure-a-graphql-host). # TraxAllowAnonymous @@ -71,6 +71,6 @@ public class LookupAnnouncementTrain ## See also -- [Anonymous Access via TraxAllowAnonymous](/docs/authorization#anonymous-access-via-traxallowanonymous) and [Required Exposure Posture](/docs/authorization#required-exposure-posture) +- [Anonymous Access via TraxAllowAnonymous](/docs/security/authorization-rules#anonymous-access-via-traxallowanonymous) and [Required Exposure Posture](/docs/security/authorization-rules#required-exposure-posture) - [TraxAuthorize](/docs/sdk-reference/attributes/trax-authorize) diff --git a/Trax.Docs/sdk-reference/attributes/trax-authorization.md b/Trax.Docs/sdk-reference/attributes/trax-authorization.md index 25f68b99c..e6d7835d5 100644 --- a/Trax.Docs/sdk-reference/attributes/trax-authorization.md +++ b/Trax.Docs/sdk-reference/attributes/trax-authorization.md @@ -12,7 +12,7 @@ grand_parent: SDK Reference nav_order: 6 --- -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/security/secure-a-graphql-host). # TraxAuthorization @@ -93,4 +93,4 @@ public static class PostureCheck ## See also - [TraxAuthorize](/docs/sdk-reference/attributes/trax-authorize) and [TraxAllowAnonymous](/docs/sdk-reference/attributes/trax-allow-anonymous) -- [Authorization](/docs/authorization) +- [Authorization](/docs/security/authorization-rules) diff --git a/Trax.Docs/sdk-reference/attributes/trax-authorize.md b/Trax.Docs/sdk-reference/attributes/trax-authorize.md index 19bbc2a23..83bc55aeb 100644 --- a/Trax.Docs/sdk-reference/attributes/trax-authorize.md +++ b/Trax.Docs/sdk-reference/attributes/trax-authorize.md @@ -12,7 +12,7 @@ grand_parent: SDK Reference nav_order: 1 --- -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/security/secure-a-graphql-host). # TraxAuthorize @@ -101,7 +101,7 @@ The caller must pass `MustBeInternal` and hold `Admin` or `Manager`. ## See also -- [Authorization](/docs/authorization), the full model and the exposure posture every GraphQL surface declares +- [Authorization](/docs/security/authorization-rules), the full model and the exposure posture every GraphQL surface declares - [TraxAllowAnonymous](/docs/sdk-reference/attributes/trax-allow-anonymous) and [TraxAuthorization](/docs/sdk-reference/attributes/trax-authorization) - [Auth sample](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples/samples/Auth), each form on a running host diff --git a/Trax.Docs/sdk-reference/graphql-api/add-trax-graphql.md b/Trax.Docs/sdk-reference/graphql-api/add-trax-graphql.md index 6070fc18d..53c2c93b7 100644 --- a/Trax.Docs/sdk-reference/graphql-api/add-trax-graphql.md +++ b/Trax.Docs/sdk-reference/graphql-api/add-trax-graphql.md @@ -61,7 +61,7 @@ Every method on `TraxGraphQLBuilder` returns the builder. | Method | Description | |--------|-------------| | `AddDbContext()` | Registers a DbContext whose `DbSet` entities marked with `[TraxQueryModel]` are exposed as GraphQL queries. See [query models](/docs/sdk-reference/graphql-api/query-models). | -| `AddTypeModule()` | Registers an additional HotChocolate `TypeModule` on the Trax schema. The module can add resolvers, DataLoader-backed relationship fields, or `ObjectTypeExtension`s on entities registered by `[TraxQueryModel]`. The module is registered as a singleton in DI. A field it contributes follows the [type-extension posture rule](/docs/authorization#fields-added-by-a-type-extension): `[TraxAuthorize]` on its resolver is a gate, and a field on a root or anonymous parent must declare one. | +| `AddTypeModule()` | Registers an additional HotChocolate `TypeModule` on the Trax schema. The module can add resolvers, DataLoader-backed relationship fields, or `ObjectTypeExtension`s on entities registered by `[TraxQueryModel]`. The module is registered as a singleton in DI. A field it contributes follows the [type-extension posture rule](/docs/security/authorization-rules#fields-added-by-a-type-extension): `[TraxAuthorize]` on its resolver is a gate, and a field on a root or anonymous parent must declare one. | | `AddFilterType()` | Overrides the auto-generated `FilterInputType` for a specific entity. `TFilter` must extend `FilterInputType`. See [custom filter and sort types](/docs/sdk-reference/graphql-api/query-models#custom-filter-and-sort-types). | | `AddSortType()` | Overrides the auto-generated `SortInputType` for a specific entity. `TSort` must extend `SortInputType`. See [custom filter and sort types](/docs/sdk-reference/graphql-api/query-models#custom-filter-and-sort-types). | | `ConfigureFiltering(Func)` | Layers opt-in operators onto HotChocolate's filter convention. Today: `AddCaseInsensitiveStringOperations()` adds `icontains` and `ieq` to every string filter input. Stock filtering is the default; nothing changes unless this is called. See [configure filtering](/docs/sdk-reference/graphql-api/configure-filtering). | @@ -271,5 +271,5 @@ builder.Services.AddTraxGraphQL(graphql => graphql - [Queries](/docs/sdk-reference/graphql-api/queries), [Mutations](/docs/sdk-reference/graphql-api/mutations), [Subscriptions](/docs/sdk-reference/graphql-api/subscriptions) - [Query models](/docs/sdk-reference/graphql-api/query-models) - [Configure filtering](/docs/sdk-reference/graphql-api/configure-filtering) -- [Authorization](/docs/authorization) -- [API security](/docs/api-security) +- [Authorization](/docs/security/authorization-rules) +- [API security](/docs/security/secure-a-graphql-host) diff --git a/Trax.Docs/sdk-reference/graphql-api/error-codes.md b/Trax.Docs/sdk-reference/graphql-api/error-codes.md index 399ab9c9d..aab84c6ed 100644 --- a/Trax.Docs/sdk-reference/graphql-api/error-codes.md +++ b/Trax.Docs/sdk-reference/graphql-api/error-codes.md @@ -28,7 +28,7 @@ These arrive in the response's top-level `errors` array. | Code | Message | Raised by | |---|---|---| -| `TRAX_AUTHORIZATION` | `Not authorized.` | Any authorization refusal. The `@authorize` directive on a `[TraxAuthorize]` query model, field or navigation target, and the filter and sort inputs that reach one; a train's `[TraxAuthorize]` requirements on `dispatch`, `queueTrain`, `runTrain` and `requeueExecution` (`TrainAuthorizationException`); the operations gate (`GateOperations`); the endpoint policy (`RequireAuthorization`), over HTTP and for every operation on a socket; a subscription that could receive nothing, refused when it subscribes. The train, policy and role names never appear in it. See [Authorization](/docs/authorization). | +| `TRAX_AUTHORIZATION` | `Not authorized.` | Any authorization refusal. The `@authorize` directive on a `[TraxAuthorize]` query model, field or navigation target, and the filter and sort inputs that reach one; a train's `[TraxAuthorize]` requirements on `dispatch`, `queueTrain`, `runTrain` and `requeueExecution` (`TrainAuthorizationException`); the operations gate (`GateOperations`); the endpoint policy (`RequireAuthorization`), over HTTP and for every operation on a socket; a subscription that could receive nothing, refused when it subscribes. The train, policy and role names never appear in it. See [Authorization](/docs/security/authorization-rules). | | `TRAX_TRAIN_NOT_FOUND` | `The requested train was not found.` | `TrainNotFoundException`: a train name that matches no registered train. The name sent is not echoed back. | | `TRAX_AMBIGUOUS_TRAIN` | Lists the candidate FullNames | `AmbiguousTrainNameException`: a short train name that matches more than one registered train. | | `TRAX_INVALID_INPUT` | `The train input failed validation.` | `TrainInputValidationException`: input JSON that does not deserialize to the train's input type, or that is larger than the mediator's `WithMaxInputJsonBytes` limit (256 KiB by default). | @@ -42,7 +42,7 @@ These arrive in the response's top-level `errors` array. | `PERSISTED_OPERATION_REQUIRED` | `Only persisted operations are accepted on this server.` | Persisted-operation enforcement: an inline document the server does not accept. HTTP status 400. See [UsePersistedOperationsEnforcement](/docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement). | | `CLIENT_OUTDATED` | `This client was built for another version of the operation than this server runs. Reload it.` | A [machine document](/docs/sdk-reference/graphql-api/machines#persisted-documents) id of an older version of the machine than this node runs, one of a machine this node does not serve that was collected, or one no version generated (malformed, or absent from a read of the store made after it was first asked for). Nothing runs; the client reloads. HTTP status 400. | | `SERVER_OUTDATED` | `This client runs a newer version of the operation than this server does yet. Retry: …` | A machine document id a node running a newer version registered, an id of a machine this node does not serve that the store holds active, or an id this node has not seen and has not read the store for since. Nothing runs; the client retries and may reach a node that runs it. HTTP status 409, not a 5xx: the client reads the code. | -| `PERSISTED_OPERATION_ID_MISMATCH` | `A request that names a persisted operation id may carry a document only when the id is that document's hash. …` | A request that sends both a persisted operation id and a document, where the id is not the document's own hash under the executor's hash algorithm. HTTP status 400. See [Persisted Operations](/docs/persisted-operations). | +| `PERSISTED_OPERATION_ID_MISMATCH` | `A request that names a persisted operation id may carry a document only when the id is that document's hash. …` | A request that sends both a persisted operation id and a document, where the id is not the document's own hash under the executor's hash algorithm. HTTP status 400. See [Persisted Operations](/docs/api/persisted-operations). | An operations mutation that the service refuses (an empty or oversized batch, a run that is not cancellable, a manifest external id that names no manifest, an acknowledgement note over 1,000 characters, a manifest update the scheduler could not run) is not an error: it returns `success: false` with a message in its payload. See [Mutations](/docs/sdk-reference/graphql-api/mutations). diff --git a/Trax.Docs/sdk-reference/graphql-api/machines.md b/Trax.Docs/sdk-reference/graphql-api/machines.md index 321710566..51beadc24 100644 --- a/Trax.Docs/sdk-reference/graphql-api/machines.md +++ b/Trax.Docs/sdk-reference/graphql-api/machines.md @@ -65,7 +65,7 @@ the server logs the failure under; the answer carries no exception text, SQL or ## Behavior -Exposing a machine requires [persisted operations](/docs/persisted-operations): each machine's operations are +Exposing a machine requires [persisted operations](/docs/api/persisted-operations): each machine's operations are served as [persisted documents](#persisted-documents) that carry its version, and a node orders a machine's versions across a rolling deploy through them. A host that exposes a machine without `UsePersistedOperations` refuses to start, naming the machines and the call to add. `SingleNode()` suits one process; more than one node uses diff --git a/Trax.Docs/sdk-reference/graphql-api/mutations.md b/Trax.Docs/sdk-reference/graphql-api/mutations.md index 37de745cb..48d2eaec5 100644 --- a/Trax.Docs/sdk-reference/graphql-api/mutations.md +++ b/Trax.Docs/sdk-reference/graphql-api/mutations.md @@ -48,7 +48,7 @@ through it, so an open namespace lets any caller change scheduled work. Mutation train and input the caller chose (`requeueExecution`, `resumeExecution`, `workQueue.queueTrain`) also apply that train's `[TraxAuthorize]` requirements. Mutations that enqueue what a manifest fixed (`triggerManifest`, `triggerManifestDelayed`, `triggerGroup`, dead-letter requeues) answer to the -gate alone. See [Authorization: The Operations Surface](/docs/authorization#the-operations-surface). +gate alone. See [Authorization: The Operations Surface](/docs/security/authorization-rules#the-operations-surface). The fields directly under `operations`, each on the page that documents it: diff --git a/Trax.Docs/sdk-reference/graphql-api/mutations/work-queue.md b/Trax.Docs/sdk-reference/graphql-api/mutations/work-queue.md index e5709604d..92b682aae 100644 --- a/Trax.Docs/sdk-reference/graphql-api/mutations/work-queue.md +++ b/Trax.Docs/sdk-reference/graphql-api/mutations/work-queue.md @@ -24,7 +24,7 @@ Creates a new work queue entry. The dispatcher picks it up on its next poll. An Upgrading from a version where this mutation wrote the row itself: it now authorizes, fires `OnQueue` and stamps the subject key. See [Enqueue and Outcome Changes](/docs/migration-guides/enqueue-and-outcome-changes). -The entry is created through [`ITrainExecutionService.QueueAsync`](/docs/sdk-reference/mediator-api/i-train-execution-service#queueasync), so the train's `[TraxAuthorize]` requirements apply on top of the operations gate. Authorization runs before `inputJson` is read: a caller who may not run the train gets a GraphQL error with code `TRAX_AUTHORIZATION` and message `"Not authorized."`, not `success: false`, even when the input is malformed, and nothing is inserted. See [Authorization: The Operations Surface](/docs/authorization#the-operations-surface). +The entry is created through [`ITrainExecutionService.QueueAsync`](/docs/sdk-reference/mediator-api/i-train-execution-service#queueasync), so the train's `[TraxAuthorize]` requirements apply on top of the operations gate. Authorization runs before `inputJson` is read: a caller who may not run the train gets a GraphQL error with code `TRAX_AUTHORIZATION` and message `"Not authorized."`, not `success: false`, even when the input is malformed, and nothing is inserted. See [Authorization: The Operations Surface](/docs/security/authorization-rules#the-operations-surface). Four kinds of exception propagate out of the mutation rather than becoming `success: false`. Authorization (an `UnauthorizedAccessException`, which `TrainAuthorizationException` is) surfaces as the `TRAX_AUTHORIZATION` error above. Cancellation of the request ends it. An infrastructure failure, meaning a database, EF Core, network, I/O or timeout exception anywhere in the exception's chain (a `DbException` such as `NpgsqlException`, `DbUpdateException`, `TimeoutException`, `SocketException`, `HttpRequestException` or `IOException`), is logged on the server and arrives as a GraphQL error with HotChocolate's masked `"Unexpected Execution Error"` message, so nothing about the server reaches the caller. That includes a data-layer exception caused by the train's own `OnQueue` hook; a hook that means to refuse throws its own exception. The mediator's `TrainAuthorizationNotConfiguredException`, for a `[TraxAuthorize]` train on a host with no `ITrainAuthorizationService` registered, is a host misconfiguration, so it is logged and masked the same way. diff --git a/Trax.Docs/sdk-reference/graphql-api/queries.md b/Trax.Docs/sdk-reference/graphql-api/queries.md index cbda4f8e0..7bcd744e0 100644 --- a/Trax.Docs/sdk-reference/graphql-api/queries.md +++ b/Trax.Docs/sdk-reference/graphql-api/queries.md @@ -169,7 +169,7 @@ list type, and an effect's settings are on `operations` namespace, so whatever gates it (`RequireAuthorization()` or `GateOperations(...)`, see [AddTraxGraphQL](/docs/sdk-reference/graphql-api/add-trax-graphql)) decides who reads them. There is no separate field-level gate: a caller who can read one execution's input can read a manifest's -properties too. [Authorization](/docs/authorization#the-operations-surface) explains the choice. +properties too. [Authorization](/docs/security/authorization-rules#the-operations-surface) explains the choice. A member marked [`[TraxSensitive]`](/docs/sdk-reference/configuration/save-train-parameters#masking-sensitive-fields) is masked in all three, written as `{"_redacted": true}`. An execution's `input` is recorded that diff --git a/Trax.Docs/sdk-reference/graphql-api/query-models.md b/Trax.Docs/sdk-reference/graphql-api/query-models.md index e2fac3eb6..cbbea0e06 100644 --- a/Trax.Docs/sdk-reference/graphql-api/query-models.md +++ b/Trax.Docs/sdk-reference/graphql-api/query-models.md @@ -125,7 +125,7 @@ what it is. Put `[TraxAuthorize]` or `[TraxAllowAnonymous]` on the resolver meth a method and Trax emits the matching directive. HotChocolate's `[Authorize]` and `[AllowAnonymous]` are refused in their place. A field on a `[TraxAuthorize]` entity inherits that gate and needs nothing. See -[Fields Added by a Type Extension](/docs/authorization#fields-added-by-a-type-extension). +[Fields Added by a Type Extension](/docs/security/authorization-rules#fields-added-by-a-type-extension). `ExtensionResolversDeclareParentRequirements` in `Trax.Api.GraphQL.Testing` fails the build when a resolver reads an undeclared property. See @@ -220,7 +220,7 @@ The configuration is validated at `Build()` time. Each failure mode throws `Inva ### Authorization -A `[TraxQueryModel]` entity is exposed via GraphQL, so it must declare its authorization posture explicitly: `[TraxAuthorize]` to gate it or `[TraxAllowAnonymous]` to open it. An entity with neither fails at `TraxGraphQLBuilder.Build()` (unless the endpoint is gated with `RequireAuthorization()`, which covers it). See [Authorization guide - Required Exposure Posture](/docs/authorization#required-exposure-posture). +A `[TraxQueryModel]` entity is exposed via GraphQL, so it must declare its authorization posture explicitly: `[TraxAuthorize]` to gate it or `[TraxAllowAnonymous]` to open it. An entity with neither fails at `TraxGraphQLBuilder.Build()` (unless the endpoint is gated with `RequireAuthorization()`, which covers it). See [Authorization guide - Required Exposure Posture](/docs/security/authorization-rules#required-exposure-posture). Apply `[TraxAuthorize]` to a `[TraxQueryModel]` entity to gate access. The directive attaches at GraphQL type level *and* at the entry field, so the gate enforces uniformly: @@ -236,11 +236,11 @@ public class Article { ... } Combinator semantics, exact case-sensitive role matching, and inheritance behavior match the per-train `[TraxAuthorize]` surface. Policy names referenced by a `[TraxQueryModel]` entity must be registered with `services.AddAuthorization(...)`; a `QueryModelAuthorizationValidator` hosted service throws at host start if any policy is missing. -The inverse opt-in, `[TraxAllowAnonymous]`, opens an entity to unauthenticated reads. It is mutually exclusive with `[TraxAuthorize]` and does not cascade through navigation properties to gated children. See [Authorization guide - Anonymous Access via TraxAllowAnonymous](/docs/authorization#anonymous-access-via-traxallowanonymous). +The inverse opt-in, `[TraxAllowAnonymous]`, opens an entity to unauthenticated reads. It is mutually exclusive with `[TraxAuthorize]` and does not cascade through navigation properties to gated children. See [Authorization guide - Anonymous Access via TraxAllowAnonymous](/docs/security/authorization-rules#anonymous-access-via-traxallowanonymous). -An entity a query model reaches through a navigation, that is not itself a `[TraxQueryModel]`, declares its posture the same way, with `[TraxAuthorize]` or `[TraxAllowAnonymous]` on its class; the host refuses to start naming the navigation when it does not, unless the endpoint is gated with `RequireAuthorization()`. A class EF Core maps as owned is part of its owner and needs no marker. See [Authorization guide - Entities a Query Model Reaches](/docs/authorization#entities-a-query-model-reaches). +An entity a query model reaches through a navigation, that is not itself a `[TraxQueryModel]`, declares its posture the same way, with `[TraxAuthorize]` or `[TraxAllowAnonymous]` on its class; the host refuses to start naming the navigation when it does not, unless the endpoint is gated with `RequireAuthorization()`. A class EF Core maps as owned is part of its owner and needs no marker. See [Authorization guide - Entities a Query Model Reaches](/docs/security/authorization-rules#entities-a-query-model-reaches). -See the [Authorization guide - Per-Model Authorization](/docs/authorization#per-model-authorization) for the full semantics table and limitations (a property cannot be gated on its own, though a resolver method can; no row-level filtering). +See the [Authorization guide - Per-Model Authorization](/docs/security/authorization-rules#per-model-authorization) for the full semantics table and limitations (a property cannot be gated on its own, though a resolver method can; no row-level filtering). ### Name derivation @@ -501,4 +501,4 @@ query { - [AddTraxGraphQL](/docs/sdk-reference/graphql-api/add-trax-graphql) - [ConfigureFiltering](/docs/sdk-reference/graphql-api/configure-filtering) - [Cross-schema data loaders](/docs/sdk-reference/graphql-api/cross-schema-data-loaders) -- [Authorization: Per-Model Authorization](/docs/authorization#per-model-authorization) +- [Authorization: Per-Model Authorization](/docs/security/authorization-rules#per-model-authorization) diff --git a/Trax.Docs/sdk-reference/graphql-api/trax-graphql-attribute.md b/Trax.Docs/sdk-reference/graphql-api/trax-graphql-attribute.md index ba14f5555..59bf22210 100644 --- a/Trax.Docs/sdk-reference/graphql-api/trax-graphql-attribute.md +++ b/Trax.Docs/sdk-reference/graphql-api/trax-graphql-attribute.md @@ -95,7 +95,7 @@ The mutation's `externalId` is the external id the dispatched run carries, so a The train must declare its authorization posture: `[TraxAuthorize]` to gate it or `[TraxAllowAnonymous]` to open it. A train with neither fails at host startup (`AddTraxGraphQL`), unless the endpoint is gated with `RequireAuthorization()`. See -[Authorization: Required Exposure Posture](/docs/authorization#required-exposure-posture). +[Authorization: Required Exposure Posture](/docs/security/authorization-rules#required-exposure-posture). ### Input type requirements diff --git a/Trax.Docs/sdk-reference/graphql-client.md b/Trax.Docs/sdk-reference/graphql-client.md index 00d6e709b..b41f730f8 100644 --- a/Trax.Docs/sdk-reference/graphql-client.md +++ b/Trax.Docs/sdk-reference/graphql-client.md @@ -12,7 +12,7 @@ nav_order: 12 The outbound client validates hand-written GraphQL queries against a server's schema and runs them. It ships in three packages. For the concepts (query modes, schema sources, keyed clients), see -[GraphQL Client](/docs/api-graphql-client). +[GraphQL Client](/docs/api/call-another-graphql-server). ```csharp compile services diff --git a/Trax.Docs/sdk-reference/graphql-client/add-keyed-trax-graphql-client.md b/Trax.Docs/sdk-reference/graphql-client/add-keyed-trax-graphql-client.md index 37efb643d..b482db99c 100644 --- a/Trax.Docs/sdk-reference/graphql-client/add-keyed-trax-graphql-client.md +++ b/Trax.Docs/sdk-reference/graphql-client/add-keyed-trax-graphql-client.md @@ -85,4 +85,4 @@ public class OrderSync( - [AddTraxGraphQLClient](/docs/sdk-reference/graphql-client/add-trax-graphql-client) - [GraphQLClientAttribute](/docs/sdk-reference/graphql-client/graphql-client-attribute) -- [GraphQL Client](/docs/api-graphql-client) +- [GraphQL Client](/docs/api/call-another-graphql-server) diff --git a/Trax.Docs/sdk-reference/graphql-client/add-trax-graphql-client.md b/Trax.Docs/sdk-reference/graphql-client/add-trax-graphql-client.md index 8f762f07f..bdfc36365 100644 --- a/Trax.Docs/sdk-reference/graphql-client/add-trax-graphql-client.md +++ b/Trax.Docs/sdk-reference/graphql-client/add-trax-graphql-client.md @@ -83,4 +83,4 @@ public class PlayerLookup(IGraphQLClientExecutor graphql) - [AddKeyedTraxGraphQLClient](/docs/sdk-reference/graphql-client/add-keyed-trax-graphql-client) - [TraxGraphQLClientBuilder](/docs/sdk-reference/graphql-client/trax-graphql-client-builder) - [IGraphQLClientExecutor](/docs/sdk-reference/graphql-client/i-graphql-client-executor) -- [GraphQL Client](/docs/api-graphql-client) +- [GraphQL Client](/docs/api/call-another-graphql-server) diff --git a/Trax.Docs/sdk-reference/graphql-client/typed-request.md b/Trax.Docs/sdk-reference/graphql-client/typed-request.md index 012ed2b77..fa788ad0b 100644 --- a/Trax.Docs/sdk-reference/graphql-client/typed-request.md +++ b/Trax.Docs/sdk-reference/graphql-client/typed-request.md @@ -127,4 +127,4 @@ query GetPlayer($id: String!) { - [IGraphQLClientRequest](/docs/sdk-reference/graphql-client/i-graphql-client-request) - [IGraphQLClientExecutor](/docs/sdk-reference/graphql-client/i-graphql-client-executor) -- [GraphQL Client](/docs/api-graphql-client) +- [GraphQL Client](/docs/api/call-another-graphql-server) diff --git a/Trax.Docs/sdk-reference/mediator-api/i-trusted-execution-scope.md b/Trax.Docs/sdk-reference/mediator-api/i-trusted-execution-scope.md index 1c050b389..b46396cd3 100644 --- a/Trax.Docs/sdk-reference/mediator-api/i-trusted-execution-scope.md +++ b/Trax.Docs/sdk-reference/mediator-api/i-trusted-execution-scope.md @@ -13,7 +13,7 @@ source: - Trax.Mediator/src/Trax.Mediator/Services/TrustedExecution/TrustedExecutionScope.cs --- -> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/api-security). +> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See [API Security](/docs/security/secure-a-graphql-host). # ITrustedExecutionScope @@ -105,6 +105,6 @@ public class NightlyImportJob(ITrustedExecutionScope trust, ITrainExecutionServi ## See also -- [Authorization: Fail-Closed Behavior](/docs/authorization#fail-closed-behavior) +- [Authorization: Fail-Closed Behavior](/docs/security/authorization-rules#fail-closed-behavior) - [ITrainExecutionService](/docs/sdk-reference/mediator-api/i-train-execution-service) - [`[TraxAuthorize]`](/docs/sdk-reference/attributes/trax-authorize) diff --git a/Trax.Docs/sdk-reference/persisted-operations.md b/Trax.Docs/sdk-reference/persisted-operations.md index 53884e390..5d412842a 100644 --- a/Trax.Docs/sdk-reference/persisted-operations.md +++ b/Trax.Docs/sdk-reference/persisted-operations.md @@ -11,7 +11,7 @@ nav_order: 11 # Persisted Operations Reference for `Trax.Api.GraphQL.PersistedOperations`. For what the feature does and how to roll it -out, see [Persisted Operations](/docs/persisted-operations). +out, see [Persisted Operations](/docs/api/persisted-operations). ## Registration diff --git a/Trax.Docs/sdk-reference/persisted-operations/add-persisted-operation-store.md b/Trax.Docs/sdk-reference/persisted-operations/add-persisted-operation-store.md index dcad71486..8e17072d9 100644 --- a/Trax.Docs/sdk-reference/persisted-operations/add-persisted-operation-store.md +++ b/Trax.Docs/sdk-reference/persisted-operations/add-persisted-operation-store.md @@ -108,4 +108,4 @@ services.AddPersistedOperationStore(store => - [IPersistedOperationStore](/docs/sdk-reference/persisted-operations/i-persisted-operation-store) - [IPersistedOperationsService](/docs/sdk-reference/persisted-operations/i-persisted-operations-service) -- [One node or many](/docs/persisted-operations#one-node-or-many) +- [One node or many](/docs/api/persisted-operations#one-node-or-many) diff --git a/Trax.Docs/sdk-reference/persisted-operations/i-persisted-operation-broadcaster.md b/Trax.Docs/sdk-reference/persisted-operations/i-persisted-operation-broadcaster.md index 97c9db317..65a2d2391 100644 --- a/Trax.Docs/sdk-reference/persisted-operations/i-persisted-operation-broadcaster.md +++ b/Trax.Docs/sdk-reference/persisted-operations/i-persisted-operation-broadcaster.md @@ -75,4 +75,4 @@ await serviceProvider.GetRequiredService().Publi - [PersistedOperationsBuilder](/docs/sdk-reference/persisted-operations/persisted-operations-builder) - [PersistedOperationException](/docs/sdk-reference/persisted-operations/persisted-operation-exceptions) -- [One node or many](/docs/persisted-operations#one-node-or-many) +- [One node or many](/docs/api/persisted-operations#one-node-or-many) diff --git a/Trax.Docs/sdk-reference/persisted-operations/persisted-operation-exceptions.md b/Trax.Docs/sdk-reference/persisted-operations/persisted-operation-exceptions.md index 0de2210e9..18309492c 100644 --- a/Trax.Docs/sdk-reference/persisted-operations/persisted-operation-exceptions.md +++ b/Trax.Docs/sdk-reference/persisted-operations/persisted-operation-exceptions.md @@ -110,7 +110,7 @@ Two refusals happen when a request is executed, not when an operation is stored, | `extensions.code` | When | |---|---| | `PERSISTED_OPERATION_REQUIRED` | Enforcement refuses an inline document. See [UsePersistedOperationsEnforcement](/docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement). | -| `PERSISTED_OPERATION_ID_MISMATCH` | The request sends a persisted operation id and a document, and the id is not the document's own hash under the executor's hash algorithm. Send the id alone to run the stored operation, or the document alone to run it inline. See [Persisted Operations](/docs/persisted-operations). | +| `PERSISTED_OPERATION_ID_MISMATCH` | The request sends a persisted operation id and a document, and the id is not the document's own hash under the executor's hash algorithm. Send the id alone to run the stored operation, or the document alone to run it inline. See [Persisted Operations](/docs/api/persisted-operations). | Every code the endpoint returns is listed in [Error Codes](/docs/sdk-reference/graphql-api/error-codes). diff --git a/Trax.Docs/sdk-reference/persisted-operations/persisted-operations-builder.md b/Trax.Docs/sdk-reference/persisted-operations/persisted-operations-builder.md index 01f178dc5..029bc0bf5 100644 --- a/Trax.Docs/sdk-reference/persisted-operations/persisted-operations-builder.md +++ b/Trax.Docs/sdk-reference/persisted-operations/persisted-operations-builder.md @@ -80,12 +80,12 @@ public sealed class CacheOptions ## Behavior -- Exactly one of `SingleNode()` and `UseRabbitMqInvalidation(...)` is required. See [One node or many](/docs/persisted-operations#one-node-or-many). +- Exactly one of `SingleNode()` and `UseRabbitMqInvalidation(...)` is required. See [One node or many](/docs/api/persisted-operations#one-node-or-many). - `GateManagement` throws `InvalidOperationException` with neither a policy nor roles, or with a role list that holds only separators. A policy it names that is not registered with `AddAuthorization` fails startup. - `ClientDocuments` throws `ArgumentException` for a blank path. - `UseRabbitMqInvalidation` throws `ArgumentException` for a blank connection string; `WithCacheMaxAge` and `KeepOlderMachineDocumentsFor` throw `ArgumentOutOfRangeException` for a value that is not positive; a second `WithInMemoryCache` throws `InvalidOperationException`. - The remaining checks run when `UsePersistedOperations` builds the options and throw `InvalidOperationException`; the [UsePersistedOperations validation table](/docs/sdk-reference/persisted-operations/use-persisted-operations#validation) lists them. -- What the allowlist does and does not protect: [Allowlist and dev carve-outs](/docs/persisted-operations#allowlist-and-dev-carve-outs). +- What the allowlist does and does not protect: [Allowlist and dev carve-outs](/docs/api/persisted-operations#allowlist-and-dev-carve-outs). ## Example @@ -108,4 +108,4 @@ services.AddTraxGraphQL(graphql => graphql - [UsePersistedOperations](/docs/sdk-reference/persisted-operations/use-persisted-operations) - [PersistedOperationsOptions](/docs/sdk-reference/persisted-operations/persisted-operations-options) -- [Persisted Operations](/docs/persisted-operations) +- [Persisted Operations](/docs/api/persisted-operations) diff --git a/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement.md b/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement.md index a3e4db7b8..a0ac7fec5 100644 --- a/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement.md +++ b/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement.md @@ -46,7 +46,7 @@ For each operation: 1. If the document came from the store (the request named a persisted id): execute it. 2. If the request carries HotChocolate's `AllowNonPersistedOperation()` override, which only host code building its own request can set: execute it. 3. Otherwise the document is inline: - - **Allowlist match** (operation name in `AllowOperations` or matching a predicate; the document id when there is no name): execute it. Both keys are chosen by the caller and the document is not inspected, so the allowlist is a convenience for trusted networks, not a control. See [Allowlist and dev carve-outs](/docs/persisted-operations#allowlist-and-dev-carve-outs). + - **Allowlist match** (operation name in `AllowOperations` or matching a predicate; the document id when there is no name): execute it. Both keys are chosen by the caller and the document is not inspected, so the allowlist is a convenience for trusted networks, not a control. See [Allowlist and dev carve-outs](/docs/api/persisted-operations#allowlist-and-dev-carve-outs). - **Management surface** (every operation selects `persistedOperations { ... }` at the root and nothing else): execute it. Authorization still applies. - **Introspection** (the parsed document's top-level selections are all `__schema`, `__type` or `__typename`): execute it unless `DisableIntrospection()` was called. - **Enforcement off** (`RequirePersisted(false)`): execute it, logging at Information when `LogNonPersistedRequests(true)`. diff --git a/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations.md b/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations.md index eb7b81364..7150d61bf 100644 --- a/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations.md +++ b/Trax.Docs/sdk-reference/persisted-operations/use-persisted-operations.md @@ -39,7 +39,7 @@ The same `TraxGraphQLBuilder`. ## Behavior Exactly one of `SingleNode()` and `UseRabbitMqInvalidation(...)` is required; see -[One node or many](/docs/persisted-operations#one-node-or-many). The options are checked when +[One node or many](/docs/api/persisted-operations#one-node-or-many). The options are checked when `UsePersistedOperations` runs (see [Validation](#validation)). ### Generated documents @@ -165,4 +165,4 @@ services.AddTraxGraphQL(graphql => graphql - [UsePersistedOperationsEnforcement](/docs/sdk-reference/persisted-operations/use-persisted-operations-enforcement), for the decision each request goes through - [Management mutations and queries](/docs/sdk-reference/persisted-operations/management-mutations) - [AddPersistedOperationStore](/docs/sdk-reference/persisted-operations/add-persisted-operation-store), for a host with no GraphQL server -- [Persisted Operations](/docs/persisted-operations) +- [Persisted Operations](/docs/api/persisted-operations) diff --git a/Trax.Docs/sdk-reference/scheduler-api/add-trax-worker.md b/Trax.Docs/sdk-reference/scheduler-api/add-trax-worker.md index 07b9fb66b..4421b5a17 100644 --- a/Trax.Docs/sdk-reference/scheduler-api/add-trax-worker.md +++ b/Trax.Docs/sdk-reference/scheduler-api/add-trax-worker.md @@ -135,7 +135,7 @@ builder.Services.AddTrax(trax => trax builder.Services.AddTraxWorker(); ``` -See [Authorization: Opting Out for Scheduler-Only Hosts](/docs/authorization#opting-out-for-scheduler-only-hosts). The [Energy Hub sample](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples/samples/DistributedWorkers) runs this way. +See [Authorization: Opting Out for Scheduler-Only Hosts](/docs/security/authorization-rules#opting-out-for-scheduler-only-hosts). The [Energy Hub sample](https://github.com/TraxSharp/Trax/tree/main/Trax.Samples/samples/DistributedWorkers) runs this way. Worker options: diff --git a/Trax.Docs/sdk-reference/scheduler-api/i-operations-service.md b/Trax.Docs/sdk-reference/scheduler-api/i-operations-service.md index 0e8636aef..f781880c9 100644 --- a/Trax.Docs/sdk-reference/scheduler-api/i-operations-service.md +++ b/Trax.Docs/sdk-reference/scheduler-api/i-operations-service.md @@ -845,7 +845,7 @@ since a machine's definition does not change while the host runs. It reads no ta ### Authorization -Neither method decides authorization itself: both leave it to the mediator, `QueueTrainAsync` and `RequeueExecutionAsync` through `QueueAsync`, and `RunTrainAsync` through `PrepareAsync`. An `ITrainAuthorizationService` decides when one is registered (Trax.Api registers one). Without one, a call inside a trusted scope passes, and a `[TraxAuthorize]` train is refused unless the host called `AllowMissingAuthorizationService()`. The dashboard calls both inside the `"dashboard"` trusted scope, because it is gated as a whole by its host (see [Authorization: The Operations Surface](/docs/authorization#the-operations-surface)). +Neither method decides authorization itself: both leave it to the mediator, `QueueTrainAsync` and `RequeueExecutionAsync` through `QueueAsync`, and `RunTrainAsync` through `PrepareAsync`. An `ITrainAuthorizationService` decides when one is registered (Trax.Api registers one). Without one, a call inside a trusted scope passes, and a `[TraxAuthorize]` train is refused unless the host called `AllowMissingAuthorizationService()`. The dashboard calls both inside the `"dashboard"` trusted scope, because it is gated as a whole by its host (see [Authorization: The Operations Surface](/docs/security/authorization-rules#the-operations-surface)). ### Effects and log levels @@ -881,4 +881,4 @@ public class ReportQueue(IOperationsService operations) - [ITraxScheduler](/docs/sdk-reference/scheduler-api/i-trax-scheduler), the scheduler API it builds on - [Dashboard](/docs/dashboard) and the GraphQL [mutations](/docs/sdk-reference/graphql-api/mutations), its two callers -- [Authorization: The Operations Surface](/docs/authorization#the-operations-surface) +- [Authorization: The Operations Surface](/docs/security/authorization-rules#the-operations-surface) diff --git a/Trax.Docs/sdk-reference/train-methods/short-circuit.md b/Trax.Docs/sdk-reference/train-methods/short-circuit.md index 0163d7fe7..ef9511a2c 100644 --- a/Trax.Docs/sdk-reference/train-methods/short-circuit.md +++ b/Trax.Docs/sdk-reference/train-methods/short-circuit.md @@ -50,6 +50,37 @@ protected MonadTask ShortCircuit(TJunction instance) The steps after a `ShortCircuit` still run, whether or not it succeeded: the short-circuit value changes only what `Resolve()` returns. A later step that fails puts the train on the failure track, and `Resolve()` then returns that failure rather than the short-circuit value. +This test, from Trax.Core's suite, puts a cache in front of a price lookup. On a hit the cached price is the result and the lookup still runs; on a miss the cache's exception is dropped: + +```csharp from=Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Caching/CachedPriceTests.cs +public class PriceTrain(IPriceCache cache, IAudit audit) : Train +{ + protected override Task> Junctions() => + AddServices(cache, audit).ShortCircuit().Chain().Resolve(); +} +// ... + [Test] + public async Task Run_CacheHit_ReturnsTheCachedPriceButStillRunsTheLookUp() + { + var cache = new PriceCache(); + cache.Prices["tea"] = 4m; + var audit = new Audit(); + + var price = await new PriceTrain(cache, audit).Run(new Product("tea")); + + price.Should().Be(new Price(4m)); + audit.Lines.Should().Equal("looked up tea"); + } + + [Test] + public async Task Run_CacheMiss_IgnoresTheFailureAndReturnsTheLookUp() + { + var price = await new PriceTrain(new PriceCache(), new Audit()).Run(new Product("tea")); + + price.Should().Be(new Price(10m)); + } +``` + | Condition | Result | |-----------|--------| | The junction's output cannot be `TReturn` | Refused by the startup chain check | diff --git a/Trax.Docs/sdk-reference/trains-and-junctions/train.md b/Trax.Docs/sdk-reference/trains-and-junctions/train.md index 29d1905b3..1b465ee41 100644 --- a/Trax.Docs/sdk-reference/trains-and-junctions/train.md +++ b/Trax.Docs/sdk-reference/trains-and-junctions/train.md @@ -88,6 +88,34 @@ public abstract class Train : IRoute - **Nothing escapes `RunEither`.** A junction's exception, including cancellation, comes back as `Left`, and so does an exception `Junctions()` itself throws. - **Overriding `Run`.** `Run` is virtual so code built on Trax.Core alone can wrap it. An override that does not call `base.Run` never reaches `Junctions()`, so `DeclaredChain()`, and any check built on it, does not describe what runs (Trax.Core ADR 0003). `ServiceTrain` seals it. +A checkout whose card is always declined, from Trax.Core's tests. `RunEither` hands back the junction's own exception rather than throwing it: + +```csharp from=Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/Checkout.cs +public class ChargeCard : Junction +{ + public override Task Run(Order order) => + throw new InvalidOperationException("Card declined"); +} +// ... +public class CheckoutTrain(IOutbox outbox) : Train +{ + protected override Task> Junctions() => + AddServices(outbox).Chain().Chain().Resolve(); +} +``` + +```csharp from=Trax.Core/tests/Trax.Core.Tests.Integration/Examples/Failures/RunEitherTests.cs + [Test] + public async Task RunEither_ChargeCardThrows_ReturnsTheExceptionInsteadOfThrowing() + { + var result = await new CheckoutTrain(new Outbox()).RunEither(new Order(20m)); + + result.IsLeft.Should().BeTrue(); + var message = result.Match(Right: receipt => "", Left: failure => failure.Message); + message.Should().Be("Card declined"); + } +``` + ## Example ```csharp compile=score diff --git a/Trax.Docs/authorization.md b/Trax.Docs/security/authorization-rules.md similarity index 99% rename from Trax.Docs/authorization.md rename to Trax.Docs/security/authorization-rules.md index e2b9288d1..de9bc72f1 100644 --- a/Trax.Docs/authorization.md +++ b/Trax.Docs/security/authorization-rules.md @@ -13,11 +13,13 @@ source: - Trax.Api/src/Trax.Api.GraphQL/Configuration/TypeExtensionExposureRule.cs - Trax.Api/src/Trax.Api.GraphQL/Authorization/TraxHttpAuthenticationInterceptor.cs - Trax.Mediator/src/Trax.Mediator/Services/TrainAuthorization/ITrainAuthorizationService.cs +redirect_from: + - /docs/authorization --- # Authorization rules -> **Security notice.** NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [Secure a GraphQL host](/docs/api-security). +> **Security notice.** NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See [Secure a GraphQL host](/docs/security/secure-a-graphql-host). Every surface the Trax GraphQL endpoint exposes declares who may reach it, and a host whose declarations are missing or contradictory does not start. This page lists the rules. Why they are @@ -474,7 +476,7 @@ the union of every attribute's roles; it does not say which attributes they came - [Where Trax checks authorization](/docs/security/where-authorization-is-checked) - [Startup refusals](/docs/security/startup-refusals) -- [Secure a GraphQL host](/docs/api-security) +- [Secure a GraphQL host](/docs/security/secure-a-graphql-host) - [TraxAuthorize](/docs/sdk-reference/attributes/trax-authorize), [TraxAllowAnonymous](/docs/sdk-reference/attributes/trax-allow-anonymous), [ITrustedExecutionScope](/docs/sdk-reference/mediator-api/i-trusted-execution-scope), diff --git a/Trax.Docs/api-security.md b/Trax.Docs/security/secure-a-graphql-host.md similarity index 98% rename from Trax.Docs/api-security.md rename to Trax.Docs/security/secure-a-graphql-host.md index edd401564..0e3f2a9a5 100644 --- a/Trax.Docs/api-security.md +++ b/Trax.Docs/security/secure-a-graphql-host.md @@ -4,6 +4,8 @@ description: "Build a Trax GraphQL host with API-key and JWT authentication, gat type: how-to area: security nav_order: 1 +redirect_from: + - /docs/api-security --- # Secure a GraphQL host @@ -19,7 +21,7 @@ sample's E2E suite proves each gate. The defaults are closed. Until a surface declares who may reach it, the host does not start, and a caller with no credential reaches nothing but what you mark public. The rules are on -[Authorization rules](/docs/authorization) and the reasoning on +[Authorization rules](/docs/security/authorization-rules) and the reasoning on [Where Trax checks authorization](/docs/security/where-authorization-is-checked). Packages: `Trax.Effect.Data.Postgres`, `Trax.Effect.Provider.Json`, `Trax.Mediator`, @@ -101,7 +103,7 @@ What each part does, and where it can differ: browser sign-in, [AddTraxOidcAuth](/docs/sdk-reference/api-auth/add-trax-oidc-auth). Every `AddTrax*Auth` call adds its scheme to one combined policy and none becomes the default scheme; Trax tries each registered scheme on a GraphQL request - ([how](/docs/authorization#which-scheme-authenticates-a-graphql-request)). + ([how](/docs/security/authorization-rules#which-scheme-authenticates-a-graphql-request)). - **`AddAuthentication()` and `AddTraxPrincipalAccessor()`.** Needed only because the auth calls are conditional. With neither branch configured, the host still starts and every gated surface refuses every caller. @@ -156,7 +158,7 @@ entity it reaches through a navigation; its context goes on the builder with `.AddDbContext()`. `[TraxAuthorize]` decides who may read the type, not which rows: to scope the rows a caller sees, add a query filter that reads the caller (see [Owner-scoped contexts](/docs/effect/effect-providers/domain-data-contexts#owner-scoped-contexts)). -The rules for each kind of surface are on [Authorization rules](/docs/authorization). +The rules for each kind of surface are on [Authorization rules](/docs/security/authorization-rules). ## 3. Write the audit sink @@ -358,7 +360,7 @@ Trax does none of these for you: ## See also -- [Authorization rules](/docs/authorization) +- [Authorization rules](/docs/security/authorization-rules) - [Where Trax checks authorization](/docs/security/where-authorization-is-checked) - [Startup refusals](/docs/security/startup-refusals) - [Injecting TraxPrincipal](/docs/security/inject-the-current-principal): reading the diff --git a/Trax.Docs/security/startup-refusals.md b/Trax.Docs/security/startup-refusals.md index 0a7152c7c..306c2e7fb 100644 --- a/Trax.Docs/security/startup-refusals.md +++ b/Trax.Docs/security/startup-refusals.md @@ -37,7 +37,7 @@ A refusal is raised at one of these points: A surface is exposed when it is a `[TraxQuery]` or `[TraxMutation]` train, a `[TraxBroadcast]` train, a `[TraxQueryModel]` entity, an entity a query model reaches, or a field a type extension -adds. The rule behind these refusals is [Required exposure posture](/docs/authorization#required-exposure-posture). +adds. The rule behind these refusals is [Required exposure posture](/docs/security/authorization-rules#required-exposure-posture). | Cause | Raised | Message begins | Answer | |---|---|---|---| @@ -48,7 +48,7 @@ adds. The rule behind these refusals is [Required exposure posture](/docs/author | `[TraxAuthorize]` with an empty or whitespace `Policy`, or a `Roles` list that holds no role, on a query model | `AddTraxGraphQL` | `[TraxAuthorize] on '' has an empty or whitespace Policy value.` or `[TraxAuthorize(Roles="...")] on '' parsed to zero roles ...` | Name a policy or a role, or drop the argument | | The same, on a train or one of its interfaces | Host start | As above | As above | | An entity a query model reaches (a navigation, or a method returning an entity) that is not a query model and declares neither marker, on an endpoint without `RequireAuthorization()` | Host start | ` entit(y/ies) reached from a [TraxQueryModel] declare no usable authorization posture ...`, naming every schema coordinate that reaches it | A marker on the entity class, the navigation left out of the model's field set (`BindFields = Explicit` or `ExposeAs`), or an endpoint gate | -| A field a type extension adds to a parent that gives it no gate (a `[TraxAllowAnonymous]` type, a root type, a namespace reached from the root, the `operations` namespace under `AllowAnonymousOperations()`), with no marker of its own | Host start | ` GraphQL field(s) or type extension(s) cannot be served as declared:` | `[TraxAuthorize]` or `[TraxAllowAnonymous]` on the resolver method or the extension class. See [Fields added by a type extension](/docs/authorization#fields-added-by-a-type-extension) | +| A field a type extension adds to a parent that gives it no gate (a `[TraxAllowAnonymous]` type, a root type, a namespace reached from the root, the `operations` namespace under `AllowAnonymousOperations()`), with no marker of its own | Host start | ` GraphQL field(s) or type extension(s) cannot be served as declared:` | `[TraxAuthorize]` or `[TraxAllowAnonymous]` on the resolver method or the extension class. See [Fields added by a type extension](/docs/security/authorization-rules#fields-added-by-a-type-extension) | | Both markers on such a field | Host start | As above | Remove one | | HotChocolate's `[Authorize]` or `[AllowAnonymous]` on a field a type extension adds, or on the extension class | Host start | As above, then `GraphQL field '' () declares its authorization posture with [Authorize], which Trax does not read. ...` | `[TraxAuthorize]` or `[TraxAllowAnonymous]` | | The `@authorize` directive Trax put on a gated query model, its entry field or a gated reached entity is missing from the built schema, or one is present on a `[TraxAllowAnonymous]` entity | Host start | `... ConfigureSchema callback has ...` | Remove the `ConfigureSchema` override that replaced the type or directive | @@ -82,7 +82,7 @@ Roles are never checked at startup: Trax has no list of the roles that can exist | Cause | Raised | Message begins | Answer | |---|---|---|---| -| A registered train carries `[TraxAuthorize]` and no `ITrainAuthorizationService` is registered | Host start | `One or more registered trains declare [TraxAuthorize] but no ITrainAuthorizationService is registered in the DI container. ...`, ending with the trains | `AddTraxGraphQL()` or `AddTraxApi()` registers the default; a host that serves no submissions calls [`AllowMissingAuthorizationService()`](/docs/authorization#opting-out-for-scheduler-only-hosts) | +| A registered train carries `[TraxAuthorize]` and no `ITrainAuthorizationService` is registered | Host start | `One or more registered trains declare [TraxAuthorize] but no ITrainAuthorizationService is registered in the DI container. ...`, ending with the trains | `AddTraxGraphQL()` or `AddTraxApi()` registers the default; a host that serves no submissions calls [`AllowMissingAuthorizationService()`](/docs/security/authorization-rules#opting-out-for-scheduler-only-hosts) | | A junction takes `TraxPrincipal` in its constructor and nothing registers it (every `AddTrax*Auth` call was skipped) | Host start | ` of registered trains cannot run:`, naming the junction and `Trax.Api.Auth.TraxPrincipal` | `services.AddTraxPrincipalAccessor()`. See [Injecting TraxPrincipal](/docs/security/inject-the-current-principal) | ## Credentials @@ -130,7 +130,7 @@ A remote worker's entry points refuse to start without an authorization posture ## See also -- [Authorization rules](/docs/authorization): the rules these refusals enforce. +- [Authorization rules](/docs/security/authorization-rules): the rules these refusals enforce. - [Where Trax checks authorization](/docs/security/where-authorization-is-checked): why the host refuses rather than serves open. -- [Secure a GraphQL host](/docs/api-security): a host that passes every check. +- [Secure a GraphQL host](/docs/security/secure-a-graphql-host): a host that passes every check. diff --git a/Trax.Docs/supply-chain-security.md b/Trax.Docs/security/supply-chain.md similarity index 99% rename from Trax.Docs/supply-chain-security.md rename to Trax.Docs/security/supply-chain.md index e934cbb31..9454ba905 100644 --- a/Trax.Docs/supply-chain-security.md +++ b/Trax.Docs/security/supply-chain.md @@ -2,8 +2,10 @@ title: Supply Chain Security description: "How every Trax package gets from source to nuget.org and npm: pinned dependencies, split release jobs, keyless publishing, provenance and gated releases." type: explanation -area: general +area: security nav_order: 4 +redirect_from: + - /docs/supply-chain-security --- # Supply Chain Security diff --git a/Trax.Docs/security/where-authorization-is-checked.md b/Trax.Docs/security/where-authorization-is-checked.md index 0788763c4..3cecfecc4 100644 --- a/Trax.Docs/security/where-authorization-is-checked.md +++ b/Trax.Docs/security/where-authorization-is-checked.md @@ -9,7 +9,7 @@ nav_order: 1 # Where Trax checks authorization Trax authorizes GraphQL work in a few places and deliberately not in others. The rules themselves -are on [Authorization rules](/docs/authorization); this page is about why they are shaped the way +are on [Authorization rules](/docs/security/authorization-rules); this page is about why they are shaped the way they are, and what each choice costs. ## Nothing is public by accident diff --git a/Trax.Docs/statemachine.md b/Trax.Docs/statemachine.md index a27cb5f30..1294b8c5c 100644 --- a/Trax.Docs/statemachine.md +++ b/Trax.Docs/statemachine.md @@ -18,6 +18,8 @@ can be stored in Postgres and rebuilt from any point, so a user can start on one another. Illegal (state, data) combinations are made unrepresentable, and neither the API nor the client can produce an unhandled exception. +Whether a piece of work wants a machine or a train is on [Train or machine](/docs/statemachine/train-or-machine). + To build one, follow [Author and serve a machine](/docs/statemachine/authoring). Guards and reducers written as data are covered in [Write guards and reducers as data](/docs/statemachine/declarative-authoring), and a state that runs a train in [Invoke a train from a state](/docs/statemachine/invoking-trains). A state can also start another machine and wait for diff --git a/Trax.Docs/statemachine/authoring.md b/Trax.Docs/statemachine/authoring.md index 2c3159d6c..63c2c6dc9 100644 --- a/Trax.Docs/statemachine/authoring.md +++ b/Trax.Docs/statemachine/authoring.md @@ -170,7 +170,7 @@ public sealed class TraxCallerSnapshotPrincipal(TraxCaller caller) : ISnapshotPr ``` A request with no user key is refused as `UNAUTHENTICATED`. Authentication itself is the host's: see -[Secure a GraphQL host](/docs/api-security). +[Secure a GraphQL host](/docs/security/secure-a-graphql-host). ## 4. Register the machine in the host @@ -201,7 +201,7 @@ builder.Services.AddTraxGraphQL(graphql => ``` `SingleNode()` suits one process; more than one node uses `UseRabbitMqInvalidation(connectionString)`, as -[Turn on persisted operations](/docs/persisted-operations) describes. Host options (draft expiry, the per-user draft +[Turn on persisted operations](/docs/api/persisted-operations) describes. Host options (draft expiry, the per-user draft cap, the change feed) are under [AddStateMachines](/docs/sdk-reference/statemachine-api/add-state-machines#options). The rules for which registered machines are served, and the refusals for a marked subclass or a duplicate id, are under [Serving a machine](/docs/sdk-reference/statemachine-api/add-state-machines#serving-a-machine). diff --git a/Trax.Docs/statemachine/train-or-machine.md b/Trax.Docs/statemachine/train-or-machine.md new file mode 100644 index 000000000..78332a2f0 --- /dev/null +++ b/Trax.Docs/statemachine/train-or-machine.md @@ -0,0 +1,55 @@ +--- +title: Train or machine +description: "When to write a train and when to write a state machine: one operation that runs to its end, or a long-lived process a user moves through, and how they combine." +type: explanation +area: machines +nav_order: 1 +--- + +# Train or machine + +Trax has two ways to describe work, and they answer different questions. + +A **train** is one operation that runs from start to end: place an order, sync a supplier, send a +report. Someone (a caller, the scheduler, a worker) starts it, its junctions run in order, and a +few milliseconds or minutes later it has finished, with a result or an exception and a run record +saying which. Nothing about it waits for a person. + +A **machine** is a thing that exists for a while and changes when someone acts on it: a checkout, +an application form, an onboarding. It sits in a named state (`Cart`, then `Placing`, then `Placed`) for as +long as it takes, hours or weeks. Each action a user takes is an event the server applies or +refuses, and the state and the data that goes with it are stored after every move, so the user can +leave and come back on another device. The browser runs the same machine in TypeScript, so it knows +before asking which moves are allowed. + +| | Train | Machine | +|---|---|---| +| Lives for | one run | as long as the process it models | +| Moves when | its previous junction finishes | someone sends it an event, or a timer fires | +| Stored as | a run record, after it ends | a snapshot, after every move | +| Fails by | throwing, which stops the rest of the chain | refusing an event, which leaves it where it was | +| Started by | your code, GraphQL, the scheduler, a worker, a machine | a client over GraphQL, or a parent machine | + +## Use a train when + +- the work has a clear start and end, and nobody waits in the middle; +- it should be scheduled, retried, queued or moved to a worker; +- the question is "did it work, and if not, which step failed". + +## Use a machine when + +- a person, or another system, takes several separate actions over time; +- some actions are only allowed in some states, and the browser should know which; +- the question is "where is this one right now, and what can happen next". + +## Both + +Most applications need both, and they meet in one place: a state can [invoke a +train](/docs/statemachine/invoking-trains). The machine decides *when* the work happens, the train +does it, and the train's result moves the machine on. In the book's shop, checkout is a machine, +and when it reaches `Placing` it runs the `PlaceOrder` train and moves to `Placed` or to `Failed` +with the outcome ([chapter 13](/docs/book/the-machine-places-the-order)). + +A long train is not a reason to use a machine. A train that does a lot of work can save +[checkpoints](/docs/core/checkpoints-and-branches) so a failed run resumes after the last one. A +machine is for work that waits on someone. diff --git a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api-graphql-client.cs b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api/call-another-graphql-server.cs similarity index 63% rename from Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api-graphql-client.cs rename to Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api/call-another-graphql-server.cs index 91fcd7480..dd6c1755a 100644 --- a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api-graphql-client.cs +++ b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api/call-another-graphql-server.cs @@ -1,4 +1,4 @@ -// What the snippets on api-graphql-client.md assume and do not show. +// What the snippets on api/call-another-graphql-server.md assume and do not show. /// The reader's own handler that adds an Authorization header. public sealed class BearerTokenHandler : DelegatingHandler; diff --git a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/authorization.cs b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/security/authorization-rules.cs similarity index 94% rename from Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/authorization.cs rename to Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/security/authorization-rules.cs index d342639dd..f287eb211 100644 --- a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/authorization.cs +++ b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/security/authorization-rules.cs @@ -1,4 +1,4 @@ -// What the compiled examples on authorization.md take as given: the trains' interfaces, inputs, +// What the compiled examples on security/authorization-rules.md take as given: the trains' interfaces, inputs, // outputs and junctions, and the entity the type-extension example extends. using Trax.Core.Functional; using Trax.Core.Junction; diff --git a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api-security.cs b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/security/secure-a-graphql-host.cs similarity index 89% rename from Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api-security.cs rename to Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/security/secure-a-graphql-host.cs index 9b9d675fb..547ccd140 100644 --- a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/api-security.cs +++ b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Context/security/secure-a-graphql-host.cs @@ -1,4 +1,4 @@ -// What the compiled examples on api-security.md take as given: the invoicing trains' interfaces, +// What the compiled examples on security/secure-a-graphql-host.md take as given: the invoicing trains' interfaces, // inputs, outputs and junctions. using Trax.Core.Junction; using Trax.Effect.Services.ServiceTrain; diff --git a/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Tests/TestExcerptsNameTheirPagesTests.cs b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Tests/TestExcerptsNameTheirPagesTests.cs new file mode 100644 index 000000000..57db7681e --- /dev/null +++ b/Trax.Docs/tests/Trax.Docs.Snippets.Tests/Tests/TestExcerptsNameTheirPagesTests.cs @@ -0,0 +1,160 @@ +using System.Text.RegularExpressions; + +namespace Trax.Docs.Snippets.Tests.Tests; + +/// +/// A page that shows a library's test, and the test, name each other. +/// +/// A page backs a behaviour with an excerpt of the test that proves it +/// (from=Trax.Core/tests/...), and holds the +/// excerpt to the file. That does not tell whoever edits the test that a page shows it. So the +/// test file says so, in a comment near its top: +/// +/// // Shown on https://traxsharp.net/docs/core/memory +/// +/// Both directions are checked: a page's excerpt of a file under a library folder's +/// tests/ needs that line in the file, and a line in such a file needs the page it names +/// to excerpt the file. Samples and templates are not covered: sample pages tour their sample, +/// and a template's comments ship into the reader's project. +/// +/// Enforces Trax.Docs/adr/0050-docs-pages-are-typed-and-their-code-is-checked-against-the-code.md. +/// +[Property( + "adr", + "Trax.Docs/adr/0050-docs-pages-are-typed-and-their-code-is-checked-against-the-code.md" +)] +[TestFixture] +public class TestExcerptsNameTheirPagesTests +{ + private const string Adr = + "Trax.Docs/adr/0050-docs-pages-are-typed-and-their-code-is-checked-against-the-code.md"; + + private static readonly Regex ShownOn = new( + @"^\s*//\s*Shown on https://traxsharp\.net(?/docs(?:/[A-Za-z0-9_\-]+)*)/?\s*$", + RegexOptions.Compiled | RegexOptions.Multiline + ); + + [Test] + public void Every_ExcerptOfALibraryTest_IsNamedByTheTest() + { + var offenders = Excerpted() + .Where(e => !PagesNamedBy(e.File).Contains(e.Page)) + .Select(e => + $"{e.Location}: {e.File} has no `// Shown on https://traxsharp.net{e.Page}`" + ) + .Distinct() + .ToList(); + + offenders + .Should() + .BeEmpty( + "a test a page shows says so, so whoever edits the test knows a page depends on " + + "it. Add the line near the top of the file. See " + + Adr + + ". Offenders:\n " + + string.Join("\n ", offenders) + ); + } + + [Test] + public void Every_ShownOnLine_NamesAPageThatShowsTheFile() + { + var shown = Excerpted().Select(e => (e.File, e.Page)).ToHashSet(); + + var claims = LibraryTestFiles() + .SelectMany(file => PagesNamedBy(file).Select(page => (File: file, Page: page))) + .ToList(); + + claims + .Should() + .NotBeEmpty( + "the examples in Trax.Core's tests name their pages; finding none means this checks nothing" + ); + + var offenders = claims + .Where(c => !shown.Contains(c)) + .Select(c => $"{c.File}: says it is shown on {c.Page}, which has no from= fence of it") + .ToList(); + + offenders + .Should() + .BeEmpty( + "a `// Shown on` line is a promise the page keeps. Excerpt the file on that page, " + + "or fix or delete the line. See " + + Adr + + ". Offenders:\n " + + string.Join("\n ", offenders) + ); + } + + [Test] + public void ShownOn_IsReadOnlyFromACommentLine() + { + const string source = """ + // Shown on https://traxsharp.net/docs/core/memory + // Shown on https://traxsharp.net/docs/sdk-reference/trains-and-junctions/train/ + var url = "// Shown on https://traxsharp.net/docs/elsewhere"; + """; + + ShownOn + .Matches(source) + .Select(m => m.Groups["page"].Value) + .Should() + .Equal("/docs/core/memory", "/docs/sdk-reference/trains-and-junctions/train"); + } + + /// + /// Each fence on a published page that excerpts a file under a library folder's tests/, + /// with the page as its URL path. + /// + private static IEnumerable<(string Location, string File, string Page)> Excerpted() => + Fences + .OnPublishedPages() + .Where(f => f.From is not null && IsLibraryTest(f.From)) + .Select(f => (f.Location, f.From!, UrlPath(f.Page))); + + private static bool IsLibraryTest(string repoRelative) + { + var segments = repoRelative.Split('/'); + return segments.Length > 2 + && segments[0].StartsWith("Trax.", StringComparison.Ordinal) + && segments[0] is not ("Trax.Samples" or "Trax.Docs") + && segments[1] == "tests"; + } + + private static string UrlPath(string page) + { + var path = page.Replace('\\', '/'); + path = path.EndsWith(".md", StringComparison.Ordinal) ? path[..^3] : path; + return path == "index" ? "/docs" : "/docs/" + path; + } + + private static IReadOnlySet PagesNamedBy(string repoRelative) + { + var file = Path.Combine( + new[] { RepoRoot.Monorepo }.Concat(repoRelative.Split('/')).ToArray() + ); + return File.Exists(file) + ? ShownOn + .Matches(File.ReadAllText(file)) + .Select(m => m.Groups["page"].Value) + .ToHashSet() + : new HashSet(); + } + + /// The C# files under each library folder's tests/, repository-relative. + private static IEnumerable LibraryTestFiles() => + Directory + .EnumerateDirectories(RepoRoot.Monorepo, "Trax.*") + .Where(folder => Path.GetFileName(folder) is not ("Trax.Samples" or "Trax.Docs")) + .Select(folder => Path.Combine(folder, "tests")) + .Where(Directory.Exists) + .SelectMany(tests => + Directory.EnumerateFiles(tests, "*.cs", SearchOption.AllDirectories) + ) + .Where(file => + !file.Split(Path.DirectorySeparatorChar) + .Any(s => s is "bin" or "obj" or "node_modules") + ) + .Select(file => Path.GetRelativePath(RepoRoot.Monorepo, file).Replace('\\', '/')); +} diff --git a/Trax.Docs/tests/Trax.Docs.Tests/Tests/SdkReferenceBlockTests.cs b/Trax.Docs/tests/Trax.Docs.Tests/Tests/SdkReferenceBlockTests.cs index 4a05262ec..e3ec5dcfa 100644 --- a/Trax.Docs/tests/Trax.Docs.Tests/Tests/SdkReferenceBlockTests.cs +++ b/Trax.Docs/tests/Trax.Docs.Tests/Tests/SdkReferenceBlockTests.cs @@ -68,7 +68,7 @@ public class SdkReferenceBlockTests "reference/comparison.md", "contributing/releases.md", // Only code block is a `gh attestation verify` CLI command, no SDK calls. - "supply-chain-security.md", + "security/supply-chain.md", // Convention pages whose code is directory trees, SQL, csproj fragments or test code // rather than SDK calls. There is no SDK method for a folder layout to link to. "reference/project-layout.md", diff --git a/Trax.Docs/what-trax-does.md b/Trax.Docs/what-trax-does.md new file mode 100644 index 000000000..d66d88e77 --- /dev/null +++ b/Trax.Docs/what-trax-does.md @@ -0,0 +1,90 @@ +--- +title: What Trax does +description: "A map of everything Trax does, one capability per row, with the book chapter that teaches it, the page that explains it and the package that ships it." +type: explanation +area: general +nav_order: 2 +--- + +# What Trax does + +Trax is built around one thing: the **train**, a class that does one piece of business work as a +chain of small steps called junctions. Everything else in Trax is a way to use that same class. +The database records every run of it. The mediator, the scheduler and workers decide when and +where it runs. The GraphQL API and the operations console let a client or an operator call it and +watch it. A state machine walks a user through a longer process and runs trains along the way. You +write the train once, and each of these attaches to it without changing it. + +```text + ┌──────────────────────────────┐ + your code ─────────────►│ │ + a GraphQL client ──────►│ a train │───► a run record + the scheduler ─────────►│ junction → junction → ... │ (when, how it ended, + a worker process ──────►│ │ which step failed, why) + a state machine ───────►│ │ │ + └──────────────────────────────┘ ▼ + the operations console +``` + +The [Glossary](/docs/reference/glossary) gives each Trax name its plain equivalent. The tables +below list what Trax does, grouped the way [the book](/docs/book) teaches it. Each row names the +chapter that builds it in a real application, the page that explains it, and the package to +install ([Packages](/docs/reference/packages) has the full list). + +## Write the work + +| Capability | In plain words | Learn it | Understand it | Package | +|---|---|---|---|---| +| Trains and junctions | One operation as typed steps; a failing step stops the rest and the caller gets the exception. | [Chapter 1](/docs/book/an-order-is-a-train) | [Trains and junctions](/docs/core/trains-and-junctions) | `Trax.Core` | +| Memory | Each step's output is handed to later steps by its type, with no wiring code. | [1.6](/docs/book/an-order-is-a-train/the-train) | [Memory](/docs/core/memory) | `Trax.Core` | +| The startup chain check | A host whose chain can never run refuses to start, naming the step and the missing type. | [1.9](/docs/book/an-order-is-a-train/when-the-chain-does-not-fit) | [Startup chain check](/docs/core/startup-chain-check) | `Trax.Mediator` | +| Decisions | A branch in the chain is a typed question a decider answers: a rule, a lookup or a model. Answers can be recorded and replayed. | [Chapter 5](/docs/book/does-this-order-look-like-fraud) | [Decisions](/docs/core/decisions) | `Trax.Core` | +| Parallel steps and checkpoints | Steps that run side by side, and a saved point a failed run resumes after. | [Chapter 14](/docs/book/checks-in-parallel-and-resuming) | [Checkpoints and branches](/docs/core/checkpoints-and-branches) | `Trax.Core` | +| Testing | Test a step on its own, a train on in-memory storage, and the whole host as a client calls it. | [Chapter 2](/docs/book/testing-place-order) | [Testing](/docs/cross-cutting/testing) | `Trax.Effect.Data.InMemory` | + +## Record every run + +| Capability | In plain words | Learn it | Understand it | Package | +|---|---|---|---|---| +| The run record | Every run is a row: its input, output, timing, and the step and exception that failed it. | [Chapter 3](/docs/book/every-order-is-recorded) | [Trax.Effect](/docs/effect) | `Trax.Effect` and a data provider | +| Your own tables | Your application's EF Core data beside Trax's, in the same database. | [Chapter 4](/docs/book/the-shops-own-data) | [Domain data contexts](/docs/effect/effect-providers/domain-data-contexts) | `Trax.Effect.Data.Postgres` | +| Run events | Lifecycle hooks and junction events, pushed to other processes and to browsers. | [Chapter 10](/docs/book/failures-retries-and-dead-letters) | [How event broadcasting works](/docs/effect/how-event-broadcasting-works) | `Trax.Effect` | + +## Run it when and where you need + +| Capability | In plain words | Learn it | Understand it | Package | +|---|---|---|---|---| +| Calling a train by its input | Run a train by handing over its input, without referencing the class. | [Chapter 2](/docs/book/testing-place-order) | [Mediator](/docs/mediator) | `Trax.Mediator` | +| Queued work | Ask for a run now and let it start when there is room, capped per train. | [Chapter 11](/docs/book/a-separate-worker) | [Queued work](/docs/mediator/queued-work) | `Trax.Mediator` | +| Schedules | Cron, intervals, one-off delays, and trains that run after another succeeds. | [Chapter 9](/docs/book/scheduled-work) | [Scheduling](/docs/scheduler) | `Trax.Scheduler` | +| Retries and dead letters | Retry with backoff, then park the run for an operator to requeue or acknowledge. | [Chapter 10](/docs/book/failures-retries-and-dead-letters) | [How retries work](/docs/scheduler/how-retries-work) | `Trax.Scheduler` | +| Workers | Run the same trains in other processes, over HTTP, SQS or AWS Lambda, and recover a run whose worker died. | [Chapter 11](/docs/book/a-separate-worker) | [Deployment topologies](/docs/reference/deployment-topologies) | `Trax.Scheduler` | + +## Serve it + +| Capability | In plain words | Learn it | Understand it | Package | +|---|---|---|---|---| +| Authorization | Nothing is served until a rule says who may call it; an unguarded train stops the host. | [Chapter 6](/docs/book/customers-and-staff) | [Authorization rules](/docs/security/authorization-rules) | `Trax.Effect` (the attributes), `Trax.Api.Auth` (sign-in) | +| GraphQL | Trains become typed queries and mutations, and query models become filterable queries. | [Chapter 7](/docs/book/serve-it-over-graphql) | [The GraphQL API](/docs/api) | `Trax.Api.GraphQL` | +| The operations console | Every run, schedule, dead letter and machine in one place, with requeue and cancel. | [Chapter 8](/docs/book/the-operations-console) | [Operations console](/docs/dashboard) | `Trax.Api.Dashboard` | +| Generated clients | A typed TypeScript client generated from the documents your frontend writes. | [Chapter 15](/docs/book/a-generated-client) | [Trax CLI](/docs/reference/cli) | the `trax` tool | +| Persisted operations | The server runs only the GraphQL documents you registered and refuses the rest. | [Chapter 17](/docs/book/require-persisted-operations) | [Persisted operations](/docs/api/persisted-operations) | `Trax.Api.GraphQL.PersistedOperations` | + +## Walk a user through a process + +| Capability | In plain words | Learn it | Understand it | Package | +|---|---|---|---|---| +| State machines | A long-lived flow (a checkout, a form) as named states the server and the browser agree on. | [Chapter 12](/docs/book/checkout-is-a-state-machine) | [State machines](/docs/statemachine) | `Trax.Effect.StateMachine` | +| A machine runs a train | A state starts a train and moves on with its result. | [Chapter 13](/docs/book/the-machine-places-the-order) | [Train or machine](/docs/statemachine/train-or-machine) | `Trax.Effect.StateMachine` | +| The browser twin | The same machine in TypeScript, so the browser predicts each move before the server confirms it. | [Chapter 16](/docs/book/checkout-in-the-browser) | [State machines](/docs/statemachine) | `@traxsharp/state-machine` | + +## Ship it + +| Capability | In plain words | Learn it | Understand it | Package | +|---|---|---|---|---| +| Deployment | One host, or a hub and workers against one Postgres and RabbitMQ, with health checks. | [Chapter 18](/docs/book/deploy) | [Deployment topologies](/docs/reference/deployment-topologies) | | +| Architecture guards | Tests your build runs to keep trains, junctions and registrations in the shape Trax expects. | [Chapter 2](/docs/book/testing-place-order) | [Architecture guards](/docs/reference/architecture-guards) | `Trax.Core.Testing` | + +## SDK Reference + +> [ServiceTrain](/docs/sdk-reference/trains-and-junctions/service-train) | [AddMediator](/docs/sdk-reference/configuration/add-mediator) | [AddScheduler](/docs/sdk-reference/scheduler-api/add-scheduler) | [AddTraxGraphQL](/docs/sdk-reference/graphql-api/add-trax-graphql) diff --git a/Trax.Effect/SECURITY.md b/Trax.Effect/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Effect/SECURITY.md +++ b/Trax.Effect/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Effect/src/Trax.Effect.StateMachine.Persistence/StateMachinesBuilderExtensions.cs b/Trax.Effect/src/Trax.Effect.StateMachine.Persistence/StateMachinesBuilderExtensions.cs index 1d4b5dadb..5cebc547d 100644 --- a/Trax.Effect/src/Trax.Effect.StateMachine.Persistence/StateMachinesBuilderExtensions.cs +++ b/Trax.Effect/src/Trax.Effect.StateMachine.Persistence/StateMachinesBuilderExtensions.cs @@ -56,7 +56,8 @@ params Assembly[] assemblies throw new InvalidOperationException( "AddStateMachines() requires a data provider. Configure one in AddEffects() first: " + ".UsePostgres(connectionString), .UseSqlite(connectionString), or .UseInMemory(). " - + "The snapshot store needs a database to persist drafts and effect claims." + + "The snapshot store needs a database to persist drafts and effect claims. See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#addstatemachines-requires-a-data-provider" ); if (builder.Root.MediatorConfigured) diff --git a/Trax.Effect/src/Trax.Effect/Configuration/TraxEffectBuilder/TraxEffectBuilder.Build.cs b/Trax.Effect/src/Trax.Effect/Configuration/TraxEffectBuilder/TraxEffectBuilder.Build.cs index 03675371c..fc669bcb3 100644 --- a/Trax.Effect/src/Trax.Effect/Configuration/TraxEffectBuilder/TraxEffectBuilder.Build.cs +++ b/Trax.Effect/src/Trax.Effect/Configuration/TraxEffectBuilder/TraxEffectBuilder.Build.cs @@ -16,7 +16,8 @@ internal TraxEffectConfiguration.TraxEffectConfiguration Build() + " .UsePostgres(connectionString) // or .UseSqlite(connectionString) / .UseInMemory()\n" + " .AddJunctionProgress()\n" + " )\n" - + " );\n" + + " );\n\n" + + "See https://traxsharp.net/docs/cross-cutting/troubleshooting#addjunctionprogress-requires-a-data-provider-usepostgres-usesqlite-or-useinmemory" ); } diff --git a/Trax.Mediator/SECURITY.md b/Trax.Mediator/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Mediator/SECURITY.md +++ b/Trax.Mediator/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Mediator/src/Trax.Mediator/Exceptions/NoTrainForInputException.cs b/Trax.Mediator/src/Trax.Mediator/Exceptions/NoTrainForInputException.cs index 826c5b349..2e2cd0552 100644 --- a/Trax.Mediator/src/Trax.Mediator/Exceptions/NoTrainForInputException.cs +++ b/Trax.Mediator/src/Trax.Mediator/Exceptions/NoTrainForInputException.cs @@ -43,5 +43,5 @@ private static string Describe(Type inputType, IReadOnlyList scannedAsse : "" ) + "Add a train that takes this input type, or add the assembly that holds its train to " - + "ScanAssemblies(...)."; + + "ScanAssemblies(...). See https://traxsharp.net/docs/cross-cutting/troubleshooting#could-not-find-train-with-input-type-x"; } diff --git a/Trax.Mediator/src/Trax.Mediator/Extensions/BuilderOrderExtensions.cs b/Trax.Mediator/src/Trax.Mediator/Extensions/BuilderOrderExtensions.cs index 3d815fc98..da7bea6f7 100644 --- a/Trax.Mediator/src/Trax.Mediator/Extensions/BuilderOrderExtensions.cs +++ b/Trax.Mediator/src/Trax.Mediator/Extensions/BuilderOrderExtensions.cs @@ -21,7 +21,8 @@ namespace Trax.Mediator.Extensions; [EditorBrowsable(EditorBrowsableState.Never)] public static class BuilderOrderExtensions { - internal const string AddEffectsFirst = "Call AddEffects(...) before AddMediator(...)."; + internal const string AddEffectsFirst = + "Call AddEffects(...) before AddMediator(...). See https://traxsharp.net/docs/cross-cutting/troubleshooting#call-addeffects-before-addmediator"; internal const string AddMediatorOnce = "AddMediator(...) is already called. Call it once and configure everything in that call."; diff --git a/Trax.Mediator/src/Trax.Mediator/Services/ChainVerification/TrainChainStartupValidator.cs b/Trax.Mediator/src/Trax.Mediator/Services/ChainVerification/TrainChainStartupValidator.cs index 510d12031..56656c248 100644 --- a/Trax.Mediator/src/Trax.Mediator/Services/ChainVerification/TrainChainStartupValidator.cs +++ b/Trax.Mediator/src/Trax.Mediator/Services/ChainVerification/TrainChainStartupValidator.cs @@ -123,6 +123,8 @@ public async Task StartingAsync(CancellationToken cancellationToken) $"{failedTrains} of {checkedTrains} registered trains cannot run:" + Environment.NewLine + string.Join(Environment.NewLine, problems.Select(p => " - " + p)) + + Environment.NewLine + + "See https://traxsharp.net/docs/cross-cutting/troubleshooting#n-of-m-registered-trains-cannot-run" ); logger?.LogDebug("Verified the chains of {TrainCount} trains.", checkedTrains); diff --git a/Trax.Mediator/tests/Trax.Mediator.Tests.MemoryLeak.Integration/UnitTests/TrainChainStartupValidatorTests.cs b/Trax.Mediator/tests/Trax.Mediator.Tests.MemoryLeak.Integration/UnitTests/TrainChainStartupValidatorTests.cs index fbaa489e1..76ac5a07b 100644 --- a/Trax.Mediator/tests/Trax.Mediator.Tests.MemoryLeak.Integration/UnitTests/TrainChainStartupValidatorTests.cs +++ b/Trax.Mediator/tests/Trax.Mediator.Tests.MemoryLeak.Integration/UnitTests/TrainChainStartupValidatorTests.cs @@ -385,6 +385,18 @@ public async Task Startup_WhenAJunctionsInputNeverReachesMemory_RefusesToStart() .And.Contain("Chain a junction that produces it first"); } + [Test] + public async Task Startup_WhenAChainCannotRun_TheRefusalLinksItsTroubleshootingEntry() + { + var failure = await Start(); + + failure! + .Message.Should() + .EndWith( + "See https://traxsharp.net/docs/cross-cutting/troubleshooting#n-of-m-registered-trains-cannot-run" + ); + } + [Test] public async Task Startup_WhenAChainReadsItsInput_RefusesToStart() { diff --git a/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/PublicApi/Trax.Mediator.received.txt b/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/PublicApi/Trax.Mediator.received.txt index 3c45d7f5e..25dd2fe68 100644 --- a/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/PublicApi/Trax.Mediator.received.txt +++ b/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/PublicApi/Trax.Mediator.received.txt @@ -85,9 +85,11 @@ namespace Trax.Mediator.Extensions { public static class BuilderOrderExtensions { - [System.Obsolete("Call AddEffects(...) before AddMediator(...).", true)] + [System.Obsolete("Call AddEffects(...) before AddMediator(...). See https://traxsharp.net/docs/cros" + + "s-cutting/troubleshooting#call-addeffects-before-addmediator", true)] public static Trax.Mediator.Configuration.TraxBuilderWithMediator AddMediator(this Trax.Effect.Configuration.TraxBuilder.TraxBuilder builder, System.Func configure) { } - [System.Obsolete("Call AddEffects(...) before AddMediator(...).", true)] + [System.Obsolete("Call AddEffects(...) before AddMediator(...). See https://traxsharp.net/docs/cros" + + "s-cutting/troubleshooting#call-addeffects-before-addmediator", true)] public static Trax.Mediator.Configuration.TraxBuilderWithMediator AddMediator(this Trax.Effect.Configuration.TraxBuilder.TraxBuilder builder, params System.Reflection.Assembly[] assemblies) { } [System.Obsolete("AddMediator(...) is already called. Call it once and configure everything in that" + " call.", true)] diff --git a/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/Tests/BuilderOrderDiagnosticsTests.cs b/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/Tests/BuilderOrderDiagnosticsTests.cs index 5d3ffe35e..0aae142b3 100644 --- a/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/Tests/BuilderOrderDiagnosticsTests.cs +++ b/Trax.Mediator/tests/Trax.Mediator.Tests.Meta/Tests/BuilderOrderDiagnosticsTests.cs @@ -36,19 +36,23 @@ private static IEnumerable WrongOrder() { yield return new TestCaseData( "trax.AddMediator(assembly)", - "Call AddEffects(...) before AddMediator(...)." + "Call AddEffects(...) before AddMediator(...). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#call-addeffects-before-addmediator" ).SetName("AddMediator(assemblies) before AddEffects"); yield return new TestCaseData( "trax.AddMediator()", - "Call AddEffects(...) before AddMediator(...)." + "Call AddEffects(...) before AddMediator(...). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#call-addeffects-before-addmediator" ).SetName("AddMediator() before AddEffects"); yield return new TestCaseData( "trax.AddMediator(assemblies: [assembly])", - "Call AddEffects(...) before AddMediator(...)." + "Call AddEffects(...) before AddMediator(...). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#call-addeffects-before-addmediator" ).SetName("AddMediator(assemblies: [...]) before AddEffects"); yield return new TestCaseData( "trax.AddMediator(mediator => mediator.ScanAssemblies(assembly))", - "Call AddEffects(...) before AddMediator(...)." + "Call AddEffects(...) before AddMediator(...). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#call-addeffects-before-addmediator" ).SetName("AddMediator(configure) before AddEffects"); yield return new TestCaseData( "trax.AddEffects().AddMediator(assembly).AddMediator(assembly)", diff --git a/Trax.Samples/SECURITY.md b/Trax.Samples/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Samples/SECURITY.md +++ b/Trax.Samples/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Samples/samples/PersistedOperations/README.md b/Trax.Samples/samples/PersistedOperations/README.md index d694d63ed..669ed9c25 100644 --- a/Trax.Samples/samples/PersistedOperations/README.md +++ b/Trax.Samples/samples/PersistedOperations/README.md @@ -68,4 +68,4 @@ The suite runs against the `persisted_operations_e2e_tests` database on port 543 ## Docs [Persisted Operations sample](https://traxsharp.net/docs/samples/persisted-operations) and -[Persisted Operations](https://traxsharp.net/docs/persisted-operations). +[Persisted Operations](https://traxsharp.net/docs/api/persisted-operations). diff --git a/Trax.Samples/templates/content/Trax.Samples.Api/Program.cs b/Trax.Samples/templates/content/Trax.Samples.Api/Program.cs index 15bb6a0de..d8a5c0254 100644 --- a/Trax.Samples/templates/content/Trax.Samples.Api/Program.cs +++ b/Trax.Samples/templates/content/Trax.Samples.Api/Program.cs @@ -42,7 +42,7 @@ // only in Development: Trax.Api refuses to start a host outside Development with a key // containing "do-not-use-in-production" registered. Anywhere else, register real // credentials (AddHashed keys from a secret store, or AddTraxJwtAuth) before removing the -// IsDevelopment() check: https://traxsharp.net/docs/api-security. +// IsDevelopment() check: https://traxsharp.net/docs/security/secure-a-graphql-host. // AddAuthentication() stays outside the check. AddTraxApiKeyAuth registers authentication // itself, so without this line a start outside Development fails in UseAuthentication() // with "Unable to resolve service for type IAuthenticationSchemeProvider". AddAuthorization() diff --git a/Trax.Samples/templates/content/Trax.Samples.Api/README.md b/Trax.Samples/templates/content/Trax.Samples.Api/README.md index 2b15fcf90..3b966d503 100644 --- a/Trax.Samples/templates/content/Trax.Samples.Api/README.md +++ b/Trax.Samples/templates/content/Trax.Samples.Api/README.md @@ -156,7 +156,7 @@ operation is refused. - Replace the demo key with real credentials (`keys.AddHashed(...)` from a secret store, or `AddTraxJwtAuth`), then remove the `IsDevelopment()` check around `AddTraxApiKeyAuth`. See - [API Security](https://traxsharp.net/docs/api-security). + [API Security](https://traxsharp.net/docs/security/secure-a-graphql-host). - Outside `dotnet run` the URL comes from `ASPNETCORE_URLS` (or `ASPNETCORE_HTTP_PORTS`), not `launchSettings.json`. diff --git a/Trax.Samples/templates/content/Trax.Samples.Hub/Program.cs b/Trax.Samples/templates/content/Trax.Samples.Hub/Program.cs index 4a79168f9..4e06a1296 100644 --- a/Trax.Samples/templates/content/Trax.Samples.Hub/Program.cs +++ b/Trax.Samples/templates/content/Trax.Samples.Hub/Program.cs @@ -48,7 +48,7 @@ // operator key holds. Both exist only in Development: Trax.Api refuses to start a host outside // Development with a key containing "do-not-use-in-production" registered. Anywhere else, register real // credentials (AddHashed keys from a secret store, or AddTraxJwtAuth) before removing the -// IsDevelopment() check: https://traxsharp.net/docs/api-security. +// IsDevelopment() check: https://traxsharp.net/docs/security/secure-a-graphql-host. // AddAuthentication() stays outside the check. AddTraxApiKeyAuth registers authentication // itself, so without this line a start outside Development fails in UseAuthentication() // with "Unable to resolve service for type IAuthenticationSchemeProvider". AddAuthorization() diff --git a/Trax.Samples/templates/content/Trax.Samples.Hub/README.md b/Trax.Samples/templates/content/Trax.Samples.Hub/README.md index c3be4ba4c..4841c9e94 100644 --- a/Trax.Samples/templates/content/Trax.Samples.Hub/README.md +++ b/Trax.Samples/templates/content/Trax.Samples.Hub/README.md @@ -158,7 +158,7 @@ operation is refused and `/trax/dashboard` is a 404. - Replace the demo keys with real credentials (`keys.AddHashed(...)` from a secret store, or `AddTraxJwtAuth`), then remove the `IsDevelopment()` check around `AddTraxApiKeyAuth`. See - [API Security](https://traxsharp.net/docs/api-security). + [API Security](https://traxsharp.net/docs/security/secure-a-graphql-host). - Add a sign-in the browser carries (a cookie or OpenID Connect), choose who may load the console, `RequirePolicy("")` or `RequireRoles("")`, in place of `AllowAnonymousShell()`, then remove the `IsDevelopment()` check around it. See diff --git a/Trax.Samples/templates/content/Trax.Samples.Scheduler/Program.cs b/Trax.Samples/templates/content/Trax.Samples.Scheduler/Program.cs index da2035745..800954cee 100644 --- a/Trax.Samples/templates/content/Trax.Samples.Scheduler/Program.cs +++ b/Trax.Samples/templates/content/Trax.Samples.Scheduler/Program.cs @@ -38,7 +38,7 @@ // refuses to start a host outside Development with a key containing // "do-not-use-in-production" registered. Anywhere else, register real credentials (AddHashed // keys from a secret store, or AddTraxJwtAuth) before removing the IsDevelopment() check: -// https://traxsharp.net/docs/api-security. +// https://traxsharp.net/docs/security/secure-a-graphql-host. // AddAuthentication() stays outside the check. AddTraxApiKeyAuth registers authentication // itself, so without this line a start outside Development fails in UseAuthentication(). if (builder.Environment.IsDevelopment()) diff --git a/Trax.Samples/templates/content/Trax.Samples.Scheduler/README.md b/Trax.Samples/templates/content/Trax.Samples.Scheduler/README.md index f0ad95e2c..3112aa8e6 100644 --- a/Trax.Samples/templates/content/Trax.Samples.Scheduler/README.md +++ b/Trax.Samples/templates/content/Trax.Samples.Scheduler/README.md @@ -142,7 +142,7 @@ the trains still run on schedule, every operation is refused, and `/trax/dashboa - Replace the operator key with real credentials (`keys.AddHashed(...)` from a secret store, or `AddTraxJwtAuth`), then remove the `IsDevelopment()` check around `AddTraxApiKeyAuth`. See - [API Security](https://traxsharp.net/docs/api-security). + [API Security](https://traxsharp.net/docs/security/secure-a-graphql-host). - Add a sign-in the browser carries (a cookie or OpenID Connect), choose who may load the console, `RequirePolicy("")` or `RequireRoles("")`, in place of `AllowAnonymousShell()`, then remove the `IsDevelopment()` check around it. See diff --git a/Trax.Scheduler/SECURITY.md b/Trax.Scheduler/SECURITY.md index e8e81d837..07ecba48a 100644 --- a/Trax.Scheduler/SECURITY.md +++ b/Trax.Scheduler/SECURITY.md @@ -12,4 +12,4 @@ Fixes ship against the latest version published to NuGet. Older major versions a ## Supply-chain posture -Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +Trax builds and publishes through a defense-in-depth pipeline: SHA-pinned actions, isolated release credentials, OIDC trusted publishing with SLSA provenance, and locked + audited dependencies (a known-vulnerability advisory fails the build). See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.Build.cs b/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.Build.cs index 8b22d22bd..4b66460f0 100644 --- a/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.Build.cs +++ b/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.Build.cs @@ -376,7 +376,8 @@ private void ValidateSubmitterRequirements() + " .AddEffects(effects => effects.UsePostgres(connectionString)) // or .UseSqlite(connectionString) / .UseInMemory()\n" + " .AddMediator(assemblies)\n" + " .AddScheduler(scheduler => ...)\n" - + " );\n" + + " );\n\n" + + "See https://traxsharp.net/docs/cross-cutting/troubleshooting#addscheduler-requires-a-data-provider-usepostgres-usesqlite-or-useinmemory" ); } } diff --git a/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.SeedValidation.cs b/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.SeedValidation.cs index 1c262224f..815731c69 100644 --- a/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.SeedValidation.cs +++ b/Trax.Scheduler/src/Trax.Scheduler/Configuration/SchedulerConfigurationBuilder/SchedulerConfigurationBuilder.SeedValidation.cs @@ -149,7 +149,7 @@ private void ValidateSeedDeclarations() + $"(prefix '{longer.Prefix}'). Each start, one batch would delete the other's " + "manifests and their history.\n\n" + "Give the batches names where neither plus '-' starts the other, or put them " - + "in different groups." + + "in different groups. See https://traxsharp.net/docs/cross-cutting/troubleshooting#batch-x-prunes-manifests-whose-external-id-starts-with-" ); } @@ -176,7 +176,8 @@ private void RejectSingleSchedulesInsideBatchPrunes() + $"Give '{single.ExternalId}' an external ID that does not start with " + $"'{prune.Prefix}'" + (prune.ScopedToGroup ? ", put it in a different group," : "") - + " or make it one of the batch's items." + + " or make it one of the batch's items. See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#batch-x-prunes-manifests-whose-external-id-starts-with-" ); } } @@ -203,7 +204,8 @@ Func value + $"{Describe(value(conflicting))} by {conflicting.DeclaredBy}. " + "Every start writes a group setting a member states, so the value in force " + "would depend on seeding order.\n\n" - + $"State {setting} on one member of the group, or the same value on each." + + $"State {setting} on one member of the group, or the same value on each. See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#manifest-group-x-is-given-two-different-y-values" ); } diff --git a/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/CronParser.cs b/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/CronParser.cs index 0a383a197..b6f906b4e 100644 --- a/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/CronParser.cs +++ b/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/CronParser.cs @@ -40,7 +40,8 @@ public static CronExpression Parse(string expression) var format = DetectFormat(expression) ?? throw new FormatException( - $"Invalid cron expression: '{expression}'. Expected 5 or 6 space-separated fields." + $"Invalid cron expression: '{expression}'. Expected 5 or 6 space-separated fields. " + + "See https://traxsharp.net/docs/cross-cutting/troubleshooting#invalid-cron-expression-x-expected-5-or-6-space-separated-fields" ); return CronExpression.Parse(expression, format); diff --git a/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/Schedule.cs b/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/Schedule.cs index 9fc7c7348..6956a3378 100644 --- a/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/Schedule.cs +++ b/Trax.Scheduler/src/Trax.Scheduler/Services/Scheduling/Schedule.cs @@ -71,7 +71,8 @@ public static Schedule FromInterval(TimeSpan interval) throw new ArgumentOutOfRangeException( nameof(interval), interval, - "A schedule interval must be at least one second." + "A schedule interval must be at least one second. See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#a-schedule-interval-must-be-at-least-one-second" ); return new() { Type = ScheduleType.Interval, Interval = interval }; @@ -110,7 +111,8 @@ public static Schedule FromCron(string expression) throw new FormatException( $"Cron expression '{expression}' never fires: it has no occurrence in the next " + $"{CronSearchWindowYears} years. Check the day of the month against the months " - + "it names (February has no 30th, April no 31st)." + + "it names (February has no 30th, April no 31st). See " + + "https://traxsharp.net/docs/cross-cutting/troubleshooting#cron-expression-x-never-fires-it-has-no-occurrence-in-the-next-10-years" ); return new() { Type = ScheduleType.Cron, CronExpression = expression }; diff --git a/Trax.Website/SECURITY.md b/Trax.Website/SECURITY.md index f5f8a72a2..c9d4d47e4 100644 --- a/Trax.Website/SECURITY.md +++ b/Trax.Website/SECURITY.md @@ -12,4 +12,4 @@ Only the site as deployed at [traxsharp.net](https://traxsharp.net), built from ## Supply-chain posture -The site builds through SHA-pinned actions, installs from the committed lockfile with install scripts disabled in CI, and fails a build whose runtime dependencies carry a high-severity advisory. The docs pages are rendered from [Trax.Docs](https://github.com/TraxSharp/Trax/tree/main/Trax.Docs) `main`. See the [Supply Chain Security](https://traxsharp.net/docs/supply-chain-security) guide. +The site builds through SHA-pinned actions, installs from the committed lockfile with install scripts disabled in CI, and fails a build whose runtime dependencies carry a high-severity advisory. The docs pages are rendered from [Trax.Docs](https://github.com/TraxSharp/Trax/tree/main/Trax.Docs) `main`. See the [Supply Chain Security](https://traxsharp.net/docs/security/supply-chain) guide. diff --git a/Trax.Website/src/app/dashboard/page.tsx b/Trax.Website/src/app/dashboard/page.tsx index 192eda1f1..ccd155e30 100644 --- a/Trax.Website/src/app/dashboard/page.tsx +++ b/Trax.Website/src/app/dashboard/page.tsx @@ -1,13 +1,13 @@ import type { Metadata } from "next"; -// The operations dashboard, served as a static build under /dashboard/demo/ (see +// The operations console, served as a static build under /dashboard/demo/ (see // scripts/build-dashboard-demo.sh) and framed here. It answers from recordings of real sample hosts, // made by Trax.Samples' scripts/recordings/dashboard.mjs; Trax.Api/src/Trax.Api.Dashboard/ClientApp/README.md says how. export const metadata: Metadata = { - title: "Dashboard", + title: "Operations console", description: - "The Trax operations dashboard, running in your browser on recordings of the Scheduling, Recovery and Persisted operations samples.", + "The Trax operations console, running in your browser on recordings of the Scheduling, Recovery and Persisted operations samples.", }; const DEMO = "/dashboard/demo"; @@ -17,10 +17,10 @@ export default function DashboardPage() { // The dashboard fills the viewport below the header (h-16 plus its 1px border). Its own banner says // it runs on recordings. Below 1024px, the width it is laid out for, the frame scrolls sideways.
-

Dashboard

+

Operations console