npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@activescott/auth-provider-sms

v2.2.0

Published

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

Readme

@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

| | SmsTransport | VerificationTransport | | -------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | Who makes the code | this package | the vendor | | Who checks the code | this package | the vendor | | US A2P 10DLC registration | yours to do — brand + campaign via The Campaign Registry, days to weeks, plus monthly fees | none — 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 line | yours | the 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

| Option | Default | Description | | ----------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 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 | | messageTemplate | built-in | (code, appName) => string to customize the text | | otp.length | 6 | Digits in the code | | otp.maxAttempts | 5 | Wrong 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.