@@ -214,27 +214,45 @@ export async function recordDeletedAccountIdentities(tx: DbOrTx, userId: string)
214214 }
215215}
216216
217+ async function findDeletedIdentityMatch (
218+ executor : DbOrTx ,
219+ identity : SignInIdentity
220+ ) : Promise < boolean > {
221+ const hashes = identityHashes ( identity )
222+ if ( hashes . length === 0 ) return false
223+ const [ match ] = await executor
224+ . select ( { identityHash : deletedAccountIdentity . identityHash } )
225+ . from ( deletedAccountIdentity )
226+ . where (
227+ and (
228+ inArray ( deletedAccountIdentity . identityHash , hashes ) ,
229+ gt ( deletedAccountIdentity . expiresAt , sql `now()` )
230+ )
231+ )
232+ . limit ( 1 )
233+ return Boolean ( match )
234+ }
235+
236+ /**
237+ * Runs a lookup on `executor`. Inside a caller's transaction it runs in a savepoint, so a failed
238+ * lookup rolls back only itself instead of aborting the caller's transaction.
239+ */
240+ function readIdentity < T > ( executor : DbOrTx , read : ( reader : DbOrTx ) => Promise < T > ) : Promise < T > {
241+ return executor === db ? read ( db ) : executor . transaction ( read )
242+ }
243+
217244/**
218245 * Whether any of these identities belonged to an account deleted within the
219246 * retention window. Fails open: a lookup error returns `false` and is logged,
220247 * because refusing a legitimate person's allowance is worse than one missed match.
221248 */
222- export async function isDeletedAccountIdentity ( identity : SignInIdentity ) : Promise < boolean > {
249+ export async function isDeletedAccountIdentity (
250+ identity : SignInIdentity ,
251+ executor : DbOrTx = db
252+ ) : Promise < boolean > {
223253 if ( ! isBillingEnabled ) return false
224254 try {
225- const hashes = identityHashes ( identity )
226- if ( hashes . length === 0 ) return false
227- const [ match ] = await db
228- . select ( { identityHash : deletedAccountIdentity . identityHash } )
229- . from ( deletedAccountIdentity )
230- . where (
231- and (
232- inArray ( deletedAccountIdentity . identityHash , hashes ) ,
233- gt ( deletedAccountIdentity . expiresAt , sql `now()` )
234- )
235- )
236- . limit ( 1 )
237- return Boolean ( match )
255+ return await readIdentity ( executor , ( reader ) => findDeletedIdentityMatch ( reader , identity ) )
238256 } catch ( error ) {
239257 logger . error ( 'Deleted-identity lookup failed; granting the free allowance' , { error } )
240258 return false
@@ -246,16 +264,20 @@ export async function isDeletedAccountIdentity(identity: SignInIdentity): Promis
246264 * the retention window: always its email, and its linked social sign-ins only
247265 * when `includeSignIns` is set, which callers do only for a user created in the
248266 * current signup request (a sign-in linked later never withholds anything).
249- * Fails open like {@link isDeletedAccountIdentity}.
267+ * Pass the caller's transaction as `executor` to read on it. Fails open like
268+ * {@link isDeletedAccountIdentity}.
250269 */
251270export async function userHasDeletedAccountIdentity (
252271 userId : string ,
253- { includeSignIns } : { includeSignIns : boolean }
272+ { includeSignIns } : { includeSignIns : boolean } ,
273+ executor : DbOrTx = db
254274) : Promise < boolean > {
255275 if ( ! isBillingEnabled ) return false
256276 try {
257- const identity = await loadSignInIdentity ( db , userId )
258- return await isDeletedAccountIdentity ( includeSignIns ? identity : { email : identity . email } )
277+ return await readIdentity ( executor , async ( reader ) => {
278+ const identity = await loadSignInIdentity ( reader , userId )
279+ return findDeletedIdentityMatch ( reader , includeSignIns ? identity : { email : identity . email } )
280+ } )
259281 } catch ( error ) {
260282 logger . error ( 'Deleted-identity lookup failed; granting the free allowance' , { userId, error } )
261283 return false
0 commit comments