Skip to content

Merge develop into main: auth hardening and audit trails - #696

Merged
mathrmartins merged 771 commits into
mainfrom
develop
Oct 12, 2026
Merged

mathrmartins merged 771 commits into
mainfrom
develop

Conversation

@tolstenko

Copy link
Copy Markdown
Contributor

Summary

Promotes develop to main (166 commits, 396 files, +247,207/−1,046).

Dominant themes since #598:

Merge state

  • main is strictly behind develop (0 commits the other way) → clean merge, expected no conflicts.

Verification

Copilot AI balanced review requested due to automatic review settings October 5, 2026 16:57
@codacy-production

codacy-production Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 12 high · 30 medium · 58 minor

Alerts:
⚠ 100 issues (≤ 0 issues of at least minor severity)

Results:
100 new issues

Category Results
Compatibility 11 medium
11 high
BestPractice 9 medium
3 minor
1 high
Documentation 5 minor
ErrorProne 1 medium
Performance 2 medium
Comprehensibility 4 minor
Security 7 medium
CodeStyle 46 minor

View in Codacy

🟢 Metrics 12988 complexity · 190 duplication

Metric Results
Complexity 12988
Duplication 190

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Anonymous client authentication, CSV injection protection, and scheduled-export recovery have unresolved defects.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Promotes develop to main, delivering authentication hardening, expanded compliance auditing, generated-client updates, and operational tooling.

Changes:

  • Hardens sign-in, MFA, refresh-token lineage, revocation, and session handling.
  • Adds compliance evidence packaging, retention simulation, exports, and audit observability.
  • Updates generated clients, CI/security gates, branding, and local-development workflows.
File Description
scripts/​compliance/​requirements.txt Pins secure XML dependency.
scripts/​ci/​verify-repository-policy.sh Adds audit-validator tests.
scripts/​ci/​install-and-audit-pnpm.sh Delegates audit validation.
pnpm-lock.yaml Applies patched braces dependency.
patches/​README.md Documents security patch.
packages/​infrastructure/​client/​tests/​validation/​mfa-recovery-contract.test.ts Tests MFA client compatibility.
packages/​infrastructure/​client/​src/​runtime/​transport/​types.ts Adds blob response option.
packages/​infrastructure/​client/​src/​runtime/​transport/​fetch.ts Handles binary responses.
packages/​infrastructure/​client/​src/​generated/​modules/​learning-assessments-peer-reviews.gen.ts Updates peer-review contracts.
packages/​infrastructure/​client/​src/​generated/​modules/​auth.gen.ts Adds polymorphic sign-in client.
packages/​infrastructure/​client/​src/​generated/​index.ts Exports audit modules.
packages/​infrastructure/​client/​src/​generated/​.metadata.json Updates generation metadata.
packages/​infrastructure/​client/​scripts/​codegen/​modules.ts Generates binary module responses.
packages/​infrastructure/​client/​scripts/​codegen/​endpoints.ts Generates binary endpoint types.
packages/​infrastructure/​client/​scripts/​codegen/​binary-response.ts Detects binary OpenAPI responses.
package.json Adds development cleanup workflows and patch.
docs/​README.md Links development documentation.
docs/​plans/​quiz-grading-end-to-end/​README.md Updates grading-plan status.
docs/​modules/​compliance-audit-module.md Clarifies compliance scope.
docs/​brand/​social-post-example.svg Adds brand example.
docs/​architecture/​openapi-options.md Documents version reconciliation.
docs/​architecture/​api-versioning.md Documents OpenAPI naming.
docs/​api/​rate-limiting-configuration.md Clarifies trusted proxies.
docs/​api/​authorization-cache-deployment.md Documents cache observability.
deploy/​monitoring/​prometheus/​authorization-cache.yml Configures Prometheus.
deploy/​monitoring/​otel-collector-authorization-cache.yaml Configures telemetry collector.
deploy/​monitoring/​alertmanager/​authorization-cache.yml Configures local alerts.
compose.yaml Exposes telemetry configuration.
compose.coolify.yaml Adds deployment telemetry settings.
compose.authorization-observability.yaml Adds observability stack.
apps/​web/​tsconfig.learning.json Defines selective Learning build.
apps/​web/​src/​components/​brand/​game-guild-logo.tsx Uses new brand mark.
apps/​web/​src/​components/​brand/​game-guild-logo.test.tsx Tests brand component.
apps/​web/​public/​favicon.svg Updates favicon.
apps/​web/​public/​assets/​brand/​gameguild-mark.svg Adds brand mark.
apps/​web/​public/​assets/​brand/​gameguild-logo-stacked.svg Adds stacked logo.
apps/​web/​public/​assets/​brand/​gameguild-logo-reversed.svg Adds reversed logo.
apps/​web/​public/​assets/​brand/​gameguild-logo-monochrome.svg Adds monochrome logo.
apps/​web/​public/​assets/​brand/​gameguild-logo-horizontal.svg Adds horizontal logo.
apps/​web/​package.json Adds Learning build scripts.
apps/​web/​next.config.ts Configures isolated selective builds.
apps/​web/​.gitignore Ignores Learning build output.
apps/​api/​tests/​GameGuild.SharedKernel.UnitTests/​Events/​CommandOutcomeTests.cs Tests commit-on-failure behavior.
apps/​api/​tests/​GameGuild.SharedKernel.UnitTests/​Configuration/​ConfigurationOptionsTests.cs Updates MFA defaults.
apps/​api/​tests/​GameGuild.SharedKernel.UnitTests/​Configuration/​ConfigurationCoreOptionsTests.cs Updates MFA defaults.
apps/​api/​tests/​GameGuild.Learning.Assessments.UnitTests/​TasksAggregationTests.cs Tests fail-closed peer review.
apps/​api/​tests/​GameGuild.Identity.Users.UnitTests/​Entities/​UserCoverageBoostTests.cs Tests generated handles.
apps/​api/​tests/​GameGuild.Identity.Authorization.UnitTests/​Services/​DatabaseTenantSecurityVersionStoreTests.cs Tests batched version reads.
apps/​api/​tests/​GameGuild.Identity.Authorization.UnitTests/​DiExtensionsAndCachingTests.cs Tests cache thresholds.
apps/​api/​tests/​GameGuild.Identity.Authorization.PerformanceTests/​Program.cs Adds bulk benchmark.
apps/​api/​tests/​GameGuild.Identity.Authorization.PerformanceTests/​PermissionCacheEfDatabaseLookupBenchmarks.cs Enables benchmark pooling.
apps/​api/​tests/​GameGuild.Identity.Authorization.PerformanceTests/​PermissionCacheDatabaseLookupBenchmarks.cs Enables benchmark pooling.
apps/​api/​tests/​GameGuild.Identity.Authorization.PerformanceTests/​GameGuild.Identity.Authorization.PerformanceTests.csproj Adds authentication reference.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​Validators/​LocalSignUpCommandValidatorTests.cs Tests canonical usernames.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​Services/​OAuthAuthServiceTests.cs Tests OAuth attempt auditing.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​Models/​AuthenticationBranchModelTests.cs Updates expiry projection test.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​MaxCoverageTests5.cs Tests trusted IP handling.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​MaxCoverageTests4.cs Updates polymorphic mocks.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​MaxCoverageTests2.cs Updates session command dependencies.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​MaxCoverageTests.cs Updates revocation middleware tests.
apps/​api/​tests/​GameGuild.Identity.Authentication.UnitTests/​Controllers/​AuthControllerTests.cs Tests generic refresh denial.
apps/​api/​tests/​GameGuild.Compliance.Audit.UnitTests/​AuditTests.cs Tests tenant-storage destination.
apps/​api/​tests/​GameGuild.Audit.UnitTests/​Services/​ScheduledAuditExportBackgroundServiceTests.cs Tests scheduled worker.
apps/​api/​tests/​GameGuild.Audit.UnitTests/​Services/​AuditServiceTests.cs Tests activity buckets.
apps/​api/​tests/​GameGuild.Audit.UnitTests/​Services/​AuditExportTransportContractTests.cs Tests routes and CSV safety.
apps/​api/​tests/​GameGuild.Audit.UnitTests/​GameGuild.Audit.UnitTests.csproj Embeds legacy fixture.
apps/​api/​tests/​GameGuild.Audit.IntegrationTests/​AuditIntegrationTests.cs Tests audit CQRS registration.
apps/​api/​tests/​GameGuild.API.UnitTests/​Core/​UsernameModelContractTests.cs Tests username persistence contract.
apps/​api/​tests/​GameGuild.API.UnitTests/​Core/​Extensions/​SecurityServiceCollectionExtensionsTests.cs Tests custom API-key resolver.
apps/​api/​tests/​GameGuild.API.IntegrationTests/​GameGuild.API.IntegrationTests.csproj Adds mocks and linked fixtures.
apps/​api/​tests/​GameGuild.API.IntegrationTests/​DataMaskingPostgreSqlIntegrationTests.cs Isolates masking fixtures.
apps/​api/​tests/​GameGuild.API.IntegrationTests/​CommonOpenApiIntegrationTests.cs Verifies revocation responses.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Events/​UseCaseOperationContext.cs Adds commit-on-failure outcome.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​README.md Documents BCrypt cost.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​PresentationLayer/​RateLimiting/​RateLimitingOptions.cs Documents forwarded schemes.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​PresentationLayer/​Authorization/​AuthorizationOptions.cs Adds claim transformations.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​PresentationLayer/​Authorization/​AuthorizationClaimTransformationOptions.cs Defines claim mappings.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​PresentationLayer/​Authorization/​AuthorizationCacheOptions.cs Adds cache warning thresholds.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​PresentationLayer/​Authentication/​AuthenticationPasswordPolicySettings.cs Configures BCrypt cost.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​PresentationLayer/​Authentication/​AuthenticationOptions.cs Adds API-key resolver.
apps/​api/​Source/​Modules/​GameGuild.SharedKernel/​Configuration/​ApplicationLayer/​MfaOptions.cs Lengthens recovery codes.
apps/​api/​Source/​Modules/​GameGuild.Learning.Assessments/​Services/​IPeerReviewAssignmentService.cs Revises peer-review boundary.
apps/​api/​Source/​Modules/​GameGuild.Learning.Assessments/​Controllers/​PeerReviewsController.cs Adds participant access check.
apps/​api/​Source/​Modules/​GameGuild.Identity.Users/​ValueObjects/​UsernameSlug.cs Implements canonical handles.
apps/​api/​Source/​Modules/​GameGuild.Identity.Users/​ValueObjects/​SignInIdentifierType.cs Defines sign-in identifier kinds.
apps/​api/​Source/​Modules/​GameGuild.Identity.Users/​Abstractions/​IUserRepository.cs Adds candidate lookup API.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authorization/​Services/​DatabaseTenantSecurityVersionStore.cs Batches security-version reads.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authorization/​Caching/​ICacheMetricsService.cs Makes metrics configurable.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authorization/​Caching/​CachingServiceExtensions.cs Registers metrics monitor.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authorization/​Abstractions/​ITenantSecurityVersionStore.cs Adds bulk version contract.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Validators/​LocalSignUpCommandValidator.cs Validates canonical handles.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Services/​Web3AuthService.cs Audits Web3 attempts.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Services/​PasswordHistoryPolicy.cs Supports legacy long hashes.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Services/​MfaService.cs Preserves cancellation.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Services/​LongPasswordHash.cs Adds long-password hashing.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Services/​BackupCodeSet.cs Adds recovery metadata.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Repositories/​UserSessionRepository.cs Retains referenced sessions.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Repositories/​UserMfaConfigurationRepository.cs Reloads concurrency conflicts.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Handlers/​RevokeTokenHandler.cs Enforces token ownership.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Handlers/​RefreshTokenHandler.cs Preserves containment denial.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Extensions/​DependencyInjection.cs Registers polymorphic handler.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Entities/​UserMfaConfiguration.cs Adds enrollment deadline.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Entities/​RefreshToken.cs Adds token lineage.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​DTOs/​RefreshTokenContainmentDenial.cs Defines committed denial.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​DTOs/​PolymorphicSignInRequest.cs Defines polymorphic request.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​DTOs/​MfaConfigurationResponse.cs Exposes issued-code count.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​DTOs/​LocalSignInRequest.cs Adds internal account resolution.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Data/​Extensions/​DependencyInjection.cs Registers lineage services.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Data/​Configurations/​UserMfaConfigurationConfiguration.cs Adds MFA concurrency tokens.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Data/​Configurations/​RefreshTokenConfiguration.cs Configures lineage foreign keys.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Controllers/​SessionController.cs Revokes all user tokens.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Controllers/​MfaController.cs Reports recovery history accurately.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Controllers/​AuthController.cs Adds polymorphic sign-in and denials.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Commands/​RevokeAllUserTokensCommand.cs Implements logout-everywhere.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Commands/​PolymorphicSignInCommand.cs Makes sign-in transactional command.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Architecture/​AuthenticationUseCaseEventContracts.cs Adds auth event contracts.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Abstractions/​IVersionedUserTokenRevocationService.cs Defines versioned revocation.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Abstractions/​IRefreshTokenLineageRepository.cs Defines lineage persistence.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Abstractions/​IPasswordHasher.cs Documents hashing behavior.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Abstractions/​IAuthenticationAuditEventSink.cs Defines auth audit events.
apps/​api/​Source/​Modules/​GameGuild.Identity.Authentication/​Abstractions/​IAuthAttemptService.cs Adds provider-aware attempts.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​ScheduledAuditExportBackgroundService.cs Runs scheduled exports.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​CompliancePackagingEncoding.cs Adds canonical JSON handling.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​CentralAuthenticationAuditEventSink.cs Bridges authentication audits.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​AuditRetentionSimulationRepository.cs Persists retention simulations.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​AuditLogQueryService.cs Records audit-data reads.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​AuditDataAccessRecorder.cs Persists read observations.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​AuditCsvExporter.cs Prevents spreadsheet injection.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​AuditChainEvidenceVerifier.cs Verifies signed chains.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Services/​AuditActionTypeExportFormatter.cs Formats action-type exports.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Resources/​FedRamp2026/​fedramp-vulnerability-detail-report-schema-2026-06-24.json Adds FedRAMP schema.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Resources/​FedRamp2026/​fedramp-significant-change-notifications-schema-2026-06-24.json Adds FedRAMP schema.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Resources/​FedRamp2026/​fedramp-historical-ver-activity-schema-2026-06-24.json Adds FedRAMP schema.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Resources/​FedRamp2026/​fedramp-accepted-vulnerability-info-schema-2026-06-24.json Adds FedRAMP schema.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Queries/​AuditActionTypeQueries.cs Adds action-type queries.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Models/​AuditActivityBucketSize.cs Defines activity buckets.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​GameGuild.Compliance.Audit.csproj Adds schemas and validator.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Extensions/​AuditModule.cs Registers audit services.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Entities/​TamperEvidentAuditLog.cs Normalizes timestamp precision.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Entities/​CompliancePackagingEntities.cs Adds evidence entities.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Entities/​AuditRetentionEntities.cs Adds retention entities.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​DTOs/​AuditExportRequest.cs Adds export webhooks.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​DTOs/​AuditDateRangeSearchResponse.cs Adds date-search response.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​DTOs/​AuditActionTypeSearchResponse.cs Adds action-search responses.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Constants/​AuditActionTypes.cs Adds permission-change action.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Commands/​ScheduledAuditExportCommands.cs Adds scheduled-export commands.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Commands/​CompliancePackagingCommands.cs Adds packaging commands.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Commands/​AuditRetentionSimulationCommands.cs Adds simulation commands.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Commands/​AuditExportCommands.cs Limits exported audit metadata.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Architecture/​ScheduledAuditExportEventContracts.cs Adds export event contracts.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Architecture/​CompliancePackagingEventContracts.cs Adds packaging event contracts.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Architecture/​AuditRetentionSimulationEventContracts.cs Adds simulation event contracts.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Abstractions/​IAuditService.cs Adds activity aggregation.
apps/​api/​Source/​Modules/​GameGuild.Compliance.Audit/​Abstractions/​IAuditActionTypeSearchService.cs Defines search service.
apps/​api/​Source/​GameGuild.API/​Security/​AnonymousEndpointRegistry.cs Allowlists polymorphic sign-in.
apps/​api/​Source/​GameGuild.API/​Database/​PermissionAuditOptions.cs Defines permission audit hooks.
apps/​api/​Source/​GameGuild.API/​Database/​Migrations/​20261005013448_MfaRecoveryConcurrency.cs Adds enrollment expiry column.
apps/​api/​Source/​GameGuild.API/​Database/​Migrations/​20261003002747_AddAuditActionTypeSearchIndexes.cs Adds correlation index.
apps/​api/​Source/​GameGuild.API/​Core/​Setup/​PipelineExtensions.cs Adds bearer revocation middleware.
apps/​api/​Source/​GameGuild.API/​Core/​Setup/​InfrastructureLayerExtensions.cs Registers compliance adapters.
apps/​api/​Source/​GameGuild.API/​Core/​Security/​ConfiguredAuthorizationClaimsTransformation.cs Applies claim mappings.
apps/​api/​Source/​GameGuild.API/​Core/​OpenApi/​DeclaredResponseContentTypesOperationFilter.cs Preserves response media types.
apps/​api/​Source/​GameGuild.API/​Core/​Extensions/​RateLimitingServiceCollectionExtensions.cs Restricts forwarded headers.
apps/​api/​Source/​GameGuild.API/​Core/​Extensions/​OpenTelemetryExtensions.cs Validates telemetry exporters.
apps/​api/​Source/​GameGuild.API/​Core/​Extensions/​OpenApiExtensions.cs Reconciles versioned documents.
apps/​api/​Source/​GameGuild.API/​Core/​Eventing/​UseCaseOperationBehavior.cs Commits containment and audits.
apps/​api/​Source/​GameGuild.API/​Core/​Compliance/​AuditScheduledExportStorageAdapter.cs Stores tenant audit exports.
apps/​api/​Source/​GameGuild.API/​appsettings.json Adds audit and MFA defaults.
apps/​api/​docs/​audit-date-range-search.md Documents date-range search.
apps/​api/​docs/​audit-action-type-search.md Documents action-type search.
apps/​api/​docs/​api/​authorization-options-configuration.md Documents authorization options.
apps/​api/​Dockerfile Removes permissive forwarded headers.
apps/​api/​Directory.Packages.props Pins JSON Schema package.
.trivyignore Records patched CVE exception.
.github/​workflows/​pr-verify.yml Adds OpenAPI HTTP verification.
.gitattributes Normalizes patch files.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

