Raypx

@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()

选项

参数类型默认值说明
storeOtpStore内存存储OTP 存储后端
ttlSecondsnumber300(5 分钟)Token 有效期
codeLengthnumber6验证码位数
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 不存在
expiredOTP 已过期
consumedOTP 已被使用
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_URLurl-Redis 存储 URL(用于 Redis 后端)
OTP_TOKEN_TTL_SECONDSnumber300Token 有效期(秒)

自定义存储

默认使用内存存储,适合开发和单实例部署。生产环境建议实现 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 })

On this page