Add Trellis.Microservices.Abstractions package - #2
Conversation
First .cs lands in xavierjohn/Trellis.Microservices. The Abstractions package ships a single public static class — TrellisInternalJwtClaimNames — that hosts the canonical contract literals for the Trellis internal JWT v1. Why --- Today in xavierjohn/Trellis, the canonical claim names live as `internal const` literals inside Trellis.Yarp, with the consumer side (Trellis.Asp.Authorization.TrellisInternalJwtActorOptions) hard-coding the same strings as defaults. Both sides agree by CONVENTION only — code review and a contract test that loads both projects and asserts equality. A typo or future-contract-version change to one side without the other would create a silent fail-open / fail-closed divergence. This package promotes those constants to public, gives them a stable namespace (Trellis.Microservices.Abstractions), and lets BOTH sides reference one source-of-truth literal set. Third-party gateway and consumer implementations now have a versioned NuGet contract to compile against. What ships ---------- - public static class TrellisInternalJwtClaimNames — 8 public const strings (Subject, JwtId, Permissions, ForbiddenPermissions, ContractVersion, PermissionsCount, ForbiddenPermissionsCount, CurrentContractVersion="1"). Values are bit-for-bit identical to the existing internal copy in Trellis.Yarp. - AOT-compatible (IsAotCompatible=true), no runtime dependencies. - TrellisApiRefName=microservices-abstractions — auto-packs the API reference doc into the .nupkg's trellis/ folder for LLM discovery. - TrellisShipsCookbook=true — this is the foundational package every consumer references transitively; ships the cookbook once via this package (mirroring how main repo's Trellis.Core ships the main cookbook). Tests (12 total, all green) --------------------------- - 8 per-constant value pins (one per public const). - Public_const_surface_is_exactly_eight_members — snapshot guard against silent surface additions/removals. - All_constants_are_non_empty. - All_constant_values_are_unique (excluding CurrentContractVersion which can legitimately collide with a future structural claim's value). - Type_modifiers_match_public_static_class_contract — pins IsPublic + IsAbstract + IsSealed (catches a regression where the class accidentally goes internal; InternalsVisibleTo would let tests still pass but consumers would break). Verification ------------ - dotnet build -c Release: 0 warnings, 0 errors. - dotnet test -c Release --no-build: 12/12 pass. - dotnet pack: nupkg correctly contains lib/, README.md (from NUGET_README.md), icon.png, both api-ref + cookbook md in trellis/, build + buildTransitive targets. - docs/lint-api-reference.ps1: clean. - All relative links in src/README.md verified to resolve from the README location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Introduces the first shipping source package in this repository: Trellis.Microservices.Abstractions, a versioned NuGet contract that centralizes the Trellis internal JWT v1 claim-name literals (so gateway minting and consumer hydration stay in lock-step).
Changes:
- Added
Trellis.Microservices.Abstractionspackage withTrellisInternalJwtClaimNames(8 publicconst stringcontract literals). - Added
Trellis.Microservices.Abstractions.Testswith literal pinning + contract surface guard tests. - Wired the new projects into
Trellis.Microservices.slnxand updatedCHANGELOG.md.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| Trellis.Microservices.slnx | Adds the new Abstractions src/tests projects (and package readmes) to the solution. |
| Trellis.Microservices.Abstractions/src/Trellis.Microservices.Abstractions.csproj | Defines the new package metadata and packing behavior (API ref + cookbook + NuGet README). |
| Trellis.Microservices.Abstractions/src/TrellisInternalJwtClaimNames.cs | Adds the public, canonical internal-JWT v1 claim-name constants. |
| Trellis.Microservices.Abstractions/src/README.md | Repository-facing package README describing purpose, usage, and contract rules. |
| Trellis.Microservices.Abstractions/src/NUGET_README.md | NuGet package README content to be packed as /README.md in the nupkg. |
| Trellis.Microservices.Abstractions/tests/Trellis.Microservices.Abstractions.Tests.csproj | Adds the test project referencing the src project. |
| Trellis.Microservices.Abstractions/tests/TrellisInternalJwtClaimNamesTests.cs | Adds tests pinning literal values and guarding the public const surface/modifiers. |
| CHANGELOG.md | Documents the new package and tests in Unreleased, plus notes about planned cleanup. |
|
|
||
| ### Notes | ||
|
|
||
| - The pre-existing `internal` copy of `TrellisInternalJwtClaimNames` in `xavierjohn/Trellis`'s `Trellis.Yarp` will be removed in the upcoming PR C (which moves `Trellis.Yarp` into this repo and rewires it to depend on `Trellis.Microservices.Abstractions`). |
There was a problem hiding this comment.
Fixed in the next commit. Rewrote the note to be accurately scoped to this repo: PR C lands Trellis.Yarp here (with the imported version using the public Abstractions class, no internal duplicate); removal of the internal copy from upstream xavierjohn/Trellis is a separate PR (PR D) against that repo, documented in that repo's CHANGELOG — not this one.
Round-1 Copilot reviewer finding (CHANGELOG.md:14): The 'Unreleased / Notes' bullet said the internal copy of TrellisInternalJwtClaimNames in xavierjohn/Trellis's Trellis.Yarp would be removed in PR C. The PR body, however, correctly placed removal-from-main in PR D (PR C moves the file INTO this repo; PR D removes it FROM main). Rewrite the bullet to be accurately scoped to this repo: - PR C in THIS repo lands Trellis.Yarp here, rewired to use the public Abstractions class (so the imported version has no internal duplicate). - Removal of the internal copy from upstream xavierjohn/Trellis is a separate PR (PR D) against that repo, documented in that repo's CHANGELOG — not this one. This eliminates the cross-repo timing claim from this CHANGELOG. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…CHANGELOG
Round-1 Copilot reviewer findings (all 11 applied):
1+5. ReleaseGateScenarios.cs:16 - class XmlDoc listed a "KeyRotation"
gateway-to-downstream scenario that doesn't exist. Updated the
docstring to list the actual 8 scenarios by category.
2. HarnessFixtures.cs:94 - fixture claimed to use "strict cookbook
Recipe 1 profile" but missed RequireExpirationTime=true. Added.
3. HarnessFixtures.cs:62 - remarks said "mediator pipeline 401s" but
this harness has no mediator. The /probe endpoint handler itself
returns 401 on Maybe.None. Comment updated.
4. E2EHarness.Tests.csproj:37 - removed unused
Microsoft.Extensions.TimeProvider.Testing PackageReference
(nothing in the project uses FakeTimeProvider).
6+7+8. POST -> GET across ReleaseGateScenarios.cs:27, HarnessFixtures.cs:30,
README.md:27. The harness actually uses GET /probe with
Authorization: Bearer header (per HarnessFixtures.ProbeAsync); the
"POST" wording in the docs was inaccurate.
9. README.md:69 - P4-invariant mapping row for MapInboundClaims=false /
TryAllIssuerSigningKeys=false was still over-claiming. Narrowed
further: the row now says the fixture configures both consistent
with Recipe 1 but does NOT prove either's protective effect (the
actor provider has explicit sub short-long fallback that lets the
contract work even with MapInboundClaims=true; the wrong-kid
fallback path is not exercised with the single-key fixture).
10. GatewayHarness.cs:89 - the TestServerForwarderHttpClientFactory
captures an HttpMessageHandler but never disposes it (handler leak
across larger/looped runs). Made the factory IDisposable so DI
disposes the handler when the host disposes.
11. CHANGELOG.md:11 - section heading "0.1-alpha.d - Cookbook Recipes
1+2 inlined" had bullets describing the Abstractions package. Root
cause: a prior edit during PR #7 ate the 0.1-alpha.b heading and
mis-attributed 0.1-alpha.b's bullets to 0.1-alpha.c, then PR #8
compounded the damage by renaming 0.1-alpha.c -> 0.1-alpha.d.
Reconstructed the full CHANGELOG from git history. Structure now:
## Unreleased - E2E harness (this PR)
## 0.1-alpha.d - Recipes inlining + slot-ref cleanup (PR #7)
## 0.1-alpha.c - Yarp move + AspNetCore carve-out (PR #3)
## 0.1-alpha.b - Abstractions package (PR #2)
## 0.1-alpha.a - Initial bootstrap (PR #1)
The 0.1-alpha.d body was recovered from origin/main's Unreleased
section (post-PR-#7-merge); the 0.1-alpha.c body was recovered from
PR #3's merge-commit CHANGELOG state via git show.
Build + test + lint: 0 warnings, 0 errors; 231/231 pass; doc-lint clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What
PR B of the carve-out. Ships the first
.cstoxavierjohn/Trellis.Microservices: the Abstractions package containing a single public static classTrellisInternalJwtClaimNameswith the canonical contract-claim literals for the Trellis internal JWT v1.Why
The constants currently live as
internal sealed classinxavierjohn/Trellis'sTrellis.Yarp/src/TrellisInternalJwtClaimNames.cs, while the consumer side (Trellis.Asp.Authorization.TrellisInternalJwtActorOptionsdefaults) hard-codes the same strings by convention. Both sides agree by code review only — there's a real risk a typo or contract-version bump on one side slips through and creates a silent fail-open / fail-closed divergence.This PR makes the literals a public, versioned NuGet contract that both sides will reference (in PR C and beyond).
Files
Also updated:
Trellis.Microservices.slnx— added the new project pair under/Trellis.Microservices.Abstractions/CHANGELOG.md— Unreleased section + retroactive0.1-alpha — Initial bootstrapentry for PR Bootstrap repository scaffolding and microservices documentation #1Test coverage (12 tests, all green)
public const stringexactly matches the contract literalPublic_const_surface_is_exactly_eight_membersAll_constants_are_non_emptyAll_constant_values_are_uniqueCurrentContractVersionwhich can legitimately collide with a future structural-claim value)Type_modifiers_match_public_static_class_contractIsPublic+IsAbstract+IsSealed. Without this, theInternalsVisibleTodeclaration inDirectory.Build.propswould let the const-value tests still pass if the class accidentally got changed tointernal, but the published NuGet would be unusable.Package contents (verified via
dotnet pack)Auto-versioned by Nerdbank.GitVersioning to
0.1.0-alpha.{height}.g{sha}per the0.1-alpha.{height}placeholder inversion.json.Verification
dotnet build Trellis.Microservices.slnx -c Release— 0 warnings, 0 errorsdotnet test Trellis.Microservices.slnx -c Release --no-build— 12/12 pass (715 ms)pwsh docs/lint-api-reference.ps1— clean (4 docs scanned)dotnet pack— nupkg layout matches expectation;TrellisApiRefName+TrellisShipsCookbookmechanisms work as designedsrc/README.mdresolve from the README locationCode review (pre-commit)
Ran one GPT-5.5 round (
code-reviewagent, sync, on Sonnet target docs). Findings:IsPublic/IsAbstract/IsSealed—InternalsVisibleTowould mask a regression tointernalType_modifiers_match_public_static_class_contracttest (count above)../...relative links insrc/README.mdresolved to non-existent paths (need../../...)Trellis.Yarp/Trellis.Microservices.AspNetCoredir links). Pointed at the existing API ref docs indocs/docfx_project/api_reference/instead of the not-yet-existent package dirs (avoid forward-pointer breakage).Zero T1 findings.
NOT in this PR
Trellis.Yarp/andTrellis.Microservices.AspNetCore/package source — lands in PR C (the move + carve-out from main).internalduplicate fromxavierjohn/Trellis'sTrellis.Yarp/src/TrellisInternalJwtClaimNames.cs— lands in PR D (main repo cleanup), after the Trellis.Yarp move (PR C) is in place.version.jsonfrom the placeholder0.1-alpha.{height}to a final pre-1.0 / pre-release form — still tracked as outstanding decision Bootstrap repository scaffolding and microservices documentation #1 insession-state/files/carveout-plan.md. The current placeholder cleanly produces0.1.0-alpha.{height}.g{sha}which is publishable as-is if needed.Reviewer focus
public const stringvalue inTrellis.Microservices.Abstractions/src/TrellisInternalJwtClaimNames.csexactly matches the existinginternal const stringinxavierjohn/Trellis Trellis.Yarp/src/TrellisInternalJwtClaimNames.cs. Any drift here is a contract break.Type_modifiers_match_public_static_class_contracttest — the rationale (InternalsVisibleTo masks ainternalaccidental regression) is worth one careful read. The test is small but high-value.csprojshape —IsAotCompatible=true,TrellisApiRefName=microservices-abstractions,TrellisShipsCookbook=true. NoPackageReferenceentries (no runtime deps).🤖 Generated with GitHub Copilot CLI