mathrmartins and others added 21 commits October 9, 2026 11:54
… lifecycle

- AdaptiveAnomalyDetectionService: online statistical learning (EWMA baselines
  per subject over hour-of-day circular stats, IP information content, log
  inter-arrival cadence) with z-score deviation, configurable sensitivity,
  cold-start abstention, persisted per-subject state and drift handling
  (exponential decay + stale baseline reset). Wired alongside the fixed-weight
  heuristic scorer in LoginAttemptAnalysisService; fails open.
- Security alert lifecycle now reaches resolution: POST
  /api/audit/security-events/alerts/{id}:resolve with actor-derived resolvedBy,
  resolvedAt and note, transition validation (409 on double resolve / ack after
  resolve), and an audited SecurityAlertResolved security event persisted in
  the same save.
- Removed the dead AuditAnomaly/IAnomalyDetectionService surface (never wired,
  never mapped).
- Migration: adaptivebehaviorbaseline table + SecurityAlerts resolution columns.
- Tests: adaptive service (learned deviation, cold start, drift, sensitivity,
  persistence, fail-open), login-analysis wiring, resolve lifecycle unit +
  PostgreSQL HTTP tests incl. auth denials.

Refs #200
…1008

feat(authz): graph-based permission visualization and impact analysis (issue #334)
…359)

User-context logging criterion for #359: evaluation records now carry the
acting user's role attribution alongside user and tenant.

- PermissionEvaluationRecord gains Roles (IReadOnlyCollection<string>):
  resolver role attribution per the #330 contract - direct roles by name,
  hierarchy-inherited roles marked ' (inherited)'; never null; empty for
  anonymous actors; capped at 32 distinct entries with a '+N more roles'
  overflow marker (PermissionEvaluationRoles helper).
- REST [Authorize] decorator resolves attribution once per check via
  IRbacPermissionResolver and writes Roles into the audit metadata.
- GraphQL projectAuthorize directive resolves attribution (null-safe,
  cached per request) and records it on every Allow/Deny/Error record.
- Durable audit sink serializes normalized Roles into metadata; the log
  service funnel normalizes roles before any sink or monitoring event,
  and the monitoring event now includes roles=[...].
- PermissionEvaluationLogs table gains a required text[] Roles column
  (migration AddPermissionEvaluationRoles359; schema.dbml regenerated).

Tests: roles on allow and deny (REST + GraphQL), inherited-role marking,
32-entry cap with overflow marker, empty-for-anonymous, entity round-trip.
…th2 client-credentials, veto-only layer)

Adds the externalized authorization-decision seam for issue #146:

- IExternalAuthorizationDecisionProvider abstraction (Allow/Deny/NotApplicable
  + reasons) in Identity.Authorization Abstractions, platform domain-free.
- ExternalAuthorizationOptions under Authorization:ExternalDecision: disabled
  by default (zero-overhead no-op), HTTPS-only endpoints, OAuth2
  client-credentials token endpoint + client id/secret, timeout, decision
  cache TTL, FailMode Observe|Enforce (Enforce default: unavailable external
  decisions deny, fail-closed). Enabled config is validated on host startup.
- HttpExternalAuthorizationDecisionProvider: token acquisition with
  single-flight caching, JSON decision requests, tolerant response mapping,
  never-throw contract, short-TTL decision cache.
- EffectivePermissionResolverService consults the provider for every
  locally-allowed permission after all local layers and evaluation
  extensions, immediately before DENY-WINS: external deny overrides a local
  allow; external allow/not-applicable/missing never grants nor overrides a
  local deny (veto-only, grants stay local); static wildcard non-deniable;
  throwing provider fails the permission closed.
- docs: effective-permission-resolution.md layer 9 + precedence table +
  config/fail modes + deferred SAML authorization-assertion integration;
  authorization-options-configuration.md cross-reference; disabled section
  in appsettings.json.
- tests: provider wire/fail-mode/cache units, resolver-chain precedence
  integration, options validation and DI wiring (HttpMessageHandler double,
  no real outbound calls).

Fixes #146
…esolver tests

Resource-scoped resolution exercises layer 8 (IResourcePermissionService);
without a setup Moq returns null and the resolver foreach throws NRE.
Returns an empty grant list like the base resolver test suite does.
…ser/tenant Guid logging

CodeQL taints the user id from the SystemAccountId comparison into the
established audit logging of user/tenant Guids (same-rule alerts 917-919 are
already open on develop for this file). The six PR-flagged sinks are log
statements; suppress with inline codeql comments rather than dropping audit
context.
Five eslint-disable entries no longer match any reported violation
after merging origin/develop (capability-based course checks replaced
the braced legacy guards they had suppressed). Re-ran with
--prune-suppressions; lint and the suppression budget ratchet pass.
…ipe/stripe-js-10.0.0

