Owns private-package entitlements and short-lived feed credentials for commercial distribution, with revocation-aware credential introspection.
Primary path at a glance
The coral node marks the module’s central decision or state boundary.
Capability boundary
- Tenant/customer/product/channel/package-prefix entitlement scope
- Grant and irreversible revoke lifecycle
- Protected short-lived feed tokens and fail-closed introspection
The module explicitly does not own the following: A package entitlement grants access to a product, version channel, and package prefix. It is neither a runtime license nor a tenant feature, and the module is not itself a NuGet server or credential provider.
Code map
| Project | Primary responsibility |
|---|---|
BitzOrcas.Platform.CommercialDistribution.Contracts | Public contracts, ports, DTOs, and events |
BitzOrcas.Platform.CommercialDistribution.Domain | Aggregates, invariants, and pure domain rules |
BitzOrcas.Platform.CommercialDistribution.Application | Commands, queries, handlers, and application policy |
BitzOrcas.Platform.CommercialDistribution.Infrastructure | Persistence, connectors, and framework adapters |
Source location: src/Platform/CommercialDistribution
Core use cases
GrantPackageEntitlementCommandRevokePackageEntitlementCommandListPackageEntitlementsQueryGetPackageEntitlementQueryIssueFeedTokenCommandValidateFeedTokenCommand
When reading a use case, start with its request contract and permission declaration, follow how the handler coordinates an aggregate or port, then inspect post-commit events, cache changes, and audit.
Key models and contracts
| Key type | What to inspect |
|---|---|
PackageEntitlement | Entitlement aggregate and invariants |
PackageEntitlementSummary | Management read model |
IPackageEntitlementCommandStore | Aggregate persistence port |
ICommercialFeedTokenProtector | Credential protection seam |
Use-case boundary example
| Field | Value |
|---|---|
| Representative contract | ValidateFeedTokenCommand |
| Result | Result<FeedTokenValidationResult> |
| Review focus | Treat introspection as a fail-closed gateway contract: reveal no failure reason and always re-query current entitlements after validating the token envelope. |
Pseudocode
// ① Any envelope or username failure becomes the same inactive result.if (!tokenProtector.TryUnprotect(command.Token, out var payload) || payload is null || payload.ExpiresAt <= clock.UtcNow || command.UserName != $"feed:{payload.CustomerId}") return FeedTokenValidationResult.Inactive;// ② Re-query current rights so revocation beats an unexpired token.var rights = await entitlements.FindByCustomerAsync( payload.TenantId, payload.CustomerId, payload.ProductId, cancellationToken);// ③ A read-store failure also closes access without disclosing internals.if (rights.IsFailure) return FeedTokenValidationResult.Inactive;// ④ Package prefixes and version channels aggregate from the current entitlement rows.return rights.Value.Count == 0 ? FeedTokenValidationResult.Inactive : new FeedTokenValidationResult(true, payload.CustomerId, payload.ProductId, payload.ExpiresAt, rights.Value.Select(item => item.PackagePrefix) .Distinct(StringComparer.Ordinal) .OrderBy(value => value, StringComparer.Ordinal).ToArray(), rights.Value.Select(item => item.VersionChannel) .Distinct(StringComparer.Ordinal) .OrderBy(value => value, StringComparer.Ordinal).ToArray());Integration relationships
A feed gateway calls the unauthenticated introspection endpoint behind its own network boundary. The endpoint validates the protected payload and then reloads active entitlements so a revocation takes effect without waiting for token expiry.
This page describes the module boundary. See private feeds and customer authentication for repository, gateway, client authentication, and revocation operations, and license issuance for the separation between package rights and runtime licenses.
Cross-module collaboration follows these directions:
- Callers depend on this module’s public Contracts or a narrow port.
- State changes propagate through versioned integration events.
- Database, messaging, cache, file, and vendor SDK details stay in Infrastructure.
- The composition root registers fail-closed defaults before production adapters.
Security, tenancy, and privacy
Management operations require commercial-distribution permissions and the default-off feed-management feature. Token tampering, expiry, username mismatch, entitlement revocation, and store failure all return the same inactive response without disclosing the reason.
Every tenant-scoped write persists TenantId, and list/detail paths share one authorization boundary. Logs, events, and audit retain only the minimum fields needed for diagnosis.
Failure semantics, idempotency, and observability
Grant is idempotent for the same entitlement ID and scope and conflicts on a changed scope. Revocation is irreversible. QueryShape supplies ORM-neutral list/detail reads, while Data Protection protects token integrity and confidentiality.
| Situation | Required behavior |
|---|---|
| Validation, not found, conflict, or forbidden | Return typed Result/Error data and let the shared mapper produce Problem Details |
| Adapter or critical state unavailable | Fail closed for security/high-value work; mark read-only degradation explicitly |
| Duplicate request or event | Use a stable idempotency key with a database uniqueness constraint as the final guard |
| External timeout | Propagate cancellation, record redacted diagnostics, and retry only safe operations with bounds |
Test matrix
| # | Required scenario | Suggested level |
|---|---|---|
| 1 | idempotent grant and conflicting replay | unit + integration |
| 2 | revoke then introspect an unexpired token | integration/contract |
| 3 | tamper, expiry, username mismatch, and store outage | integration/contract |
| 4 | EF/SqlSugar-free contracts and application layers | integration/contract |
Keep one universal red line under test: no identifier, cache key, event, or query condition from tenant A may let tenant B read or mutate data.
Source navigation and change checks
Run from the BitzOrcasVNext repository root:
# List public types and confirm that new contracts belong to the module boundary.rg -n "^public (sealed |abstract |static |partial )*(record|class|interface|enum)" src/Platform/CommercialDistribution -g '*.cs'
# Inspect cross-layer references; callers must not depend on another module's Infrastructure.rg -n "ProjectReference|PackageReference" src/Platform/CommercialDistribution -g '*.csproj'
# Find unfinished or elided implementation; expect no matches.rg -n "TODO|FIXME|// \.\.\." src/Platform/CommercialDistribution -g '*.cs'Extension and release checklist
- Is the new capability expressed through explicit Contracts, permissions, and a feature key?
- Can persistence, connectors, and providers be replaced through narrow adapters?
- Does production still resolve a Null/Unavailable default, and could health checks report a false green?
- Is there evidence for transaction, event, cache-invalidation, audit, and background-job failure/retry behavior?
- Did bilingual docs, diagrams, contract tests, and runbooks change with the code?
Back to module catalog · Module dependency diagram · Add a module guide