@activescott/auth-provider-sms

中文文档
2.3.0发布于 2026/10/07MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 4676

SMS one-time-code provider for @activescott/auth

原始使用文档
文档目录6 节
  1. Two kinds of transport
  2. Usage
  3. Configuration
  4. Phone numbers
  5. Testing pattern: capture transport
  6. Security model

@activescott/auth-provider-sms

npm version License: MIT

SMS one-time-code provider for @activescott/auth. The user enters their mobile number, gets texted a 6-digit code, and types (or autofills) it to sign in.

This package has no vendor dependencies — delivery is injected as a transport. Use a vendor package or write your own:

  • @activescott/auth-sms-twilio — Twilio: TwilioMessagingTransport (SMS, or RCS via a Messaging Service) and TwilioVerifyTransport (Twilio Verify)
  • ConsoleTransport (included) — prints codes to the server console for development
  • Custom: implement SmsTransport { sendMessage(to, message): Promise<boolean> }. An AWS End User Messaging transport is drafted in PR #37 — implemented and unit-tested but unverified against a live AWS account; if you want AWS and can test it end to end, feel free to take over that PR (#36 has the checklist).

Two kinds of transport

SmsTransportVerificationTransport
Who makes the codethis packagethe vendor
Who checks the codethis packagethe vendor
US A2P 10DLC registrationyours to do — brand + campaign via The Campaign Registry, days to weeks, plus monthly feesnone — the vendor sends from senders it already registered
Cost per sign-in (US)~$0.011–0.013 per SMS~$0.058 (Twilio Verify: $0.05 per success + channel fee)
Message text, code length, WebOTP lineyoursthe vendor's

SmsProvider accepts either. Routes, request bodies, cookie binding, abuse checks, and error responses are identical, so switching is a one-line change at construction:

new SmsProvider({ appName: "MyApp" }, new TwilioVerifyTransport({ ... }))

Pick VerificationTransport when 10DLC registration is the thing standing between you and shipping — at low sign-in volume its fixed monthly fees often exceed the entire Verify bill. Pick SmsTransport when volume makes per-message price dominate, or you want control over the message text.

Usage

import { Auth, InMemoryChallengeStore } from "@activescott/auth"
import { SmsProvider, ConsoleTransport } from "@activescott/auth-provider-sms"
import { TwilioMessagingTransport } from "@activescott/auth-sms-twilio"

const auth = new Auth({
  session: { secret: process.env.JWT_SECRET! /* ... */ },
  userStore,
  identityStore,
  challengeStore: new InMemoryChallengeStore(), // DB-backed in production
  providers: [
    new SmsProvider(
      { appName: "MyApp", webOtpDomain: "myapp.example" },
      process.env.NODE_ENV === "production"
        ? new TwilioMessagingTransport({
            accountSid: process.env.TWILIO_ACCOUNT_SID!,
            authToken: process.env.TWILIO_AUTH_TOKEN!,
            messagingServiceSid: process.env.TWILIO_SMS_MESSAGING_SERVICE_SID,
          })
        : new ConsoleTransport(),
    ),
  ],
})

The login form posts the phone number to /auth/sms/initiate (the provider texts the code, sets an HttpOnly challenge cookie, and redirects back with ?sent=1); the code form posts to /auth/sms/verify. A runnable app demonstrating the full flow is at examples/react-router:

<form method="post" action="/auth/sms/initiate">
  <input name="phone" type="tel" autocomplete="tel" required />
  <button>Text me a code</button>
</form>

<form method="post" action="/auth/sms/verify">
  <input
    name="code"
    autocomplete="one-time-code"
    inputmode="numeric"
    pattern="[0-9]{6}"
    required
  />
  <button>Sign in</button>
</form>

Configuration

OptionDefaultDescription
appName"App"Shown in the default message text
expiry"10m"Code lifetime ("30s", "10m", "1h", ...)
webOtpDomain(off)Appends the WebOTP line @domain #code for Android/Chrome one-tap autofill
messageTemplatebuilt-in(code, appName) => string to customize the text
otp.length6Digits in the code
otp.maxAttempts5Wrong guesses before the challenge is invalidated (RATE_LIMITED)
otp.cookieName"auth_sms_challenge"Challenge cookie name

