@raypx/otp
一次性密码服务。
@raypx/otp 提供一次性密码(OTP)的生成和验证服务,支持自定义存储后端和基于用途的 OTP 分类。
核心导出
import { createOtpService } from "@raypx/otp"
import type { OtpService, OtpStore, OtpPurpose, OtpRecord } from "@raypx/otp"createOtpService()
创建 OTP 服务实例。
import { createOtpService } from "@raypx/otp"
const otp = createOtpService()选项
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
store | OtpStore | 内存存储 | OTP 存储后端 |
ttlSeconds | number | 300(5 分钟) | Token 有效期 |
codeLength | number | 6 | 验证码位数 |
const otp = createOtpService({
store: myRedisStore,
ttlSeconds: 600, // 10 分钟
codeLength: 4, // 4 位数字
})API
issue()
签发 OTP。
const record = await otp.issue({
purpose: "email-verification",
recipient: "user@example.com",
})
// record.id: string (UUID)
// record.code: "482916" (6 位数字)
// record.expiresAt: Date也可以指定自定义验证码:
const record = await otp.issue({
purpose: "phone-verification",
recipient: "+8613800138000",
code: "123456", // 自定义验证码(用于测试)
})verify()
验证 OTP。
const result = await otp.verify({
id: record.id,
code: "482916",
consume: true, // 验证成功后标记为已使用
})
if (result.ok) {
// 验证成功
} else {
// result.reason: "not_found" | "expired" | "consumed" | "mismatch"
}验证失败原因:
| 原因 | 说明 |
|---|---|
not_found | 指定 ID 的 OTP 不存在 |
expired | OTP 已过期 |
consumed | OTP 已被使用 |
mismatch | 验证码不匹配 |
类型
OtpPurpose
OTP 用途标识,使用字符串类型,允许自定义用途:
type OtpPurpose = string
// 常见用途示例
"email-verification"
"phone-verification"
"password-reset"
"two-factor"OtpRecord
OTP 记录:
type OtpRecord = {
id: string // UUID
purpose: OtpPurpose // 用途
recipient: string // 接收者(邮箱或手机号)
code: string // 验证码
expiresAt: Date // 过期时间
consumedAt?: Date // 使用时间
}OtpStore
存储抽象接口,允许自定义存储后端:
interface OtpStore {
create(record: OtpRecord): Promise<void>
getById(id: string): Promise<OtpRecord | null>
markConsumed(id: string, consumedAt: Date): Promise<void>
}环境变量
| 变量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
OTP_REDIS_URL | url | - | Redis 存储 URL(用于 Redis 后端) |
OTP_TOKEN_TTL_SECONDS | number | 300 | Token 有效期(秒) |
自定义存储
默认使用内存存储,适合开发和单实例部署。生产环境建议实现 Redis 存储:
import { createOtpService, type OtpStore } from "@raypx/otp"
const redisStore: OtpStore = {
async create(record) {
await redis.set(`otp:${record.id}`, JSON.stringify(record), "EX", 300)
},
async getById(id) {
const data = await redis.get(`otp:${id}`)
return data ? JSON.parse(data) : null
},
async markConsumed(id, consumedAt) {
const data = await redis.get(`otp:${id}`)
if (data) {
const record = JSON.parse(data)
record.consumedAt = consumedAt.toISOString()
await redis.set(`otp:${id}`, JSON.stringify(record))
}
},
}
const otp = createOtpService({ store: redisStore })