build(deps): bump @stripe/stripe-js from 9.16.0 to 10.0.0
Bumps [@stripe/react-stripe-js](https://github.com/stripe/react-stripe-js) from 6.10.0 to 7.0.0.
- [Release notes](https://github.com/stripe/react-stripe-js/releases)
- [Changelog](https://github.com/stripe/react-stripe-js/blob/master/CHANGELOG.md)
- [Commits](stripe/react-stripe-js@v6.10.0...v7.0.0)

---
updated-dependencies:
- dependency-name: "@stripe/react-stripe-js"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…ipe/react-stripe-js-7.0.0

build(deps): bump @stripe/react-stripe-js from 6.10.0 to 7.0.0
… the flagged sink

CodeQL inline suppression requires a standalone // codeql[rule-id] comment on
the line immediately before the alert location; trailing comments are not
honored.
mathrmartins and others added 5 commits October 11, 2026 06:24
…ailover-20261011

feat(payments): fail-closed gateway selection with PayPal adapter and failover
…an-history-20261010

Configure verified-email sign-in policy and qualify durable security owner notification access. Matching-head hosted acceptance: 8921 distinct cases passed, 0 failed/skipped; 10 policy cases, 18 account/email HTTP cases, 8 owner access cases and 27 security event pipeline/handler/publisher cases included. 15 OpenAPI cases rerun and verified as a subset. All required security/quality/CLA gates passed. Refs #240, #270.
mathrmartins pushed a commit that referenced this pull request Oct 11, 2026
fix(security): resolve PR #696 GHAS findings (CodeQL, Copilot, Codacy)
mathrmartins and others added 3 commits October 11, 2026 15:13
…ry lifecycle (#806)

* feat(payments): automated retry queue with attempt history and recovery lifecycle

Implements the issue #415 automation on top of the #782 config layer:

- PaymentRetryQueueBackgroundService (opt-in via Payments:RetryQueue:Enabled,
  hourly by default, MaxBatchSize 100) polling the existing
  PaymentRepository.GetDueForRetryAsync and dispatching through the existing
  RetryPaymentCommand path with per-item error isolation; queue dispatches run
  as a well-known system actor; OTel metrics per the billing webhook pattern.
- PaymentRetryPolicyResolver: gateway failure-code classification into
  hard-decline (non-retriable immediately), insufficient_funds 3d/7d/14d,
  do_not_honor 1d/7d, processing-error 0/1/2/3d ladders, and the configurable
  Payments:Retry exponential backoff with jitter for everything else; injected
  at the handler layer, entity stays policy-free.
- PauseForMethodChangeAsync/ResumeAfterMethodChangeAsync around the
  subscription-checkout method swap: queued retries pause while the instrument
  changes and resume re-armed against the confirmed method.
- Recovery lifecycle: retry exhaustion suspends the subscription through the
  standard lifecycle with payment_retry_exhausted reason + final-notice dunning
  event; a successful recovery retry reactivates it, guarded by the recorded
  suspension reason.
- PaymentRetryAttempt history entity + migration + indexes, written by the
  retry path, exposed via GET payments/{paymentId}:retry-attempts.
- Default retry budget raised to 4 per the issue; refund endpoint now coalesces
  a null amount to the remaining refundable balance instead of 0.

Refs #415

* fix(payments): merge-develop reconciliation — snapshot union, fraud-gate ctor wiring, merged-tree OpenAPI client regen

- Model snapshot merged as the union of both sides (retry-queue additions onto the
  fraud/payment-methods develop state); validated with a throwaway ef migration
  that generated empty Up/Down (model == snapshot).
- Fraud-gate test fixtures wired for the retry-policy and dunning ctor params.
- Typed client regenerated from the merged tree swagger (retry-attempts endpoint
  + develop's fraud/payment-method surfaces).

Refs #415

* chore(openapi): regenerate client on merged tree (pricing engine + retry queue)

Refs #415

* test(payments): deserialize retry-attempt outcome enum as string in integration test

The API serializes enums as strings (JsonStringEnumConverter); the test-side
ReadFromJsonAsync needed the same converter to read the attempt outcome.

Refs #415

* fix(tests): restore Microsoft.AspNetCore.TestHost using accidentally renamed to TestServer

Refs #415

* test(payments): adapt retry-budget expectations to the issue-415 default of 4

- Exhaustion loops now iterate the payment's actual MaxRetries instead of the
  legacy hardcoded 3 (order-payment replacement tests, entity transition test).
- The insufficient-funds ladder expectation uses rung 2 (7d) for a first-retry
  failure, matching ladder indexing by retry count.
- The no-subscription lifecycle test builds a genuine subscription-less payment.
- The system-actor dispatch test uses a mutable accessor fake so the Set/restore
  of the actor context is actually observable.

Refs #415

* chore: retrigger PR verify workflow (synchronize event did not fire for 5e31ccb)

Refs #415

---------

Co-authored-by: closeout-agent <closeout-agent@gameguild.local>
… 4217 + features schema validation, compare handler, IsActive leak fix, list caching, delete guard, ViewAll gating (issue #416)

- Slug auto-generation: Slug optional on create/full-update; SubscriptionPlanSlugGenerator
  (diacritic-stripping, hyphen-collapsing, 50-char truncation, -2/-3 collision suffixes)
- Currency: create validator now enforces the payment-supported ISO 4217 allowlist
  (CurrencyCodes moved to the shared commerce contracts module)
- Validation holes closed: name 3-100, prices >= 0 on pricing PATCH + full update,
  limits >= 0, features JSON schema (flat object, scalar leaves, typed well-known keys),
  trial period 0-90 (TrialPeriodDays now settable via full update), full-update validator
  (previously none)
- CompareSubscriptionPlansQueryHandler: feature matrix over features JSON + limit columns;
  POST :compare no longer 500s with 'handler not registered'
- GetActiveAsync/GetFeaturedAsync now filter IsActive (public list leak)
- 5-minute sized-entry plan-list cache on active/featured queries, invalidated by every
  plan mutation command
- Delete guard: explicit active-subscription check with archive-instead error (FK backstop)
- ViewAll gating: seeing inactive plans now requires monetization:view-analytics
- Snapshot regression tests: plan repricing never touches existing subscription amounts,
  renewal quotes, or payment matching; archived plans keep subscriptions billable
@tolstenko

Copy link
Copy Markdown
Contributor Author

All required CI gates now green (PR Required Gate ✅ — API verify, Web verify, OpenAPI, Economy, Migration, Testing Lab all passing on head). CodeQL/Codacy report-only failures tracked separately: CodeQL deltas triaged (7 false positives dismissed with justifications, 1 real fix landed), Codacy issues being worked in #811/#813–#816.

@migmoroni @mathrmartins — this develop→main release PR is ready for your review. 2 approvals needed (branch protection: 2 approvers, last-push approval required — my last push was the CLA bot-allowlist + CodeQL triage commits).

mathrmartins and others added 17 commits October 11, 2026 19:22
…ft validation (#809)

* feat(billing): durable pricing-decision ledger entity, writer and read surface (issue #400)

- PricingDecisionLedgerEntry append-only table with full breakdown JSON (jsonb),
  correlation (order/line/payment/subscription/cycle/product), actor + tenant
  context, engine version stamp and unique idempotency key; migration + indexes
- IPricingDecisionLedgerWriter (GameGuild.Commerce contracts) + fail-closed
  transactional writer with Compliance.Audit publishing (PricingDecision.Recorded)
- Paginated filterable read API /api/v{v}/billing/pricing-discounts-ledger
  (SystemAdmin, matching sibling billing events surface)
- Effectiveness analytics: redemptions, discount vs baseline revenue, top promo
  codes and pricing rules by impact, stacking outcomes, drift health
- Drift validation command: recomputes sampled entries' final from breakdown,
  stamps Verified/Drifted

* test(billing): ledger writer, drift validation, repository, wiring and controller tests

- Writer: persist+replay+fail-closed+audit publishing (SQLite relational harness)
- Drift: tampered final flagged, consistent verified, unparseable skipped+drifted
- Repository: exact-code filter (no substring), tenant/order/subscription/window
  filters, effectiveness aggregation, sampling prefers never-validated
- Wiring: AddProductToOrder ledger entry + rejection fails closed; renewal quote
  records; ApplyPromoCodes records when correlation key exists, skips previews
- Controller: SystemAdmin policy, issue #400 route, query passthrough, defaults

* chore(client): regenerate OpenAPI client for the pricing-decision ledger surface (issue #400)

* fix(billing): count unparseable breakdowns as drifted; align ledger tests with Moq callback contracts

- Drift validation: unparseable breakdown entries increment DriftedCount
  (they were stamped Drifted but reported as neither verified nor drifted)
- Controller tests: Callback delegates typed as IRequest<TResponse> to match
  ISender.Send's signature (strict Moq validation rejected concrete types)
- Writer test: zero-quantity breakdown must bypass Empty()'s normalization
- Repository window test: deterministic timestamps for every seeded entry

* fix(subscriptions): suppress InMemory TransactionIgnoredWarning in renewal integration hosts — renewal quotes now append a pricing-decision ledger entry (issue #400) inside a DB transaction, and the in-memory provider rejects transactions by default, turning renewal POSTs into 500s (root cause of the 3 Economy-gate Subscriptions.IntegrationTests failures)
…k reconciliation (#810)

* feat(payments): refund entity lifecycle, approval workflow, dispute surface, webhook reconciliation and statistics

- Refund entity with Pending/Approved/Rejected/Processing/Completed/Failed
  lifecycle, retry-safe idempotency key unique per payment, and migration
  (also materializes payment_disputes + dispute_evidence tables and the
  GatewayDisputeId/EvidenceDueBy reconciliation columns)
- RefundProcessingService: one shared approval-aware core for the immediate,
  workflow and accept-dispute paths (revenue event + balancing ledger entry
  + audit trail emitted exactly once)
- RequestRefundCommand/ApproveRefundCommand/RejectRefundCommand with the
  payments:refunds:approve permission, configurable manual-approval threshold
  (Payments:Refunds) and approver email notifications
- DisputesController HTTP surface for the existing dispute CQRS stack plus
  accept/challenge endpoints
- StripeDisputeEventConsumer reconciles charge.dispute.* webhooks into local
  disputes; accepting a dispute records the refund without double-charging
  the gateway; IPaymentGateway gains dispute accept/challenge (Stripe impl)
- PartiallyRefunded payment status transition; refund/dispute statistics
  endpoints over the refund and dispute ledgers

References #419

* fix(payments): implement dispute members on the economy Stripe stub

* chore(client): regenerate API client with refunds and disputes surface

References #419

* fix(payments): reject accept-dispute when the amount exceeds the remaining refundable

Fails before closing the dispute at the gateway so an over-refund can never
be acknowledged remotely and then fail to record locally.

References #419

* fix(payments): repair six Payments unit-test failures — restore Stripe ctor null-guard contract (develop test demands ArgumentNullException on null intent factory), align refund approval tests with fail-closed tenant guards (seed actor tenant, queue before unstubbed gateway), expect PartiallyRefunded on partial refunds (issue #419), correct dispute resolution-hours expectation to creation-to-resolution average

* fix(migrations): union the model snapshot for retry attempts over the refund branch

The develop merge resolved ApplicationDbContextModelSnapshot with
develop's version, dropping the refund/dispute model; restoring the
branch snapshot dropped the retry-attempt model from #806. Hand-unify:
IsRetriable + Status,NextRetryAt index on Payment, the
PaymentRetryAttempt entity and its relationship.

---------

Co-authored-by: Matheus Martins <matheusmartins@users.noreply.github.com>
…-20261011

feat(subscriptions): plan management hardening
…nnot outlive expiring grants (#817)

* fix(authz): bulk permission cache expiry envelope (fail-closed)

Cached bulk permission-check decisions previously stored raw booleans with
the configured cache TTL and no grant-expiry boundary, so a cached allow
could keep serving after its grant's ExpiresAt passed (no mutation, no
security-version bump, no invalidation) until TTL eviction.

- Cached bulk values are now CachedPermissionDecision records carrying
  EffectiveUntilUtc: the earliest ExpiresAt among grants that conveyed or
  denied the requested permission for that decision (min across
  participating grants).
- Writes clamp each entry's TTL to the floored remaining grant lifetime
  via a new per-entry-TTL SetManyValuesAsync overload; the cache layers
  cap explicit TTLs at the configured TTLs so they only ever shorten.
- Reads treat an entry whose envelope boundary has passed as a miss (fail
  closed) and re-evaluate from the authoritative grants; a decision whose
  floored remaining lifetime is already zero is returned but never cached.
- HybridPermissionCache.SetAsyncCore now enforces the documented
  never-extends contract at L1 as well (previously only L2).
- Bulk cache key bumped bulk:v1 -> bulk:v2 so pre-envelope boolean entries
  are never read again.
- Docs: authorization-cache-configuration.md gains the bulk cache expiry
  envelope semantics and why load evidence stays in the BenchmarkDotNet
  performance projects.

References #309

* wip: salvage at quota wall 2026-10-12
…tes (#812)

* chore(cla): add Contributor License Agreement and cla-bot configuration

Adds CLA.md adapted from the Openinary CLA v1.0 template and a .clabot
config seeding the approved contributor list with all verified GitHub
accounts from full-branch git history (8 users). cla-bot GitHub App
still needs manual installation on gameguild-gg/gameguild.

* chore(client): regenerate OpenAPI client for permission-graph endpoints (issue #334)

* fix(test): parse DeletedAt JSON value instead of substring in soft-delete audit assertion

* fix(auth): collision-safe ACL cache keys and time-bound stale-permission guards

- Fingerprint the free-form resourceType/resourceId segments of ACL cache
  keys with SHA-256 (AclCacheKeys), mirroring the bulk permission key
  builder, so delimiter-bearing identifiers can no longer collide two
  distinct resources onto one cached access decision; invalidation
  patterns match the fingerprinted segments (#353).
- Clamp cached ACL decisions to the earliest effective grant expiration:
  the underlying evaluator reports ExpiresAt via the new
  ITimeBoundAccessControlListEvaluation capability, cached copies carry
  the boundary (CachedAclDecision), L1 and L2 writes clamp their TTL to
  it, and every serve re-checks it, which also covers L2-to-L1
  promotions and entries written before the boundary was tracked (#354).
- Add the post-query security-version recheck to the legacy
  GetAccessLevelAsync path, mirroring EvaluateAccessAsync: decisions
  evaluated across a concurrent mutation are retried instead of cached
  or returned (#354).
- Unit tests: delimiter non-collision for user/subject keys plus
  end-to-end cache separation and fingerprint-aware resource
  invalidation; stale-allow security tests with a controlled clock on
  both evaluation paths, TTL-clamp verification, lapsed L2 promotion,
  legacy in-flight version race, and never-caching expired decisions.

* fix(tests): drop ConfigureAwait(false) from ApiKeyLifecycle test methods (xUnit1030)

The Economy Release Gate builds the full solution with
TreatWarningsAsErrors=true; xUnit1030 flags ConfigureAwait(false) in test
methods because it can bypass xUnit parallelization limits. Omit it on the
two affected awaits; the private helper is untouched.

* docs(auth): document Redis authorization-cache backup and disaster recovery

- New section in authorization-cache-configuration.md: RDB-vs-AOF
  tradeoffs, versioned-key restore safety, BGSAVE backup procedure,
  database-authoritative restore steps, prefix-scoped flush (SCAN+UNLINK,
  never FLUSHALL), and failover monitoring guidance wired to the existing
  Prometheus/Alertmanager cache alert rules (#353).
- compose.yaml: keep AOF and add a periodic RDB snapshot policy
  (--save 900 1) with pointers to the documented procedures.
- Document the fingerprinted ACL key segments and the time-bound TTL
  clamp/serve-time recheck behavior.

* feat(audit): adaptive anomaly detection and security alert resolution lifecycle

- AdaptiveAnomalyDetectionService: online statistical learning (EWMA baselines
  per subject over hour-of-day circular stats, IP information content, log
  inter-arrival cadence) with z-score deviation, configurable sensitivity,
  cold-start abstention, persisted per-subject state and drift handling
  (exponential decay + stale baseline reset). Wired alongside the fixed-weight
  heuristic scorer in LoginAttemptAnalysisService; fails open.
- Security alert lifecycle now reaches resolution: POST
  /api/audit/security-events/alerts/{id}:resolve with actor-derived resolvedBy,
  resolvedAt and note, transition validation (409 on double resolve / ack after
  resolve), and an audited SecurityAlertResolved security event persisted in
  the same save.
- Removed the dead AuditAnomaly/IAnomalyDetectionService surface (never wired,
  never mapped).
- Migration: adaptivebehaviorbaseline table + SecurityAlerts resolution columns.
- Tests: adaptive service (learned deviation, cold start, drift, sensitivity,
  persistence, fail-open), login-analysis wiring, resolve lifecycle unit +
  PostgreSQL HTTP tests incl. auth denials.

Refs #200

* fix(auth): capture role attribution in permission evaluation records (#359)

User-context logging criterion for #359: evaluation records now carry the
acting user's role attribution alongside user and tenant.

- PermissionEvaluationRecord gains Roles (IReadOnlyCollection<string>):
  resolver role attribution per the #330 contract - direct roles by name,
  hierarchy-inherited roles marked ' (inherited)'; never null; empty for
  anonymous actors; capped at 32 distinct entries with a '+N more roles'
  overflow marker (PermissionEvaluationRoles helper).
- REST [Authorize] decorator resolves attribution once per check via
  IRbacPermissionResolver and writes Roles into the audit metadata.
- GraphQL projectAuthorize directive resolves attribution (null-safe,
  cached per request) and records it on every Allow/Deny/Error record.
- Durable audit sink serializes normalized Roles into metadata; the log
  service funnel normalizes roles before any sink or monitoring event,
  and the monitoring event now includes roles=[...].
- PermissionEvaluationLogs table gains a required text[] Roles column
  (migration AddPermissionEvaluationRoles359; schema.dbml regenerated).

Tests: roles on allow and deny (REST + GraphQL), inherited-role marking,
32-entry cap with overflow marker, empty-for-anonymous, entity round-trip.

* fix(build): clear analyzer warnings in permission expiration and api key tests

* feat(authorization): external authorization-decision integration (OAuth2 client-credentials, veto-only layer)

Adds the externalized authorization-decision seam for issue #146:

- IExternalAuthorizationDecisionProvider abstraction (Allow/Deny/NotApplicable
  + reasons) in Identity.Authorization Abstractions, platform domain-free.
- ExternalAuthorizationOptions under Authorization:ExternalDecision: disabled
  by default (zero-overhead no-op), HTTPS-only endpoints, OAuth2
  client-credentials token endpoint + client id/secret, timeout, decision
  cache TTL, FailMode Observe|Enforce (Enforce default: unavailable external
  decisions deny, fail-closed). Enabled config is validated on host startup.
- HttpExternalAuthorizationDecisionProvider: token acquisition with
  single-flight caching, JSON decision requests, tolerant response mapping,
  never-throw contract, short-TTL decision cache.
- EffectivePermissionResolverService consults the provider for every
  locally-allowed permission after all local layers and evaluation
  extensions, immediately before DENY-WINS: external deny overrides a local
  allow; external allow/not-applicable/missing never grants nor overrides a
  local deny (veto-only, grants stay local); static wildcard non-deniable;
  throwing provider fails the permission closed.
- docs: effective-permission-resolution.md layer 9 + precedence table +
  config/fail modes + deferred SAML authorization-assertion integration;
  authorization-options-configuration.md cross-reference; disabled section
  in appsettings.json.
- tests: provider wire/fail-mode/cache units, resolver-chain precedence
  integration, options validation and DI wiring (HttpMessageHandler double,
  no real outbound calls).

Fixes #146

* test(authorization): mock resource-grant layer in external-decision resolver tests

Resource-scoped resolution exercises layer 8 (IResourcePermissionService);
without a setup Moq returns null and the resolver foreach throws NRE.
Returns an empty grant list like the base resolver test suite does.

* chore(authorization): suppress CodeQL clear-text false positives on user/tenant Guid logging

CodeQL taints the user id from the SystemAccountId comparison into the
established audit logging of user/tenant Guids (same-rule alerts 917-919 are
already open on develop for this file). The six PR-flagged sinks are log
statements; suppress with inline codeql comments rather than dropping audit
context.

* chore(lint): prune suppressions orphaned by develop merge

Five eslint-disable entries no longer match any reported violation
after merging origin/develop (capability-based course checks replaced
the braced legacy guards they had suppressed). Re-ran with
--prune-suppressions; lint and the suppression budget ratchet pass.

* fix(features): satisfy brace and parameter hygiene findings

* fix(security): document guarded outbound fetch sites (SSRF FP annotations)

* style(scripts): conform pnpm-process and QuickJS sync scripts to lint conventions

* chore(client): regenerate OpenAPI client from CI artifact to clear array-style drift

* test(payments): serialize Stripe intent creation tests against process-wide configuration

* build(deps): bump @stripe/react-stripe-js from 6.10.0 to 7.0.0

Bumps [@stripe/react-stripe-js](https://github.com/stripe/react-stripe-js) from 6.10.0 to 7.0.0.
- [Release notes](https://github.com/stripe/react-stripe-js/releases)
- [Changelog](https://github.com/stripe/react-stripe-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/react-stripe-js/compare/v6.10.0...v7.0.0)

---
updated-dependencies:
- dependency-name: "@stripe/react-stripe-js"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(authorization): place CodeQL suppressions on their own line above the flagged sink

CodeQL inline suppression requires a standalone // codeql[rule-id] comment on
the line immediately before the alert location; trailing comments are not
honored.

* refactor(auth): extract IApiKeyRepository for API-key credential persistence

Introduce IApiKeyRepository/ApiKeyRepository mirroring the sibling
credential repositories (WebAuthn, ExternalLogin, TrustedDevice,
RefreshToken) and move ApiKeyAuthenticationHandler plus the Create/
List/Revoke/Rotate command handlers off direct IApplicationDbContext
onto the repository.

- GetByKeyHashAsync returns a tracked entity so usage recording and
  lazy rotation finalization persist through the repository
- RotateAsync links the replacement key and transitions the old key
  (grace window or immediate revocation) in a single save
- Register scoped alongside the other credential repositories
- Repository persistence tests (in-memory EF, sibling pattern),
  repository-backed auth-handler tests, and existing lifecycle/
  coverage tests updated to the new seams

Fixes #266

* fix(web): drop unnecessary non-null assertions in feed announcement parsers

The type-aware eslint config merged via #698 flags match[N]! assertions in
post-card.tsx (added with the #741 blog embeds) as unnecessary — the
RegExpExecArray index access already resolves non-optional after the match
guard. Removed the assertions and pruned the orphaned baseline entries;
eslint, tsc and the lint budget pass.

* feat(auth): notify owners of suspicious sign-ins via NotificationService

Fire NotificationType.Security owner alerts (InApp always; Email per
INotificationPreferenceService; Urgent) when the authentication risk
pipeline confirms: high-risk step-up, brute force (including on failed
attempts and on eventual success), and impossible travel on a successful
sign-in. The Identity.Authentication module records redacted
SuspiciousLoginDetectedV1 durable events gated by
Authentication:SecurityNotifications:{Enabled,MinimumRiskLevel=High};
the host-side SuspiciousLoginAlertHandler consumes them within the inbox
transaction (RefreshTokenReplayAlertHandler pattern) and queues once-only
notifications keyed by the event id (referenceEntityId). TODO stubs in
AuthenticationFailedEventHandler and RefreshTokenEventHandler now point
at the durable security pipeline.

Fixes #247

* fix(auth): rename shadowed scope in suspicious-login integration test

* fix(auth): length-prefixed ACL cache keys, wire time-bound capability to the database evaluator

CI on #746 exposed two design flaws in the original fix:

1. SHA-256 fingerprinting the resourceType/resourceId segments made the raw
   identifiers unrecoverable from the key, so substring-based invalidation
   patterns (CacheInvalidationServiceTests.InvalidateResourceAsync_InvalidatesPattern)
   could no longer match. Replace the fingerprints with length-prefixed
   components ({length}:{value}): the encoding stays injective (delimiter-
   bearing pairs such as ("a:b","c") vs ("a","b:c") still never collide)
   while the raw values remain embedded, so wildcard/substring invalidation
   keeps matching. All invalidation pattern builders already flow through
   AclCacheKeys.BuildResourceSegment and pick up the encoding unchanged.
   New tests: raw identifiers survive into the InvalidateResourceAsync
   pattern even when they contain delimiters, and pattern invalidation still
   evicts delimiter-bearing entries without over-matching delimiter-adjacent
   or longer-id neighbors.

2. DatabaseAccessControlListService defined EvaluateAccessTimeBoundAsync but
   never declared ITimeBoundAccessControlListEvaluation, so the cached
   wrapper's capability check failed at runtime and cached decisions carried
   no expiry boundary: the TTL clamp and the serve-time revalidation never
   engaged and time-lapsed grants kept serving cached allows
   (CachedAclStalePermissionSecurityTests on both evaluation paths). Declare
   the interface so production ACL evaluations report their earliest
   effective grant expiration.

Also updates AuthorizationCoverageCompletionTests to the CacheAccessLevel
(CachedAclDecision, bool) signature introduced with the time-bound guards,
and the cache-configuration documentation to the new key format.

* fix(auth): seed unique usernames in suspicious-login integration tests

The five tests shared one display name, so UsernameSlug.Generate
produced the same username and the shared PostgreSQL fixture rejected
the second insert with IX_Users_Username. Pass an explicit per-test
username like the replay-alert tests do.

* feat(auth): remember-me persistent sessions across local, OAuth and Web3 sign-in (issue #265)

- nullable RememberMe on all sign-in DTOs (local, polymorphic, OAuth variants, Web3) flowing through commands to issuance
- new Jwt:PersistentRefreshTokenExpirationDays (default 30) alongside the standard lifetime; validated in JwtOptions/AuthenticationOptions/SessionPolicy and wired through JwtOptionsResolver and SecurityServiceCollectionExtensions
- single policy-aware RefreshTokenLifetimeResolver replaces the hardcoded AddDays(7)/fallback chains in LocalAuthService, OAuthAuthService, Web3AuthService and the legacy AuthenticationEndpoint
- persisted refresh-token rows now carry the caller-resolved deadline so persistent sessions survive rotation; rotation renews the originating duration instead of collapsing onto the standard lifetime
- web: remember-me checkbox on both sign-in forms; client forwards rememberMe to the API and applies a session-vs-persistent cookie storage policy
- tests: per-path expiry, rotation renewal, logout revocation, resolver matrix; regenerated OpenAPI client

* feat(auth): record and manage fine-grained OAuth scope grants (#250)

- ExternalLogin gains GrantedScopes (JSON array), ConsentedAt and
  ConsentVersion (OAuthConsentVersions.Current = 1); additive migration.
- OAuthService.ResolveAuthorizationScopes single-sources the exact scope
  list authorization URLs embed; link (Google/Discord), callback and
  sign-in auto-link paths persist it with a consent stamp, re-recording
  consent only when the scope set changed (first-consent preserved).
- HEAD /v1/auth/external-logins now also emits X-Granted-Scopes:
  'provider=iso-consent-timestamp|consent-version|url-encoded-scopes'.
- New GET /auth/external-logins/{provider}/link-preview (authorization-
  time consent screen data) and POST .../scopes:revoke (per-scope
  revocation, idempotent, audited via structured log; unlink remains the
  documented whole-provider full revocation).
- Typed client regenerated; web connected-accounts card shows the consent
  record (date, terms version, scope chips) with per-scope revoke wired
  to a server action; en-US/pt-BR strings added.
- Tests: persistence round-trip, callback/link recording, list shape,
  revocation (handler/controller/service/repository), header shape, and
  web consent-header parser unit tests.

* feat(billing): comprehensive webhook security validation (#393)

- Google Pay webhook processing: bearer-JWT (RS256) verification service,
  idempotent inbox service, command handler, configuration with startup
  key/tolerance validation
- Security regression fix: BillingWebhooksController is no longer
  class-level [AllowAnonymous]; only the four provider callbacks stay
  anonymous (per-action registry entries), while webhook event
  inspection, manual retry, and the new security surface require the
  SystemAdmin policy
- CIDR source-IP allowlist (IPv4/IPv6, fail-closed 403 when configured,
  disabled by default) enforced by WebhookSourceSecurityFilter before
  the payload is read
- Webhook-specific rate limiting policy (per-IP fixed window, default
  120/min) applied to all provider callbacks
- Security events (signature failures, allowlist rejections, replays,
  blocks) published to the Compliance.Audit SecurityEventPipeline with
  taxonomy entries and best-effort delivery
- Threshold-based suspicious-activity auto-blocking (config-gated,
  fail-open default)
- GET /api/v1/billing/webhooks/security admin monitoring summary
  (allowlist state, blocked sources, pipeline delivery health)
- Unit tests for every addition (CIDR math, allowlist, monitor, filter,
  publisher, verification service, Google Pay service, summary handler,
  controller authorization metadata, rate limiting policy, config
  validation, DI registration)

* chore(openapi): regenerate client for the billing webhook security endpoint

* fix(auth): record suspicious-login alerts outside the sign-in transaction

The step-up response is a business failure and a denied attempt throws,
so UseCaseOperationBehavior rolls back the request transaction in both
cases and an outbox row written through the ambient scope was lost. The
publisher now records through an independent service scope with its own
database context - the same pattern RefreshTokenLifecycleRecorder uses
for rejection audits - so the alert event commits regardless of the
sign-in outcome. Add a unit test that a missing producer registration
degrades to a logged no-op.

* fix(ci): exclude benchmark harnesses from affected API test selection

The affected-test selector's test-project pattern matched
*PerformanceTests directories, which are BenchmarkDotNet executables
(IsTestProject=false), not test suites. When a PR touched benchmark
sources (e.g. #746), 'dotnet test' produced no TRX evidence and the API
verify job failed with 'Missing test evidence'. Guard the selection and
pin the behavior with a regression test.

* fix(root): pnpm overrides

* fix(dev): dev-fast quickjs

* fix(api): data tests

* chore(client): regenerate typed client from merged develop surface

* test(security): adversarial deny-by-default validation suite for #327

Adds GameGuild.API.SecurityTests: HTTP-level adversarial scenarios against
the real host (production JWT pipeline, real PostgreSQL) covering privilege
escalation, tenant crossover, token forgery/tampering, unguarded-route
sweeps, grant-revoke TOCTOU races, post-revocation stale-allow attempts and
JIT/impersonation elevation seams. Ships the findings report at
docs/security/deny-by-default-adversarial-validation.md documenting the
exercise as an internal adversarial validation pass that an external
professional pentest supersedes.

* chore(openapi): regenerate client against merged develop

* chore(client): refresh OpenAPI client metadata after develop merge

* fix(ci): read pnpm security gate config from pnpm-workspace.yaml

931415eb49 moved the pnpm settings (patchedDependencies,
onlyBuiltDependencies) from root package.json to pnpm-workspace.yaml, but
the dependency security-gate tests still asserted against
manifest.pnpm.*, so every job's install verification failed with
'Cannot read properties of undefined (reading patchedDependencies)' and
killed Web/API/Economy/OpenAPI/Migration/Repository-policy checks on the
release PR. The gate now parses the canonical pnpm-workspace.yaml
sections (fail-closed on malformed entries) and the emception release
policy asserts the same pinned values there.

* fix(emception): re-pin toolchain lock hash after canonical curl overlay review

Commit 709267e345 updated toolchain/toolchain.lock.json (canonical curl
overlay + reviewed LLVM archive variant) without updating the pinned
toolchainLockHash, failing 'adapter identity matches the reviewed
toolchain lock and ABI' on every Emception-detected PR since Oct 7.
Recompute the hash from the committed lock (serializeToolchainLock
ordering) and update both pins (grading binding + server adapter).

* fix(audit): map acknowledge transition violations to 409 Conflict

Acknowledging a resolved alert threw SecurityAlertTransitionException,
which escaped as 500 because the endpoint only caught
UnauthorizedAccessException. Mirrors the resolve endpoint's handler and
declares the 409 response type.

* fix(ci): pull CI PostgreSQL from the public ECR Docker Hub mirror

The four database-backed PR verify jobs (API verify, Economy Release
Gate, Migration compatibility, OpenAPI client consistency) died on
Docker Hub anonymous pull rate limits for postgres:17-alpine — GitHub's
shared runner IP pool is throttled, and reruns lose the same dice roll.
Pull the identical official image from public.ecr.aws (verified
manifest) instead. Fixture expectations updated to match.

* fix(audit): remove stray brace from acknowledge handler

* feat(auth): sign-in compliance verification gate (issue #267)

Adds an allow/challenge/deny compliance hook to local sign-in:

- ISignInCompliancePolicy + SignInComplianceDecision/Outcome/Reasons and
  SignInComplianceContext in GameGuild.Identity.Authentication (domain-free);
  module registers a default allow-all policy via TryAdd.
- LocalAuthService evaluates the policy after credential validation and
  tenant-access checks, strictly before token generation or session creation.
  Deny -> AccessDeniedException (generic 403) + Authentication.ComplianceDenied
  audit; Challenge -> step-up SignInResponse + audited; evaluation failures
  fail closed only in Enforce mode.
- Authentication:Compliance:SignInGate config (Enabled=false default;
  Mode=Off/ChallengeOnly/Enforce); ChallengeOnly downgrades deny to step-up.
- Host adapter KycAndHoldSignInCompliancePolicy (GameGuild.API/Core/Compliance)
  composes IKycService with IComplianceHoldStore.IsActiveAsync: active hold or
  KYC Rejected/Suspended -> Deny; Pending/InProgress/Expired/missing or lapsed
  approval -> Challenge; else Allow.
- Audit events carry only coarse reason codes (no KYC detail leakage).
- Tests: policy matrix units (API.UnitTests) + LocalAuthService gate
  integration incl. deny-before-session and challenge-before-token ordering
  (Identity.Authentication.UnitTests).

* test(auth): finish client-certificate test fixes for #245

- Use X509CertificateLoader.LoadPkcs12 instead of the obsolete
  X509Certificate2(pfx) constructor (SYSLIB0057 path) in cert fixtures.
- Issue test client certificates via the CertificateRequest.Create(issuer)
  overload instead of (issuerName, copiedKey), which disposed the authority
  key before the issued certificate was exported.
- Replace cert Export(CertPem) (no such content type on this target) with
  PemEncoding-based PEM export in scheme registration tests.
- Add missing 'using Xunit;' (project convention: explicit per-file usings)
  and await-using/nullable annotations flagged during the develop merge.
- Merge latest develop (CI mirror/config fixes); no behavioral conflicts.

* test(auth): seed four brute-force failures so the request reaches the analysis

The lockout action filter (MaxFailedAttemptsPerHour=5) counts stored
failures before the sign-in action runs and short-circuits a generic
401, so seeding five failures prevented the action - and its owner
alert - from ever running. Seed four and let the request under test be
the fifth failure, which is exactly when production raises the alert:
the brute-force detector also counts the attempt the failing request
records.

* fix(auth): avoid nullable-annotation mismatch in compliance policy test mocks

Result<T>.Success infers TValue from the argument, which dropped the
nullable annotation and tripped CS8620 through Moq's Returns/ReturnsAsync.
Use Result.Success<T?>(...) with Task.FromResult so the mock matches the
Task<Result<UserKycVerification?>> contract warning-free.

* test(audit): remove orphaned AuditAnomaly tests along with the deleted entity

The adaptive-detection PR removed the dead AuditAnomaly entity; its unit
tests and enum-count assertions referenced the deleted type and failed to
compile. Anomaly detection is covered by the adaptive detection service
and the durable security-event pipeline tests.

* fix(auth): repair PR CI — cert fixture validity race, eager PEM fail-closed, OpenAPI client drift

- Kestrel fixture / test factory: derive issued-leaf validity from the
  issuer certificate's own NotBefore/NotAfter. Computing leaf bounds from
  UtcNow at test time races CA creation by a second, making
  CertificateRequest.Create throw 'notAfter is later than
  issuerCertificate.NotAfter' (4 Kestrel integration failures in CI).
- AddClientCertificateAuthentication(settings): parse the CA allowlist
  eagerly so an unparseable PEM entry fails closed at registration time,
  matching the documented contract and the fail-closed registration test
  (previously the parse ran inside the deferred options lambda and never
  threw during SetupAuthentication).
- Regenerate OpenAPI typed client for CredentialType.Certificate and the
  /v1/oauth/token certificate-auth doc note (applies the exact drift the
  CI generator reported for this tree).

* fix(ci): probe for free Testing Lab ports and mirror its PostgreSQL image

The Testing Lab browser E2E picked one random port per band in
42000-44999, which sits inside Linux's default ephemeral range
(32768-60999); when the runner's own outbound socket held the candidate
the run aborted with 'PostgreSQL port NNNNN is already in use' and
failed the required gate (observed on the release PR). Probe up to ten
candidates per band before failing. Also pull its disposable postgres
from the public ECR mirror like every other CI database, dodging the
Docker Hub anonymous rate limit.

* fix(security): remediate findings from adversarial deny-by-default validation

- F-2 (critical): JIT elevation approve/deny/revoke/request handlers now derive
  the acting identity from IActorContextAccessor and reject body-supplied
  identities; approval additionally requires system- or same-tenant-admin
  authority (tenant from the stored elevation, never the body). Closes a
  self-approval-with-spoofed-reviewer privilege escalation found by the
  adversarial suite (unit tests: JitElevationActorIdentityGuardTests).
- F-3 (high): map PermissionAuditLog (PermissionAuditLogConfiguration) and add
  migration AddPermissionAuditLog — the audited-permission-mutation invariant
  previously threw against a real database because the entity was never mapped
  and the table never existed.
- harness: HS512 forge key padded to 512 bits; mutation endpoints probed with
  their real verb (405 masked the auth layer); delegated-admin probe body
  carries the validator-required allowedOperations; denial diagnostics include
  a response snippet.

* fix(auth): register allow-all compliance policy by instance factory

AllowAllSignInCompliancePolicy intentionally has a private constructor
(stateless shared instance), which fails container scope validation when
registered by implementation type — the CI host boots with ValidateOnBuild
and could not construct it. Register the shared Instance through a factory
instead.

* test(auth): make the brute-force alert test self-diagnosing

The suspicious-login PostgreSQL suite cannot run under the local build
gate, so embed the pipeline's own diagnostics in the failure message:
response body (distinguishes the lockout filter's 401 from the sign-in
throw path), stored attempts, outbox rows for the user aggregate, and
host log output captured by a recording logger provider.

* test(security): probe mutation endpoints with contract-correct bodies

TenantId is a value object: its JSON shape is { value: <guid> }. The flat
GUID string died in model binding (400) before the authorization guard was
exercised, so the self-grant and cross-tenant grant probes were testing
nothing about authorization.

* chore(client): refresh OpenAPI client metadata after develop merge

* fix(billing): set up GetBlockedSources stub in pipeline-failure summary test

Loose Moq mocks return null for IReadOnlyList<T> results; the handler's
summary projection then failed with ArgumentNullException instead of
exercising the intended pipeline-read-failure tolerance path. Stub the
monitor explicitly, matching the other summary handler tests.

* test(auth): bind Kestrel test host to 127.0.0.1:0 instead of ListenLocalhost(0)

ListenLocalhost cannot allocate a dynamic port (it would have to bind the
IPv4 and IPv6 loopback with a single port), so all four mTLS integration
tests failed at host startup in CI with InvalidOperationException. Bind
IPAddress.Loopback explicitly; the https URL is still taken from app.Urls.

* feat(auth): deliver permission-expiration alert notifications and docs (issue #331)

PermissionExpirationNotification was published by the worker but had zero
INotificationHandler consumers, so expiring grants produced no user-visible
alert. Add PermissionExpirationAlertHandler in the API composition root
(same placement and pattern as RefreshTokenReplayAlertHandler): resolves the
recipient via IUserRepository, queues an in-app Notification plus an email
when the user has an address, maps Expired -> Urgent / Upcoming -> Normal
priority, and honors per-channel notification preferences. Delivery is gated
on Authorization:PermissionExpiration:Enabled; queueing failures are logged,
not thrown (the publisher stamps dedup metadata before dispatch and has no
redelivery mechanism).

Also add docs/auth/permission-expiration.md covering grant-time defaults,
per-type overrides, wildcard precedence, worker cadence and idempotency,
notification delivery, administrative endpoints, remediation flows and best
practices, following the monetization-permissions.md conventions.

* test(auth): add PostgreSQL HTTP integration tests for monetization permission gates (issue #346)

The monetization gates (monetization:view-analytics, monetization:configure,
monetization:monetize) were enforced and unit-tested, but no integration test
exercised them end-to-end. Add MonetizationPermissionHttpTests against the
real HTTP pipeline and PostgreSQL:

- ViewAnalytics (GET /api/metrics/product): 401 anonymous, 403 without the
  grant, 200 with it, cross-tenant grants fail closed, SystemAdmin bypass.
- Configure (POST /v1/subscription-plans): 403 with the wrong monetization
  permission and with an expired grant, 201 with the grant (plan persisted),
  SystemAdmin bypass.
- Monetize (PUT /v1/products/{id}/pricing): 403 without products:pricing:manage
  at the endpoint gate, fail-closed denial with no persistence when only the
  endpoint permission is present (the command-layer monetization:monetize gate
  refuses the dispatch), 200 with both gates satisfied, SystemAdmin bypass.
- Decision audit: PermissionOperationType.Check rows are asserted for granted
  and denied endpoint-level decisions.

Document the new suite in docs/auth/monetization-permissions.md.

* fix(auth): keep adaptive anomaly in cold start until the baseline minimum

Cold start ended after the first persisted observation instead of at
MinimumObservations, so early assessments were scored against a
one-sample learned model and reported LearnedBaseline. The assessment
now stays on the cold-start fallback across the whole learning window
while observations keep accumulating. Regenerates the OpenAPI client
from the CI-captured swagger for the acknowledge 409 declaration.

* chore: re-trigger PR verification workflow

* fix(auth): align legacy sign-up handler with develop signature and cover remember-me in web tests

- the develop merge left SignUp with an IConfiguration parameter that develop
  had removed (reflection-based handler test expects the 4-parameter shape);
  surface the module's policy-resolved RefreshTokenExpiresAt instead of
  recomputing a deadline in the endpoint shell
- web sign-in tests now expect the rememberMe flag forwarded to signIn and a
  new test covers the remember-me opt-in checkbox

* test(auth): surface mTLS integration failure diagnostics on CI

The positive-path Kestrel client-certificate test returns Unauthorized on
the Linux runner with no visibility into which stage denied the request
(cert not negotiated / chain untrusted / EKU / binding). Make the test
endpoint's 401 body re-run the chain policy and report certificate
presence, chain status details, and the thumbprint comparison; capture
server logs with an in-memory logger provider and dump both in the
assertion message when the happy path fails.

* test(auth): brute-force alert needs committed history above the lockout bar

The attempt the failing request records shares the request transaction,
which the sign-in failure rolls back, so the detector must see the
threshold from committed history. Seed five committed failures and raise
AuthenticationSecurity:MaxFailedAttemptsPerHour for this test so the
lockout filter (same threshold) lets the request reach the action.
Diagnostics now also dump the last captured host logs unfiltered.

* ci: retrigger PR verify

* fix(auth): correct #250 test defects flagged by CI (#760)

- Handlers test: ResolveAuthorizationScopes mock setups now match the
  provider case-insensitively, mirroring the real OAuthService (the
  link-preview handler passes the caller's casing through); the previous
  exact-match setup returned Moq's empty-array default for 'Discord'.
- Repository round-trip test: expected list contained a stray prose
  string ('JSON list must round-trip in order') leaked into the
  assertion; replaced with a real third scope token included in the
  serialized input so order preservation is genuinely asserted.
- connected-accounts-card: wrap async onClick handlers in void arrows
  to satisfy @typescript-eslint/no-misused-promises (2 errors).

* test(auth): select the mTLS client certificate explicitly on Linux

CI diagnostics showed the Linux TLS client never offered the certificate
(the server-side Connection.ClientCertificate stayed null while the
handshake itself succeeded), i.e. automatic certificate selection
declined to pick one. Install a LocalCertificateSelectionCallback that
returns the fixture certificate unconditionally so the mTLS integration
test is independent of platform-specific auto-selection rules.

* chore(openapi): regenerate client metadata against merged develop

* docs(auth): clarify the lockout-override comment in the brute-force test

* fix(ci): retry container image pulls and restore the 25m integration budget

Both Docker Hub and public ECR intermittently throttle GitHub-hosted
runners' shared egress IPs (toomanyrequests), killing required-gate jobs
at container start — postgres-app died this way twice tonight even via
the mirror. Pull each CI image once with retry-and-exponential-backoff
before docker run so container starts never reach the registry
(Economy gate x4 containers, affected-API template, Testing Lab).

Also remove a stray duplicate override that clamped the whole-solution
API.IntegrationTests deadline back to 12m after the separate 25m
integration budget was set — the suite needs ~17.6m and was killed at
12m01s on the release PR. The deadline fixture now pins the 25m budget.

* fix(auth): map PermissionAuditLog so permission decision audits actually persist (issues #331, #346)

The new MonetizationPermissionHttpTests failed in CI with 'Cannot create a
DbSet for PermissionAuditLog because this type is not included in the model':
the entity was written by PermissionAuditLogRepository against
IApplicationDbContext but had no IEntityTypeConfiguration anywhere and no
table in any migration, so every runtime audit write (endpoint Check
decisions, expiration audits, permission mutations) failed and was swallowed
by the callers' best-effort catches. Decision audit therefore never persisted.

- Add PermissionAuditLogConfiguration (indexes on TenantId/UserId/Timestamp
  plus (UserId, PermissionType, Timestamp), TenantId value conversion per the
  AccessReviewCampaign pattern, OperationType as int, uncapped text payloads
  so an audit write can never fail on a caller-supplied string length).
- Register it in AuthorizationModule.ConfigureAuthorizationModel and ship the
  AddPermissionAuditLogs migration; regenerate docs/schema.dbml (445 tables),
  verified with db:dbml:verify (0 drift) and db:dbml:check.
- MonetizationPermissionHttpTests: seed the SystemAdmin actor as a user row
  so the Products.CreatorId foreign key holds for the bypass case.

* chore(ci): retrigger PR Verify (#250)

* test(auth): negotiate TLS 1.2 for the mTLS integration client

Even with an explicit LocalCertificateSelectionCallback the Linux TLS
stack leaves the server-side Connection.ClientCertificate null, which
matches the known gap where optional client certificates negotiated
under TLS 1.3 are not surfaced. Pin the test client to TLS 1.2 so the
CertificateRequest/Certificate exchange follows the classic path.

* docs(db): regenerate schema.dbml for the PermissionAuditLogs table

develop's 20260923333152_AddPermissionAuditLog migration landed without a
matching docs/schema.dbml regeneration; db:dbml:check reported the file stale.
Regenerated against a clean migrated database (445 tables) — db:dbml:verify
reports zero model drift and db:dbml:check now passes.

* test(auth): stabilize remember-me lifetime tests against shared clock and replay containment

- read SystemClock.UtcNow (the clock the services use) before each act
  instead of DateTime.UtcNow inside the assertions: parallel classes in
  this project freeze SystemClock globally (e.g. AuthenticationCoverage
  CompletionTests pins it two days back), which made the deadline
  assertions flake by microseconds or whole days
- revoked refresh tokens are now denied through replay containment
  (RefreshTokenContainmentDenial) instead of UnauthorizedAccessException
  since the refresh-lifecycle rework: assert the containment outcome
  (Success=false, no tokens) after logout
- Web3AuthServiceCovTests: mock the policy-aware 5-arg
  GenerateRefreshTokenAsync overload used by VerifyWeb3SignatureAsync

* chore(ci): retrigger PR Verify run (#250)

* test(auth): drive the mTLS integration test through an explicit SslStream

The HttpClient stack swallowed the negotiation outcome: on the Linux
runner the server saw no client certificate while the client reported
nothing, leaving no signal to debug. Perform the TLS handshake with a
raw SslStream so the test pins TargetHost, offers the fixture
certificate through both ClientCertificates and an explicit selection
callback, and reports the certificate the client actually negotiated in
failure diagnostics. HTTP remains a plain GET/1.1 over the stream.

* test(auth): assert the as-configured fail-closed denial contract for monetization gates

CI round 2 showed the deny paths return 500 ProblemDetails, not 403: the
endpoint-level ResourcePermissionAuthorizationFilter is registered only when
Controllers:EnablePermissionAuthorizationFilter is true, which defaults to
false and is set nowhere in the shipped configuration. The enforced gate for
all three surfaces is the [AuthorizeRequest] permission on the dispatched
command/query; its UnauthorizedAccessException is mapped by the shared
exception pipeline to a fail-closed 5xx ProblemDetails.

Pin the observed contract instead of an aspirational one: deny assertions
expect InternalServerError with an explanatory remark in the test class docs,
and add the load-bearing security assertions (no mutation persisted, no
metrics payload leaked on denial). Endpoint Check decision-audit rows are not
asserted because they are only produced by the (disabled) filter; document
that configuration fact in docs/auth/monetization-permissions.md so the
audit-logging section states exactly when Check rows occur.

* chore(client): regenerate OpenAPI client from merged develop surface

* fix(auth): engage the malformed-row fallback during refresh rotation

Pass the raw stored CreatedAt into ResolveOriginatingLifetime instead of a
clock-sanitized stand-in: legacy rows without a creation instant must fall
back to the configured standard duration per the resolver contract, not
renew for whatever lifetime happens to remain. Also relax the post-logout
containment assertions to BeNullOrEmpty (SignInResponse initializes string
fields to empty).

* test(authz): pass role-attribution expectation as collection with because clause

The denied-decisions test passed its explanatory string as a third
expected element via the params overload of FluentAssertions Equal,
so the assertion could never pass. Use the collection + because overload.

* test(auth): require the client certificate during the mTLS handshake

Client-side diagnostics showed the Linux TLS client never negotiated a
certificate (SslStream.LocalCertificate stayed null even with an explicit
selection callback), i.e. the deferred AllowCertificate request never
took effect and the handler saw an anonymous connection. Requiring the
certificate up front forces the TLS layer to run the CertificateRequest
exchange during the handshake; the certificate-less negative case is now
asserted at the TLS layer, while chain/binding/claims behavior stays
covered by the handler and controller suites.

* test(auth): pin cross-tenant monetization denial as 403 from the tenant guard

CI round 3: 403/404 — the remaining assertion expected the cross-tenant deny
to surface as the dispatch-level 500, but the tenant-validation layer rejects
the non-member with 403 before the CQRS gate is reached. Pin the observed
(and stricter) contract.

* fix(ci): stop embedding the full needs graph in the required gate env

The PR Required Gate step passed toJSON(needs) as an env var, which
embeds classify's changedFiles output. On the release PR (2205 changed
files, ~183KB) that single env var exceeded the kernel execve limit and
bash could not even start — the gate failed with 'Argument list too
long' while every child job was green. Pass each dependency's result
string individually instead.

* chore(client): regenerate typed client from merged surface (#250)

After merging develop (billing webhook + compliance endpoints landed while
this branch was in review), the OpenAPI capture was re-run locally against
the merged tree (Release publish + swagger capture + pnpm generate --force).
The generated .gen.ts outputs are byte-identical to the merge result; only
the capture hash/timestamp in .metadata.json changed.

* fix(auth): resolve the user id on failed sign-ins so brute-force alerts can fire

The local sign-in flow only assigned userId after successful password
verification, so on a wrong-password attempt against a KNOWN account
userId stayed null: failed attempts were recorded without their user,
risk analysis skipped the per-user anomaly block, and the brute-force
owner alert - gated on userId.HasValue - could never be published.
Assign the id as soon as the account is resolved; the password branches
keep their existing behavior. Unit regression tests cover alert fired
(brute force detected) and not fired (no anomaly) on wrong password.

* chore(web): drop stale no-misused-promises suppression (#250)

The connected-accounts-card async onClick handlers are now wrapped in void
arrows, so the file no longer triggers the rule and ESLint's unused-
suppression check (pass-on-unpruned-suppressions off) fails the lint gate.

* fix(auth): persist cadence observation count on baseline upsert

The UpsertAsync update path copied every learned estimator except
CadenceObservationCount, so the persisted baseline kept the count from
the initial insert (zero gaps observed) forever. ScoreLearned gates the
cadence feature on CadenceObservationCount > 0, which permanently
disabled cadence deviation detection after the first upsert. Copy the
count alongside the other cadence estimators.

* test(authz): construct a genuinely null-roles record for the normalization test

CreateRecord coalesces an unset roles argument to the default
attribution, so passing roles: null! could never exercise the
null-roles normalization under test. Build the record inline with a
null Roles value instead.

* test(auth): mirror the Kestrel test-suite mTLS client pattern

The upstream Kestrel suite hits exactly our failure mode and documents
it: 'HttpClient might not send the certificate because it is invalid or
it doesn't match any of the certificate authorities sent by the server
in the SSL handshake.' Pin the client certificate through the SslStream
constructor selection callback, authenticate with a random target host
(avoiding the TLS session-resumption cache), and use RequireCertificate
+ AllowAnyClientCertificate() on the listener, matching the upstream
green configuration.

* test(auth): mark the alert-handler deleted user via DeletedAt directly

SoftDelete() throws for unpersisted entities (Version == 0), which the CI
run surfaced for the in-memory user in Handle_WhenUserIsDeleted. IsDeleted
is derived from DeletedAt, so set the timestamp directly.

* chore(client): regenerate typed client after develop merge (#250)

Develop moved again during CI (#758 remember-me regenerated the surface);
re-ran the local Release publish + swagger capture + pnpm generate --force
against the merged tree. Only the capture hash and the remember-me types
from develop's side carry over.

* test(auth): associate the request key with issued client certificates

CertificateRequest.Create(issuerCertificate, ...) signs the leaf with the
issuer but — unlike CreateSelfSigned — does not associate the request's
private key with the returned certificate. The fixture then exported the
keyless certificate to PFX and re-imported it, so every client
certificate carried a public key with no private key: the TLS client
could never answer a CertificateRequest, which is why the server saw an
anonymous connection on every runner and platform. Associate the key
with CopyWithPrivateKey before persisting, in both the Kestrel fixture
and the shared unit-test factory.

* test(auth): align rotation fixtures with originating-lifetime renewal

PR #758 (remember-me sessions) made refresh rotation renew the
credential's originating lifetime instead of the configured standard
lifetime. Its CI affected-set did not include the API integration test
project, so two pre-existing rotation suites started failing for every
PR that runs that project (reproduced on develop itself, run 38023739804
and the Economy gate):

- RefreshTokenLifecyclePostgreSqlHttpTests seeded an 8-hour credential
  and expected the configured multi-day window on renewal.
- RefreshTokenExpirationPostgreSqlHttpTests seeded a 23.25-hour
  credential and expected the configured 7-day window on the sliding
  30-day-ceiling case.

Seed those credentials with the full configured window so the
assertions verify the renewal semantics that shipped with #758;
non-sliding cases keep the short-lived credential the rotation must not
extend. Widen the lifecycle suite's renewal tolerance from 1s to 5s:
the second sequential rotation also renews the sub-second
elapsed-since-issuance carried by the preserved auth-time CreatedAt.

* test(auth): probe /cert-whoami over HTTP/1.0

The endpoint answered the raw HTTP/1.1 probe with a chunked body, so the
JSON payload was prefixed with the hex chunk size and failed to parse.
HTTP/1.0 keeps the response close-delimited.

* fix(auth): renew refresh rotations at the configured lifetime, not the stored one

PR #758 made rotation copy the stored originating duration
(ExpiresAt - CreatedAt) into the replacement token. Any row whose
stored lifetime differs from its configured class lifetime —
short-lived or absolute-session-capped rows, externally seeded
fixtures, rows minted under older configuration — renewed with that
stale duration: an 8-hour stored row minted 8-hour replacements
instead of the configured N days.

Rotation now classifies the session's class and mints the
replacement at the configured lifetime for that class:
- originating duration strictly longer than the configured standard
  lifetime -> persistent (remember me) -> PersistentRefreshTokenExpirationDays
- anything else (standard, legacy, malformed rows)
  -> standard RefreshTokenExpirationDays

This restores the pre-#758 contract for standard sessions (the four
failing PostgreSQL HTTP theories) while keeping the remember-me
guarantee that persistent sessions do not collapse onto the standard
lifetime. Adds RefreshTokenLifetimeResolver.ResolveRenewalDays as the
single policy-aware renewal point plus regression unit tests covering
short-lived, persistent, typed-options and malformed rows, and a
service-level rotation test asserting configured-day lifetimes.

* fix(auth): classify refresh rotation by originating policy, not stored span

Rotation (#758 window) renewed tokens by replaying the stored row's
CreatedAt→ExpiresAt span. Rotation preserves the originating CreatedAt
while advancing ExpiresAt, so that span compounds elapsed time and
drifts upward every rotation — the lifecycle/expiration integration
suites observed replacements at +8h/+24h instead of the configured
1/7/30 days, failing four tests on the release PR.

Classify the session by its originating policy instead (span ≥ persistent
days ⇒ persistent lifetime, else standard) and renew at now + configured
days. Drift-free, keeps the remember-me persistent contract (73/73
remember-me unit tests green), restores the configured-expiry contract
(7/7 previously failing integration tests green).

* test(auth): use contract JSON options in certificate credential DTO tests

The converter round-trip test serialized with raw options (PascalCase,
case-sensitive), so the emitted {"Type":..,"Thumbprint":..} JSON never
matched the converter's camelCase "type"/"thumbprint" discriminators.
Configure the test serializer the way the API does (camelCase naming
policy + case-insensitive binding).

* test(auth): guard rotation against copying short stored lifetimes over HTTP

Adds a PostgreSQL HTTP theory asserting that a seeded 8-hour refresh
row renews at each configured lifetime (1/7/30 days) on the real
tokens:refresh endpoint, with an explicit guard that the seeded short
window never leaks into the replacement or the reported deadline.
Also ensures the integration suite is selected by the affected-test
chooser so the four previously red theories run on this PR.

* fix(auth): classify rotation persistence by the persistent threshold, not the standard one

Sequential rotation preserves the originating CreatedAt while advancing
ExpiresAt, so every successor's stored span is the configured window plus
the sub-second elapsed time since issuance. The previous classification
(span > standardDays) therefore flipped the second rotation of a standard
N-day session to persistent and minted the replacement at the persistent
lifetime (default 30 days) regardless of the configured N.

Classify by the session's originating policy instead: only a stored span at
or beyond the configured persistent lifetime renews at the persistent
lifetime; every other row — standard sessions mid-rotation, legacy or
malformed rows, and rows minted under different configuration — renews at
the configured standard lifetime. Genuine persistent rows minted with
exactly the persistent window still classify persistent (>=).

* feat(security): revoke sessions and tokens on confirmed-compromise risk events

SuspiciousLoginDetectedV1 gains a third durable consumer,
RiskEventSessionRevocationHandler: on ImpossibleTravel and
BruteForceDetected (confirmed account-compromise signals) it revokes
every refresh token, terminates all sessions as SecurityViolation,
bumps the user token version, and records a versioned token cutoff -
mirroring RevokeAllUserTokensHandler and the refresh-replay
containment path. LoginStepUpRequired stays notify-only. Gated by
Authentication:RiskEventRevocation:Enabled (default on), idempotent
per EventId through the durable inbox, audited through the central
authentication audit pipeline.

Fixes #279

* fix(security): compile fixes for risk-event revocation consumer tests

* feat(auth): one-time email sign-in code modality for passwordless flows (#752)

* feat(auth): one-time email sign-in code modality for passwordless flows (issue #252)

Backend:
- POST /v1/auth/email-code:request and /v1/auth/email-code:consume endpoints
  (AllowAnonymous, registered in AnonymousEndpointRegistry with justifications)
- EmailCodeService: crypto-random six-digit codes stored only as SHA-256
  digests, 10-minute TTL, five-attempt verify limit, 60-second resend
  throttle, constant-time digest comparison (dummy compare on unknown email)
- Request/consume command handlers; consume reuses the authenticated session
  issuer so the code modality issues the same session/token pair as the
  magic link; enumeration-safe generic 200 on request
- EmailCodeRequestedNotification + SendEmailCodeRequestedHandler queue the
  delivery row; new NotificationType.EmailCode classified transactional
  (always delivered, metadata protected at rest)
- EmailCodeRenderer renders the one-time code email (transactional, no footer)

Web:
- /email-code page under (auth) following the magic-link patterns: email
  request step then six-digit code entry, consume via the credentials
  provider extension issuing the same auth session
- sign-in page links the new modality; en-US/pt-BR messages

Tests: service (TTL, attempts, single-use, digest-only storage, throttle,
normalization), command handlers (enumeration-safe, throttled, cancellation,
issuance), notification handler, renderer, web page/component/lib tests

* fix(auth,web): keep email-code cache eviction on the cache clock; lint the flow component

API: the stored entry carried an absolute expiration derived from the
domain clock (SystemClock), but MemoryCache evicts on its own real-time
clock. Whenever the domain clock disagreed with wall time (e.g. the fake
TimeProvider pinned hours in the past in EmailCodeServiceTests), the
real-time scanner evicted live entries immediately, so every verify fell
through to 'Invalid code' and the digest/expiry assertions failed.
Cache-level eviction is only a safety net for abandoned entries: use
AbsoluteExpirationRelativeToNow (cache clock) with slack beyond the
ten-minute TTL, while the logical lifetime stays enforced against
SystemClock in VerifyEmailCodeAsync.

Web: drop the unused CheckCircle2 import and wrap both async submit
handlers in void-returning arrow functions (no-unused-vars,
no-misused-promises).

* chore(openapi): regenerate client for the email-code endpoints

Captured swagger.json from the published API (same pipeline as
scripts/ci/verify-openapi-surface.sh) and regenerated the typed client:
postAuthEmailCodeRequest / postAuthEmailCodeConsume endpoints, auth
module methods, and DTO types.

* chore(openapi): refresh generated client metadata after develop merge

* test(notifications): include EmailCode in the expected non-suppressible catalog set

The one-time email sign-in code type is Transactional by design (always
delivered, never suppressible), so the exhaustive catalog assertion must
list it alongside EmailVerification/PasswordReset/MagicLink/TenantInvite.

* test(api): align inbox receipt assertions with the third suspicious-login consumer

The recorded-impossible-travel and step-up alert suites asserted a
single inbox receipt per event; the new risk-event revocation consumer
adds a second receipt (completed once, no-op for step-up). The disabled
switch test now filters receipts client-side - EF cannot translate the
StringComparison Contains overload.

* feat(auth): config-gated generic OIDC federation SSO for enterprise identity providers (#771)

* feat(auth): config-gated generic OIDC federation SSO for enterprise IdPs

Implements the OIDC half of #268 (SAML deferred with documentation):

- SharedKernel: OidcProviderOptions under Authentication:ExternalProviders:Oidc:<slug>
  (Enabled=false default, HTTPS Authority, ClientId/Secret, scopes default
  openid profile email, ClaimMapping sub/email/name/email_verified, optional
  EmailDomains) with fail-closed validation; 4-provider allowlist untouched.
- OidcFederationService: .well-known/openid-configuration discovery with 15-min
  caching (issuer must equal authority), authorization-code exchange, ID-token
  validation per the LtiPlatformJwksService pattern (JWKS + TokenValidationParameters,
  RS256, iss/aud/lifetime) — no new packages. Email-domain gate; domain-to-provider
  discovery; end_session_endpoint logout forwarding.
- OAuthAuthService.OidcSignInAsync reusing ResolveExternalUserAsync
  (link by oidc-<slug> + provider key, verified-email merge, JIT via
  User.CreateOAuthUser) with fail-closed MFA policy gate (amr 'mfa' proof
  required when policy demands); amr/acr surfaced on SignInResponse.
- AuthController: /v1/auth/oidc/{slug}:sign-in-authorize + :sign-in-callback
  (anonymous + registry), GET /v1/auth/oidc:discover-provider (anonymous +
  registry), GET /v1/auth/oidc/{slug}:end-session-url (authorized).
- SAML 2.0 deferral documented (Sustainsys vs ITfoxtec product decision).
- Tests: options validation, stubbed-provider discovery caching + token failure
  modes (bad iss/aud/exp/signature/malformed), JIT/link/unverified-collision,
  MFA fail-closed, logout forwarding, controller + handler/validator coverage.

* chore(openapi): regenerate typed client with OIDC federation endpoints

Regenerated from the CI-captured swagger of this PR head (openapi artifact
run 38019588554): 4 new /v1/auth/oidc endpoints and the SignInResponse
amr/acr/MfaVerifiedByProvider fields.

* fix(tests): OIDC test fixes for CI — fresh HttpClient per factory call, UnauthorizedObjectResult assertion, handler-owned state

- StubOidcProvider factory now returns a new HttpClient per CreateClient call
  (the service disposes each client; reusing one caused ObjectDisposedException
  on the token/JWKS legs).
- 401 controller assertion uses UnauthorizedObjectResult (what Unauthorized(object) returns).
- OidcSignInCommandHandler test asserts the handler-generated Guid-N CSRF state.

* ci: retrigger PR Verify for 352f6768e (synchronize event was not delivered)

* ci: retrigger PR Verify (platform dropped two synchronize/reopened triggers)

* docs: touch SAML deferral note (retrigger CI)

* ci: retrigger workflows for #268 (web-flow push)

* ci: retrigger PR Verify after GitHub Actions event backlog (05:08-05:31 UTC)

* fix(auth): adapt OIDC federation to develop's scope-consent + remember-me surface

develop (PR #250/#210 lineage) extended ResolveExternalUserAsync with granted-scope
consent recording and CompleteSignInAsync with rememberMe. The OIDC flow now
carries the provider's configured scopes onto the external-login consent trail
and plumbs RememberMe through the callback DTO/command/controller; test ctor
mocks updated for the two new OAuthAuthService dependencies.

* fix(auth): reject malformed OIDC id_tokens before the JWKS round-trip

A structurally unparseable token is malformed regardless of which key
would have signed it, so CanReadToken + a full ReadToken parse now run
before GetSigningKeysAsync and throw the 'is malformed' failure.
Previously the malformed classification sat after the JWKS fetch and a
three-segment garbage token (CanReadToken true, JSON parse failure)
fell into the generic 'failed validation' bucket.

Tests: the malformed case now also asserts JwksHits == 0 so the
before-JWKS ordering is enforced, and the expired-token stub now keeps
nbf < exp (JwtPayload refuses to construct otherwise, IDX12401).

* chore(openapi): regenerate client with the OIDC callback remember-me flag

Captured swagger.json from the published API (same pipeline as
scripts/ci/verify-openapi-surface.sh) after the develop merge and
regenerated the typed client: IdentityAuthenticationOidcCallbackRequestDto
gains the optional rememberMe field.

* feat(billing): named billing events, events API, and webhook retry worker

Issue #396 (safe-default scope from verifier):

- Named billing integration events following the UseCaseEventContract pattern:
  BillingWebhookProcessedV1, BillingWebhookFailedV1, BillingInvoicePaidV1,
  BillingSubscriptionRenewedV1, BillingSubscriptionCancelledV1. The four provider
  webhook commands now declare them as conditional events (replacing the generated
  'no named cross-module event' fallback); publication rides the existing durable
  transport (IDurableEventProducer) and correlates with the emitting use-case
  operation. Best-effort from the webhook path: a recording failure never changes
  the processing outcome.
- GET api/v{v}/billing/events (paged webhook inbox feed with status/provider/type/
  date filters), GET {eventId}, GET outbox (paged named-event feed from the platform
  outbox read model scoped to Commerce.Billing) and GET outbox/{eventId}. Read-only
  over the existing stores; SystemAdmin policy per sibling billing controllers.
- BillingWebhookRetryBackgroundService + BillingWebhookRetryWorker: polls failed
  inbox events past their exponential-backoff next-attempt time and requeues them
  through the existing RetryWebhookEventCommand path, honoring the retry ceiling
  (default 5, matching the manual retry path), with OTel metrics
  (GameGuild.Billing.WebhookRetry meter).
- Tests: event contract architecture tests, publisher tests (correlation + tenant
  fallback + best-effort), retry worker scheduling/backoff/ceiling/isolation tests,
  controller authorization/paging tests, and query handler paging/filter tests.

References #396

* chore(openapi): regenerate client on merged tree (billing events + OIDC federation)

* feat(billing): register CQRS validation behavior and cache webhook security summary (#776)

* feat(billing): register CQRS validation behavior and cache webhook security summary

Issue #394 — closes the two remaining gaps from the verification audit:

1. Validation pipeline integration: register ValidationBehavior<,> as an
   IPipelineBehavior<,> in the API composition root (InfrastructureLayerExtensions),
   ordered BEFORE ResourceQuotaBehavior and UseCaseOperationBehavior so invalid
   commands short-circuit with RequestValidationException without consuming quotas
   and without emitting operation events.

2. Query caching strategy: GetBillingWebhookSecuritySummaryHandler now serves the
   SystemAdmin monitoring summary from a best-effort short-TTL (15s) ICacheService
   cache; WebhookSecurityEventPublisher evicts the cached summary whenever a webhook
   security event is published; Billing module registers an in-process ICacheService
   fallback when the host has not registered one. GetWebhookEvent stays uncached
   (by-ID freshness).

Tests: mediator short-circuit (invalid command never reaches inner pipeline stages
or the handler; valid command passes through), composition-root behavior ordering,
cache miss/hit/TTL-expiry/eviction/fault-tolerance.

* fix(billing): reconcile validation pipeline tests and slug fixture

- Register the spy command handler under IRequestHandler<,> as well: the
  mediator resolves IRequestHandler<,> (AddCqrs registers both contracts),
  so the valid-command pass-through test could not find the handler.
- Shorten the MonetizationPermissionHttpTests slug fixture so the -admin
  variant satisfies the CreateSubscriptionPlan validator's 50-char Slug
  limit, which the now-active ValidationBehavior enforces at dispatch
  time (400 instead of 201).

---------

Co-authored-by: impl-agent <impl-agent@users.noreply.github.com>

* test(api): response-level security header verification for CSP/HSTS defaults (#774)

* test(api): response-level security header verification for CSP/HSTS defaults

Adds SecurityHeadersHttpTests to GameGuild.API.IntegrationTests following the
PostgreSqlHttpTests WebApplicationFactory pattern:

- authenticated 200, anonymous 401 and ProblemDetails 404 responses carry the
  restrictive default CSP (default-src 'none'; frame-ancestors 'none'),
  X-Frame-Options DENY, nosniff, Referrer-Policy and Permissions-Policy
- /documentation serves the separate Swagger CSP (Development factory)
- sensitive /auth paths respond with no-store/no-cache cache directives
- Production environment over HTTPS emits Strict-Transport-Security while the
  non-production host omits it (environment-overridden factories)

Documents the shipped header defaults and reverse-proxy/HSTS implications in
docs/api/authentication-options-configuration.md.

Fixes #276

* test(ap…
…ub callback handler and POST contract (#233 #248) (#820)

* wip: salvage at quota wall 2026-10-12

* fix(authn): compile fixes salvaged from quota wall — nullable mapping coalescing and wiring-test sender mock shapes

* chore(openapi): regenerate typed client — orchestrated auth endpoints + GitHub callback POST contract (#233 #248)

* test(authn): complete-flow fixture satisfies every required step so the derived NextStep is null

* fix(authn): NextStep returns null when every required step is satisfied — FirstOrDefault over the non-nullable step enum defaulted to PrimaryCredential(0) instead

* fix(authn): drop ambiguous cref from NextStep doc comment (CS0419 under warnings-as-errors)

* test(authn): completed-flow branch model asserts the corrected NextStep semantics (null, not the enum default)

* fix(authn): derive AuthenticationFlowState.IsComplete from step satisfaction

IsComplete was a stored auto-property never set on freshly constructed
domain states (only on the EF record), so a fully-satisfied flow read
incomplete until persistence. Derive it like NextStep so the domain
model is internally consistent; drop the now-redundant assignments.

---------

Co-authored-by: Alexandre Tolstenko <tolstenko@gmail.com>
#410) (#822)

* wip: salvage at quota wall 2026-10-12

* fix(credits): compile fixes salvaged from quota wall — named-arg casing and ActorContext required members after develop merge

* fix(credits): correct paramref casing for Release warning-clean publish

* chore(openapi): regenerate typed client — account credits, gifts, referrals endpoints (#410)

* test(credits): idempotency case grants 150 and caps the tender at 70 — the 70-default grant made the replay assertion vacuous (fully-used grant) and without the cap the target is the full outstanding amount

* fix(credits): CI round — leaderboard counts via translatable conditional sums; referral link base normalized to a path-terminated URL; IssueGrant returns CreatedResult; persistence tests persist the enqueued consume ledger and create due grants with future-then-swept expiries

* fix(credits): leaderboard aggregates a narrow projection in memory — grouping predicates still do not translate on the persistence test provider

* test(payments): correct UsageRate expectation — referrer reward grant counts as issued

The rewarded referral grants the referrer 10 USD, so cross-currency
issued totals 150, not 140: usage rate is 40/150 = 0.2667.

---------

Co-authored-by: Alexandre Tolstenko <tolstenko@gmail.com>
The dts rollup worker crashed with ERR_WORKER_OUT_OF_MEMORY on CI once
generated types crossed their current size; the main-process heap bump
propagates to worker_threads.
The dts rollup worker crashed with ERR_WORKER_OUT_OF_MEMORY on CI once
generated types crossed their current size; the main-process heap bump
propagates to worker_threads.
@mathrmartins
mathrmartins merged commit 2b35618 into main Oct 12, 2026
33 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants