Skip to content

[pull] main from fullstackhero:main#2

Open
pull[bot] wants to merge 868 commits into
wuuer:mainfrom
fullstackhero:main
Open

[pull] main from fullstackhero:main#2
pull[bot] wants to merge 868 commits into
wuuer:mainfrom
fullstackhero:main

Conversation

@pull

@pull pull Bot commented May 27, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

iammukeshm and others added 30 commits May 23, 2026 16:47
Full route-mocked, JWT-seeded Playwright coverage of every page in both React apps — no live backend required.
- Dashboard (121): login/demo picker, overview/activity/invoices, catalog, identity, tickets, chat, files, settings, system.
- Admin (93): login/dashboard, tenants, users/roles, billing, audits/impersonation/webhooks/notifications/health, settings.
- Shared harness in tests/helpers/shell-mocks.ts mocks all global AppShell calls (notifications/chat/profile/permissions; aborts SSE/realtime). All green, eslint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… helpers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ coverage-gate job

Fixes the stale test matrix (Chat.Tests/Files.Tests were never run in CI) and adds
a Coverage Gate job that runs the whole solution with Coverlet and fails below an
80%-ish line floor (ratchet — bump MIN_LINE as coverage rises).
…te limiting + security headers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ler + rate limiting + security headers"

This reverts commit 3d9955a, reversing
changes made to 679e6e3.
…ption handler + rate limiting + security headers)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… audit

Findings and fixes from a race-condition / code-smell audit (Roslyn navigator
detect_antipatterns, find_dead_code, detect_circular_dependencies + targeted
concurrency sweeps). Concurrency hygiene was already strong — no async void,
no sync-over-async, no fire-and-forget, thread-safe singletons, zero circular
deps. The genuine items:

- Audit (static): swap the enricher list for an atomically-replaced immutable
  array read once per WriteAsync. Kills the latent "collection modified" race
  if Configure() ever runs concurrently with enrichment, and removes a
  process-global mutable-List test-isolation hazard.
- QuotaEnforcementMiddleware: inject TimeProvider instead of DateTimeOffset.UtcNow
  for Retry-After, matching the rest of the Quota subsystem (deterministic in tests).
