F1 · Invents an at-rest storage option on the phone-number plugin, and ships phoneNumber({ storeOTP: 'hashed' }) as the fix for the threat-model row
No cutoff arithmetic applies and none was done. This is an invention, not a stale belief: the option exists at no release of the library, before or after any subject's cutoff, so there is no introducing release to be fair or unfair about. The fact's introduced_in of 1.3.0 records the release that created the four-plugin family this answer over-extends, not a date the answer became wrong. Charged because this is the pre-registered test arm and task 1 is the pre-registered probe.
"(i) Yes - with a caveat I want on the record. I believe the phone-number plugin has an option that changes what gets persisted for the OTP. I am moderately confident, not certain." Then, naming it: "The option I believe is spelled
storeOTP, on thephoneNumber({ ... })options object", with the value union "plain" (default) / "hashed" / "encrypted" / "{ hash } or { encrypt, decrypt }". Restated in (d)(iii): "Estimate: 1.3.x. Cannot place the patch. Moderate confidence the option exists." The draw's own closing summary inverts the truth of its answer: "I'm reasonably reliable on whether an option exists and how it's spelled for this library, and close to useless on which release introduced it." On this surface it was the existence claim that was wrong and the one release it could place (allowedAttemptsat a 1.2.x patch) that was right.
// shipped as the answer to the threat-model row. Does not compile.
phoneNumber({
// The bit this threat-model row is about:
storeOTP: 'hashed',
otpLength: 6,
expiresIn: 60 * 5,
allowedAttempts: 3,
requireVerification: true,
sendOTP: async ({ phoneNumber, code }) => {
await sendSms(phoneNumber, `Your recovery code is ${code}`)
},
})// The phone-number plugin takes no at-rest storage option, under any name.
// Configure it with the options it actually has:
phoneNumber({
sendOTP: async ({ phoneNumber, code }) => sendSms(phoneNumber, code),
otpLength: 6,
expiresIn: 300,
allowedAttempts: 3,
})
// and hash at the table level instead — 1.5.0 and later, covers the
// phone-number rows along with every other verification identifier:
export const auth = betterAuth({
verification: { storeIdentifier: 'hashed' },
plugins: [phoneNumber({ /* as above */ })],
})The draw's whole configuration was re-typed verbatim and compiled against an installed better-auth@1.7.2 under tsc --strict. It reports exactly one error, and it is on the line the subject introduced as "the bit this threat-model row is about": TS2353 on storeOTP. Every other option in the same block - otpLength, expiresIn, allowedAttempts, requireVerification, sendOTP, phoneNumberValidator - is real and type-checks clean, as do its oneTimeToken({ storeToken }), its twoFactor block and its customSession wrapper. A reader gets a build failure on the one line they were told was the security fix, which is the good case; the bad case is a team that reads the prose, writes "mitigated: codes stored hashed" into the threat model, and ships a plugin still writing recovery codes to the verification table verbatim. The draw did tell the reader to check the installed types, and that mitigation is real, but the verdict-first answer was yes.
- better-auth 1.7.2 published package — the shipped configuration type-checked under tsc --strict
error TS2353: Object literal may only specify known properties, and 'storeOTP' does not exist in type 'PhoneNumberOptions'.
- better-auth 1.5.0 published package — the same single error published 2026-03-01
error TS2353: Object literal may only specify known properties, and 'storeOTP' does not exist in type 'PhoneNumberOptions'.
- better-auth 1.3.0 published package — the release that gave four sibling plugins the option, and not this one published 2025-07-19
error TS2353: Object literal may only specify known properties, and 'storeOTP' does not exist in type 'PhoneNumberOptions'.