With a VerificationTransport the vendor composes the message, so appName, messageTemplate, webOtpDomain, and otp.length do nothing — configure those at the vendor instead. expiry, otp.maxAttempts, and otp.cookieName still apply; expiry bounds this package's challenge record, while the vendor enforces its own code lifetime, and whichever expires first ends the attempt.

The default message (with webOtpDomain set) looks like:

Your MyApp sign-in code is: 123456

@myapp.example #123456

The last line is the origin-bound one-time code format (a WICG spec co-edited by Apple and Google — what WebOTP and Safari's code autofill parse). The human-readable sentence matters too: Apple publishes no exact grammar for iOS Security Code AutoFill, which recognizes codes heuristically from the message text (see autocomplete="one-time-code" on MDN) — Your <app> sign-in code is: <code> follows the widely observed patterns. Custom templates should keep a similar shape for autofill to work.

Phone numbers

Input is normalized (spaces, dashes, dots, parentheses stripped; leading 00 → +) and must then be valid E.164 (+ and country code required) — the provider never guesses a default country. normalizePhoneNumber is exported if you want the same normalization client-side.

Testing pattern: capture transport

In e2e tests, wrap your SmsTransport in CaptureSmsTransport and read the code back through a test-only route (gated on a test-mode env var and a shared-secret header) instead of needing a phone — or paying for the text.

import { CaptureSmsTransport } from "@activescott/auth-provider-sms/testing"

export const smsTransport =
  process.env.E2E_TEST_MODE === "true"
    ? new CaptureSmsTransport(new TwilioTransport({ ... }))
    : new TwilioTransport({ ... })

// in your gated readback route:
const captured = smsTransport.getCapturedSms(phone) // { message, code }

The code is parsed out of the default message template; pass a second argument to CaptureSmsTransport when you configure a custom messageTemplate. It lives at the /testing subpath rather than the package root so it stays out of your application's default import graph.

Security model

Same challenge model as the email provider: one server-side challenge per send, code stored only as a salted SHA-256 hash, attempts counted before each comparison (capped at maxAttempts), constant-time compare, single-use (deleted on success), and the challenge is bound to the initiating browser by an HttpOnly cookie — a code alone is useless without it.

With a VerificationTransport the code never reaches your server at all, so there is no hash to store; the challenge record holds the phone number and the vendor's reference. Everything else is unchanged, including counting the attempt before the vendor is asked — which also caps how many billable checks one challenge can produce.

Per-number and per-IP send throttling ships in @activescott/auth and is on by default (see its abuse protection section); vendor-level fraud/pumping guards (Twilio, AWS) are still worth enabling on top.

If you think any of this should work differently — protections that belong in this library, a weakness in the model — please open an issue or PR to discuss.

版本与发布记录

@activescott/auth-provider-sms 共发布 12 个版本,首次发布于 2026/08/02;过去 12 个月发布了 12 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.3.0latest2026/10/071 个月前
2.2.02026/09/191 个月前
2.1.02026/08/122 个月前
2.0.02026/08/122 个月前
1.1.02026/08/122 个月前
1.0.02026/08/092 个月前

关于 @activescott/auth-provider-sms 的常见问题

@activescott/auth-provider-sms 是什么?

SMS one-time-code provider for @activescott/auth

如何安装 @activescott/auth-provider-sms?

在项目目录运行 npm install @activescott/auth-provider-sms,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@activescott/auth(^5.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@activescott/auth-provider-sms 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@activescott/auth-provider-sms 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@activescott/auth-provider-sms 可以商用吗?

@activescott/auth-provider-sms 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@activescott/auth-provider-sms 的下载量有多少?

根据 npm 官方下载统计,@activescott/auth-provider-sms 在 2026/10/05 – 2026/11/03 这 30 天内被下载 4,676 次,日均约 156 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@activescott/auth-provider-sms 还在维护吗?

最新版本 2.3.0 发布于 2026/10/07(1 个月前);至今共发布 12 个版本,其中过去 12 个月发布了 12 个;首次发布于 2026/08/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。