- PresenceTracker.Connect: also report the offline->online transition when the
  AddOrUpdate update factory resurrects a count==0 key (Disconnect set 0 but
  hadn't removed it yet) — previously a reconnect in that window missed the
  presence broadcast.
- ChannelMember: remove dead SetMuted (zero callers); IsMuted is now get-only
  (still persisted + exposed via DTO, populated by EF via backing field).

Investigated but intentionally NOT changed (false positives confirmed):
- catch(Exception) in hosted services already filter OperationCanceledException.
- Flagged EF "missing AsNoTracking" queries all read-then-mutate-then-save
  (tracking required); the read-only paths already use AsNoTracking.
- find_dead_code hits for ChannelAuthorization / EntityEntryExtensions are
  used extension methods; EF model snapshots are tooling artifacts.

All unit (779) + integration (665) + middleware (5) tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (accurate to current codebase)

Establish AGENTS.md as the canonical, tool-neutral AI guide (CLAUDE.md/GEMINI.md
are thin @import bridges) and build out .agents/ guidance verified against the
current code via Roslyn navigation + real-file extraction.

Rules (.agents/rules/) — lean, on-demand, indexed from AGENTS.md:
- cross-cutting: architecture, api-conventions, database, eventing, caching, jobs,
  resilience, storage, security, realtime, logging, testing, integration-testing
- per-module: identity, multitenancy, chat, files, webhooks, auditing, billing,
  catalog, tickets, notifications
- frontend: shared + admin + dashboard (the two React apps' real divergences)
- replaces the old flat modules.md/persistence.md/testing-rules.md

Skills (.agents/skills/) — audited the 6 existing against the live codebase and
rewrote them (they were stale: IRepository<T>, PagedList<T>, Moq/FluentAssertions,
Guid.NewGuid, class-level [FshModule(Order=n)], DbContext vs BaseDbContext, only 2
of the 4 module-registration sites). Added 5: add-react-page, add-full-slice,
create-migration, add-integration-event, add-permission. Skills hold the recipe.

Workflows (.agents/workflows/) — reconciled so they orchestrate and delegate to
skills (no duplicated/contradictory templates); fixed stale facts (migrations run
via DbMigrator, not on startup; TenantDbContext not "MultitenancyDbContext").

Also corrects the [FshModule] attribute (assembly-level positional, not class-level)
in architecture.md and the AGENTS.md pointer.

Docs only; no source/build impact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop nswag.consolecore from dotnet-tools and delete scripts/openapi/*
  (NSwag-based C# client generation is no longer used).
- Remove scripts/test-cli.ps1 and the requirements/frontend-and-platform.md
  planning doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…page redesign, v10

- AI-Driven Development docs section (overview, AGENTS.md & .agents/, skills &
  workflows, developing with Claude Code) + section registration.
- Sponsorship: sticky sponsor card atop the docs ToC, and a "Back the build"
  home section (Open Collective), placed right under the architecture section.
- Homepage: new "Stack at a glance" bento opener; the architecture section
  rebuilt as a modular-monolith + vertical-slice diagram (two balanced boards);
  FSH brand mark in the hero; "Who it's for" redesigned as icon-led cards;
  module-card code panels removed; competitor product references removed.
- Version: product version aligned to v10 (.NET 10) across the homepage and
  docs copy. API version paths (/api/v1, Features/v1, Contracts.v1) untouched.
- Fix: type the header nav `match` field — astro check now 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lish

- Add "Tested to production standard" landing section (Testing.astro): a
  four-pillar scoreboard (unit / integration / E2E / architecture) over a
  green test-run terminal, leading with real-PostgreSQL-via-Testcontainers.
  Wire in as section 05; renumber WhoItsFor/FAQ/FinalCta to 06/07/08. Sync
  stale test counts in Hero (900+ -> 1,400+) and the FAQ answer.
- Theme toggle: replace the muddy CSS token cross-fade with a View Transitions
  API snapshot cross-fade (GPU-composited, text stays crisp, no frame drops);
  clean instant fallback for reduced-motion / unsupported browsers.
- Mobile responsiveness pass on the homepage: drop heading bases (sections
  30px, hero 34px) so they stop dominating phones; hero stats, TechStack, and
  the testing scoreboard go single-column on mobile; hero terminal scrolls
  instead of clipping; FAQ answers reclaim full width on mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t, React SPA hosting

- Pin Terraform >= 1.15.4 and AWS provider ~> 6.46 in the roots; permissive
  floors (>= 1.15 / >= 6.0) in child modules. Commit a multi-platform
  .terraform.lock.hcl locked to aws 6.46.0.
- Collapse the shared/ -> app_stack/ wrapper into a single root (provider +
  backend now live in app_stack/), removing ~500 lines of duplicated vars and
  outputs plus a passthrough bug that silently dropped ~30 tunables. Dedupe
  tags via provider default_tags.
- Add reusable modules/static_site (private S3 + OAC CloudFront, SPA
  403/404->index.html fallback, default-on managed security headers,
  Terraform-owned config.json) and host the two React SPAs (admin, dashboard)
  that replaced the removed server-rendered Blazor service. SPA CloudFront
  origins are auto-added to the API CORS allow-list. No Route53/domain
  resources (custom aliases optional).
- Drop the unnecessary api_task_secrets IAM policy (least privilege).
- Add one-command deploy.sh / deploy.ps1: terraform apply -> optional API
  image build/push -> build + s3 sync + CloudFront invalidate for both SPAs.
- Refresh README; terraform fmt + validate clean against aws 6.46.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iammukeshm and others added 30 commits June 12, 2026 18:18
GitHub advisory GHSA-hv8m-jj95-wg3x (high severity, LZ4 decompression
AccessViolationException) was published against MessagePack < 2.5.301,
which Microsoft.AspNetCore.SignalR.StackExchangeRedis pulls in at
2.5.187. NuGet audit runs with warnings-as-errors, so every restore in
CI now fails with NU1903 — breaking all PR checks regardless of their
content. CentralPackageTransitivePinningEnabled is on, so a single
PackageVersion entry bumps the transitive across the solution.

Remove the pin once the SignalR backplane package references a patched
MessagePack itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(deps): pin transitive MessagePack to 2.5.301 (NU1903 breaks all CI)
…urfaces

fix(clients): permission-gate every nav surface to mirror server authorization
…lesAsync lookup

GetUserRolesAsync issued one membership query per role in the tenant
(N+1). One GetRolesAsync call now feeds a case-insensitive set lookup,
matching ASP.NET Identity's normalized-name comparison semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lates

PII minimization: user registration, token generation, and welcome-email
failure logs now identify users by the pseudonymous UserId only. The
email previously rode along in three message templates and flowed into
every exported log sink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ct, and schema

The property was get-only and never set anywhere, so it always
serialized as false: dead weight in the domain, ChannelMemberDto,
the dashboard client type, and a chat.ChannelMembers column. Removed
end-to-end with migration DropChannelMemberIsMuted.

Deliberate contract change: ChannelMemberDto loses a field no consumer
ever read (verified across both React apps and all tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r chat error toasts

- refreshAccessToken now aborts after the standard 30s timeout; a
  stalled refresh previously hung the shared refreshPromise and every
  queued 401-retry behind it (admin already had this guard).
- Ticket comment composer passes the body through mutate(arg) instead
  of closed-over state (golden rule #9) and gains an aria-label.
- Chat DM/delete/pin failure toasts now include the ProblemDetails
  description instead of a bare generic message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No codegen step exists - API types are hand-written per the frontend
conventions. The dependency only created false expectations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardening audit: identity N+1, PII log scrubbing, dead IsMuted column, dashboard refresh timeout
Build the link with QueryHelpers: trim the trailing slash from the
configured origin, URL-encode each value, and include the tenant the
reset page requires. A host-only OriginUrl produced "//reset-password"
(404 in the SPA router), the link lacked the tenant query param
(treated as malformed), and the email was not encoded. Adds
UserPasswordServiceTests.
Validated the full publish pipeline end-to-end (pack -> install -> scaffold
full/no-aspire/no-frontend -> build backend+frontends -> run Aspire stack ->
auth + endpoints). Three fixes surfaced:

- pack: stop shipping a local AI-tool settings file. src/.claude/settings.local.json
  was tracked and leaked into both the template package and scaffolded output
  (root .gitignore only anchored /.claude/). Untrack it and ignore **/.claude/
  settings.local.json at any depth.
- template pkg: render a README on the nuget.org gallery page. The template pack
  lives outside src/ so it never inherited src/Directory.Build.props's
  PackageReadmeFile; add PackageReadmeFile + the root README at package root
  (same README the CLI ships). Clears the "missing a readme" pack warning.
- aspire: pin the dashboard to the documented port 15888. launchSettings used an
  auto-generated 17273 while the README, README-template, fsh CLI "Next Steps"
  (FshConstants.AspireDashboardPort) and `fsh doctor` all advertise 15888.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(identity): correct password-reset email link (trailing slash, tenant, URL-encoding)
…ness

chore(nuget): publish-readiness fixes for CLI + template packages
The gallery icon was 1692x1692 (150 KB) — above NuGet's ~1024px practical max.
Downscale the same FSH mark to 512x512 (53 KB), preserving transparency. Embedded
at the root of both the CLI and template packages via PackageIcon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(nuget): optimize package icon to 512x512
…laims

UserPermissionService computed the effective permission set from a user's
DIRECT roles only, while IdentityService.AddRoleClaimsAsync already unions
direct + group-derived roles when minting the JWT. Result: a user whose only
role comes via a UserGroup saw the role in their token but failed every
.RequirePermission() gate (and GET /identity/permissions under-reported).

Union direct roles with roles reachable via UserGroups -> GroupRoles before
resolving permissions (group mutations already invalidate this cache entry).
Query-only — no schema change.

Tests:
- GroupRolePermissionTests (integration): a group-only user's own-permissions
  include the group role's grants and pass the gated endpoint.
- AuthorizationMetadataTests (architecture): RequiredPermissionAttribute exists
  exactly once implementing IRequiredPermissionMetadata, so gates can't silently
  fail open via a duplicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he JWT

The dashboard JWT carries only role names; auth-context.tsx fetches the
effective permission list from GET /api/v1/identity/permissions, caches it, and
exposes permissionsHydrated. Update the agent rules/skills to match the shipped
behavior, and align the Playwright auth-seed helper: permissions in the fake JWT
are inert (gated specs mock GET /identity/permissions instead).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop `dotnet workload install aspire` (devcontainer postCreate + template
  README prerequisite): .NET 10 ships Aspire as NuGet packages, no workload.
- DbMigrator is a console Generic Host, so its env gate reads DOTNET_ENVIRONMENT,
  not ASPNETCORE_ENVIRONMENT — fix the seed-demo refusal message + docs that told
  users to set the wrong variable.
- fsh CLI: the `new` example used the non-existent `--no-git`; use `--no-frontend`.
- CONTRIBUTING: Node.js 20+ to match both READMEs (was 22+).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore: pre-publish cleanup — group-permission fix, fetched-permissions docs, .NET 10 nits
Ran npm audit fix in clients/admin and clients/dashboard (all fixes
within existing semver ranges) plus an esbuild override:

- react-router 7.14.1 -> 7.18.0 (turbo-stream RCE, __manifest DoS, CSRF)
- vite 7.3.2 -> 7.3.5 (server.fs.deny bypass, launch-editor NTLMv2)
- ws 7.5.10 -> 7.5.11 (memory-exhaustion DoS)
- js-yaml 4.1.1 -> 4.2.0 (quadratic-complexity DoS)
- @babel/* -> 7.29.7 (sourceMappingURL arbitrary file read)
- esbuild -> 0.28.1 via overrides (dev-server file read); vite 7 pins
  ^0.27.0 and 0.28.1 is the only patched release, so an override avoids
  the breaking vite 8 / plugin-react 6 major bump

Both apps: npm audit reports 0 vulnerabilities and the production build
(tsc -b + vite build) passes clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registration failures (duplicate email/username, weak password) were thrown
as CustomException defaulting to HTTP 500, and the actual Identity reasons —
already carried in ErrorMessages → ProblemDetails `errors` — were dropped by
the clients. Users saw a misleading "500 error while registering a new user"
with no indication of what to fix.

Backend: throw these (and password-mismatch) as 400 BadRequest, since they are
client-input errors, not server faults.

Frontend: surface the ProblemDetails `errors` array in both apps. dashboard's
describe() and a new shared admin describeError() now show the real reason
(e.g. "Email 'x' is already taken.") instead of the bare status + generic
detail. Both shapes handled (Identity flat string[]; FluentValidation map).

Tests: tighten the duplicate-email and weak-password registration integration
tests to assert 400 instead of just non-success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arity

fix(identity): return 400 with specific reasons on registration failure
The admin app posted to /api/v1/identity/users/register and
/api/v1/identity/users/change-password, but the API serves these as
bare /api/v1/identity/register and /api/v1/identity/change-password
(the /users prefix is only for user-collection sub-resources). The
mismatch returned 405/404, so "Add new user" and "Change password"
failed in the admin UI.

The dashboard app already calls the correct bare routes, and the
identity integration tests exercise them — only admin's users.ts was
wrong, and it was internally inconsistent (it correctly used the
IDENTITY base for forgot-password/reset-password/profile but the
/users BASE for these two). Point both calls at the IDENTITY base and
update the route-mocked Playwright specs to match.

Fixes #1309

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-route

fix(admin): correct identity register & change-password routes (405)
…1312)

WebhookSubscription.SecretHash stores the per-subscription signing secret
encrypted at rest via ASP.NET Data Protection (IWebhookSecretProtector.Protect),
then recovered with Unprotect to sign outbound HMAC payloads. The name
"SecretHash" wrongly implies a one-way, irreversible hash, misrepresenting the
value's security properties and inviting mishandling.

- Rename the persisted property/column to ProtectedSecret (matches the existing
  protectedSecret local in CreateWebhookSubscriptionCommandHandler) with a
  RenameColumn migration (reversible, no data loss).
- Rename IWebhookDeliveryService.DeliverAsync secretHash parameter to
  signingSecret: there the value is already unprotected plaintext used for
  signing, matching the signingSecret local in WebhookDispatchJob.

No public contract change (the secret is write-only via command).
The tenant billing grace span is named "grace window" in code (GraceWindowDays
option in both TenantBillingOptions and Identity's TenantGraceOptions, the
Billing:GraceWindowDays config key, and the _graceWindowDays field) while every
user-facing and conceptual reference calls it the grace period. "Grace period"
is the standard SaaS billing term; standardize the vocabulary on it.

- Rename the GraceWindowDays property in both options classes, the bound config
  key (Billing:GraceWindowDays -> Billing:GracePeriodDays), all references, and
  the _graceWindowDays field to GracePeriodDays.
- Align the surrounding comments/XML docs that still said "grace window".

BREAKING (config): deployments setting Billing:GraceWindowDays must rename the
key to Billing:GracePeriodDays. The default (7 days) is unchanged. No public
contract change: the GraceEndsUtc DTO/event fields are untouched.
…1315)

* docs(plan): WhatsApp wallet + topup Phase 1 implementation plan

* feat(billing): add Topup invoice purpose + wallet/topup enums

* feat(billing): add Wallet aggregate + WalletTransaction ledger

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(billing): add TopupRequest aggregate with status transitions

* feat(billing): persist wallet/ledger/topup tables + filter invoice unique index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(billing): wallet/topup DTOs + string-enum mappings

* feat(billing): wallet top-up invoice generation and credit-on-paid

Adds GetOrCreateWalletAsync, CreateTopupInvoiceAsync to IBillingService/BillingService.
MarkInvoicePaidAsync now credits the tenant wallet and completes the TopupRequest in the
same unit of work when Purpose == Topup. Invoice.CreateTopupDraft convenience factory
added. Invoice number scheme: TOP-{yyyyMM}-{tenantToken}-{requestSuffix} ensures
collision-safety for multiple top-ups in the same month.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(billing): guard topup-invoice to Pending requests + eager-load wallet ledger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(billing): tenant wallet + topup-request endpoints

Adds the three dashboard-facing wallet endpoints under
api/v1/billing: GET /wallet/me, POST /wallet/topup-requests,
GET /wallet/topup-requests/me. Handlers root-gate to caller
tenant via IMultiTenantContextAccessor; BillingDbContext is
not EF-filtered so every handler explicitly filters by
callerTenantId. Validators enforce Amount bounds and
pagination invariants. Integration tests confirm happy-path
and cross-tenant isolation (Tenant B cannot see Tenant A's
requests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(billing): operator list/approve/reject topup-request endpoints

Adds three admin-facing endpoints under api/v1/billing/wallet/topup-requests:
- GET  (root cross-tenant list, non-root scoped to own tenant)
- POST /{id}/approve  (creates+issues Topup invoice via IBillingService)
- POST /{id}/reject   (transitions request to Rejected)

Root-gating mirrors GetInvoicesQueryHandler. Approve/reject require
BillingPermissions.Manage and use .WithIdempotency(). All handlers are
public sealed with ValueTask<T> and ConfigureAwait(false). Three validators
added (Architecture.Tests requires validator per command/paginated-query).
Architecture.Tests extended with Approve/Reject as valid endpoint verb prefixes.
4 integration tests (approve happy-path, reject happy-path, cross-tenant isolation,
root tenantId filter) + 12 unit validator tests — all green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(billing): reject of non-pending topup returns 4xx, not 500

Add an explicit Pending-state guard in RejectTopupRequestCommandHandler
that throws CustomException(HttpStatusCode.Conflict) before delegating
to the domain Reject() method.  Without the guard the domain's
InvalidOperationException fell through to the global 500 branch.

Add integration test Reject_of_already_rejected_request_returns_409_Conflict
to TopupApprovalTests that asserts a second reject on the same request
returns 409 Conflict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(dashboard): WhatsApp wallet page + request top-up

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(admin): top-up requests review/approve/reject page

* test(e2e): wallet topup request + admin approval flows

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(billing): enforce exactly-once topup credit via unique partial index

Top-up wallet credit on invoice-paid was exactly-once only via an
in-process status check; two concurrent MarkInvoicePaid calls on the same
top-up invoice could both credit and commit. Add a unique partial index on
WalletTransactions(ReferenceId) filtered to Kind=Topup so a second ledger
row for the same top-up request cannot be inserted -- the concurrent second
transaction violates the constraint and rolls back, leaving exactly one
credit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1314)

Resolves CodeQL cs/exposure-of-sensitive-information (alert #27). The
delivery-failure warning logged the raw recipient address, writing PII to
an external sink (the log). Drop {Email} from the template; {Context}
already identifies the operation for debugging.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull merge-conflict Resolve conflicts manually

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants