After /tool/signup the client switches to SID-only headers (no UDID), so
the next request's body can't be decrypted unless the server already
knows the SID's UDID. ShadowverseSessionService now mirrors the client's
Cute/Cryptographer.MakeMd5(viewerId + udid) formula (salt
"r!I@ws8e5i="), and ToolController.Signup prestores the mapping at the
end. Verified against a live signup capture: viewerId=1 +
udid=62747917-93bc-454c-abb4-ef423b3c9317 produces the captured SID
dc4aac79d35fe15dfb6262e0071bb03c.
Note: this only fixes the fresh-signup path. Clients restarting with a
cached viewer_id (which skip /tool/signup entirely) still hit the same
issue — separate follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the portal pair (shadowverse-portal.com deck-builder endpoints) as
anonymous routes on the app server. The translation middleware learns a new
[NoWireEncryption] attribute that skips both AES calls but keeps the rest of
the msgpack + base64 + envelope pipeline intact, matching prod's portal wire
profile observed in data_dumps/traffic_prod_deckcode.ndjson.
Storage is a 3-minute IMemoryCache — codes are anonymous-global, 4-char
lowercase alphanumeric (matches the shortest prod sample). Foil bit is
stripped on mint to match prod's normalize-on-encode behaviour.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EnsureCurrentAsync now takes viewerId (was Viewer), so it works with
LoadController's AsNoTracking-loaded detached viewers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three endpoints + 9 integration tests. Captured-data-is-catalog: viewer's
achievement Level starts at MIN(Level) per type from the catalog (not 1),
so the assembler always has a row to render against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests intentionally deferred to controller integration tests (Tasks
18-21) which exercise the assembler end-to-end via the wire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads existing state from DB on each call (don't trust navigation
property — caller may pass it stale or double-tracked). Adds via DbSet
only, not via navigation property, to avoid EF double-tracking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Also wires IMissionCatalogRepository + IViewerMissionRepository +
IMissionProgressService into DI. Task 17's separate DI step is now
subsumed by these registrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Crystal synthesis in BuyPremiumAsync is now unconditional: always remove
any crystal entry ApplyAsync may have added, then append the fresh
post-deduction total. Prevents stale on-screen balances when a retroactive
grant also touches crystal (or when no grants fire and the conditional
guard would have been the only crystal entry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BattlePassSalesPeriodInfoDto, BattlePassProductDto, BattlePassItemListResponse DTOs,
GetItemListAsync on BattlePassService (one product if not premium + CanPurchase, empty if
already premium or off-season), and the /battle_pass/item_list controller action.
2 new integration tests; all 408 pass.
GetOrCreateProgressAsync now persists the new row itself and catches
DbUpdateException on unique-constraint violations — concurrent /info
calls no longer throw 500s. BattlePassService no longer calls
SaveChangesAsync after the get-or-create. FormatWireDate uses a named
JstOffset constant instead of an inline TimeSpan.FromHours(9).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).