-
Notifications
You must be signed in to change notification settings - Fork 1
feat(subscriptions): live transfer of subscriptions from non-deleted accounts #377
base: feature/delete-account-impl
Are you sure you want to change the base?
Changes from 2 commits
ba34b3c
5f62616
91b97f1
fca7248
aa66721
5d9e7a4
0dbb1d3
a70330c
28c81f1
03d6ba2
94a3501
003c3de
b28b16a
02ea5c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { prisma } from "@/utils/prisma"; | ||
|
|
||
| /** | ||
| * Record "any authenticated act" on the account. The live-transfer contest | ||
| * window uses lastAuthAt strictly as a veto - an old owner who touches any | ||
| * authenticated route during the window cancels the pending transfer - so | ||
| * the stamp must be reliable exactly when it matters: | ||
| * | ||
| * - The write is awaited before the request proceeds (a fire-and-forget | ||
| * stamp could land after settlement locked and read the row). | ||
| * - The timestamp is database now(), the same clock that stamps the pending | ||
| * row's createdAt, so app/DB clock skew can never make a later act | ||
| * compare as older. | ||
| * - Throttling applies only while the account has no pending outgoing | ||
| * transfer. With one pending, every authenticated act is stamped | ||
| * unconditionally - a suppressed write inside the throttle window would | ||
| * otherwise leave lastAuthAt before the pending row and the transfer | ||
| * would settle despite real victim activity. | ||
| * - Failures propagate (fail closed): a failed stamp must never silently | ||
| * cost a veto. Callers fail the request with a 5xx so the client retries; | ||
| * the alternative - swallowing the error and proceeding - lets a | ||
| * transient DB blip during a contest window hand the subscription to the | ||
| * claimant despite real owner activity. An UPDATE matching zero rows | ||
| * (account deleted mid-request) is not a failure: there is no veto left | ||
| * to preserve. | ||
| */ | ||
| const STAMP_INTERVAL_MS = 5 * 60 * 1000; | ||
|
|
||
| let stampFailureForTests: Error | null = null; | ||
|
|
||
| /** Test seam: make stamp writes fail with the given error (null clears). */ | ||
| export const __setAuthActivityStampFailureForTests = ( | ||
| err: Error | null, | ||
| ): void => { | ||
| stampFailureForTests = err; | ||
| }; | ||
|
|
||
| export const stampAuthActivity = async ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| accountId: string, | ||
| knownLastAuthAt: Date | null, | ||
| ): Promise<void> => { | ||
| const withinThrottle = | ||
| knownLastAuthAt !== null && | ||
| Date.now() - knownLastAuthAt.getTime() < STAMP_INTERVAL_MS; | ||
| if (withinThrottle) { | ||
| const pending = await prisma.subscriptionTransfer.findFirst({ | ||
| where: { status: "pending", fromAccountId: accountId }, | ||
| select: { id: true }, | ||
| }); | ||
| if (!pending) return; | ||
| } | ||
| if (stampFailureForTests) throw stampFailureForTests; | ||
| await prisma.$executeRaw` | ||
| UPDATE "Account" SET "lastAuthAt" = now() WHERE id = ${accountId}::uuid | ||
| `; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.