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

@weifuwujs/auth

v1.3.0

Published

weifuwu 用户系统:scrypt、会话轮换、防枚举、限流、无 JS 表单页(memory/sqlite/postgres store + 契约套件)

Readme

用户系统(@weifuwujs/auth)

零配置、安全默认的用户系统:scrypt、服务端不透明 session、cookie 属性、登录轮换、防枚举、 限流、Origin 校验、单次短 TTL 重置 token。不进主入口,不 import 不背依赖。

五分钟闭环

npx @weifuwujs/weifuwu init my-app
cd my-app
cp -r "$(npm root)/@weifuwujs/auth/templates/react-router-auth/." .
npx @weifuwujs/auth migrate   # 生成 data/auth.db
npm run dev                   # http://localhost:3000/login

页面为 React Router 无 JS 表单(渐进增强):注册 / 登录 / 忘记密码 / 账户(改密、登出)。 叠加层(templates/react-router-auth)内容:

| 文件 | 作用 | | --- | --- | | app/auth.ts | createAuth() 单例(默认 sqliteAuthStore('data/auth.db')) | | app/server.ts | use(auth.middleware).mount(auth) + use('/account', auth.require()) + 注入 authUser | | app/routes/{login,register,reset-password,account}.tsx | 无 JS 表单页(PRG:失败 303 回页带 ?error=) |

端点

挂载:app.mount(auth)(JSON);同一端点同时接受 application/x-www-form-urlencoded (表单 → 303 + Location,页面回显错误)。

| 方法 | 路径 | 说明 | | --- | --- | --- | | POST | /auth/register | 注册(201;表单 303) | | POST | /auth/login | 登录,轮换旧会话(防 session fixation) | | POST | /auth/logout | 登出并清 cookie | | GET | /auth/me | 当前用户(ctx.auth.user 每请求现查) | | PATCH | /auth/me | 改邮箱(需当前密码) | | POST | /auth/password/change | 改密(保留当前会话,踢其他) | | POST | /auth/password/forgot | 恒 200(防枚举);未配置 hook 时 dev 打印链接、生产静默 | | POST | /auth/password/reset | 单次 token;过期/复用一律无效 |

配置

// app/auth.ts
import { createAuth } from '@weifuwujs/auth'
import { sqliteAuthStore } from '@weifuwujs/auth/sqlite'

export const auth = createAuth({
  store: sqliteAuthStore('data/auth.db'),
  session: { cookieName: 'ww_session', maxAge: 30 * 24 * 3600 }, // secure 默认随生产环境开启
  rateLimit: { max: 10, windowMs: 60_000 },   // 默认每 IP 每分钟 10 次
  origin: false,                               // 默认同源校验;反代跨域时显式列白名单
})

// app/server.ts
const app = createApp().use(auth.middleware).mount(auth)
app.get('/orders', auth.require(), listOrders)
app.get('/billing', auth.require((user) => user.role === 'admin'), billing)

最小可跑示例(零依赖,node 直接运行;test/batteries/readme-examples.test.ts 会真跑它):

import assert from 'node:assert/strict'
import { createApp } from '@weifuwujs/weifuwu'
import { createAuth, memoryAuthStore } from '@weifuwujs/auth'

const auth = createAuth({ store: memoryAuthStore(), rateLimit: { max: 1000 } })
const app = createApp().use(auth.middleware).mount(auth)

const post = (path: string, body: unknown) =>
  app.fetch(new Request(`http://readme${path}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  }))

assert.equal((await post('/auth/register', { email: '[email protected]', password: 'secret123' })).status, 201)
assert.equal((await post('/auth/login', { email: '[email protected]', password: 'secret123' })).status, 200)
assert.equal((await post('/auth/login', { email: '[email protected]', password: 'wrong' })).status, 401)

示例标注:ts exec = 由测试真跑;ts fragment = 片段(依赖前后文,不独立运行);自足示例带包标签(如 ts tasks),由测试编译。

  • ctx.auth = { user, login(user), logout(), verifyPassword(plain) };
  • auth.require():HTTP 未登录 → 页面 302 /login?next=…、API 401;角色/谓词不过 → 403; WS 升级未登录 → 401 拒绝;
  • 只有一个中间件实例:HTTP 用 app.use(auth.middleware),WS 同时用 auth.install(app)(use + wsUse)。

换 store

import { postgresAuthStore } from '@weifuwujs/auth/postgres'
const auth = createAuth({ store: postgresAuthStore(process.env.DATABASE_URL!) })

自定义 store 用契约套件自证(memory / sqlite / postgres 跑的是同一套):

import { authStoreContract } from '@weifuwujs/auth/testing'
authStoreContract('my-store', () => myAuthStore())

工厂会被套件多次调用:每次须返回独立的 store(sqlite 驱动用 ':memory:' 或唯一路径)—— 复用同一文件会状态泄漏并假红。

AuthStore 可拆(UserStore / SessionStore / ResetTokenStore),listUsers?() 为可选管理能力。

换 hasher(存量 bcrypt 惰性迁移)

import { scryptHasher } from '@weifuwujs/auth'
import { createAuth } from '@weifuwujs/auth'

const bcryptHasher = {
  async hash(plain: string) {
    return doBcryptHash(plain) // 你的实现
  },
  verify(plain: string, stored: string) {
    if (stored.startsWith('$2')) return doBcryptCompare(plain, stored)
    return scryptHasher.verify(plain, stored)
  },
  needsRehash: (stored: string) => stored.startsWith('$2'),
}

createAuth({ store, passwordHasher: bcryptHasher })

登录成功后 needsRehash 为真时自动用 hash() 重算并落库;hash/verify 允许同步或异步。

hooks

createAuth({
  store,
  hooks: {
    onRegister: (user) => analytics.track('signup', user.id),
    onLogin: (user) => audit.log('login', user.id),
    onPasswordChange: (user) => mailer.notify(user.email),
  },
})

hooks 被 awaited;抛错只记日志,不改变请求成败(外发邮件、统计不得拖垮登录)。 sendResetToken(user, url) 单独配置:未配置时 dev 打印链接、生产静默不发(防枚举)。

侧表 profile(业务字段不进 users)

// 业务字段放自己的表/服务,用示例中间件按需加载
const loadProfile: Middleware<Context, Context & { profile?: Profile }> = async (
  req, ctx, next,
) => next(req, { ...ctx, profile: await profiles.byUserId(ctx.auth?.user?.id) })

const app = createApp()
  .use(auth.middleware)
  .use(loadProfile)
  .mount(auth)

users 只存身份字段;ctx.auth.user(PublicUser)永不携带业务字段与 passwordHash。

CLI(不加载用户代码)

weifuwu-auth migrate [--store sqlite:data/auth.db|postgres://…]
weifuwu-auth user:list
weifuwu-auth user:disable [email protected]
weifuwu-auth user:reset-password [email protected] [--password <plain>]

user:reset-password 未指定明文时生成 24 字节随机密码并只打印一次。

CLI 惯例(1.3 起五包统一,见 docs/alignment.md):无命令 / 未知命令 → USAGE exit 2 且不写 stdout(此前静默 exit 0);--help → stdout + exit 0;错误走 stderr 且前缀 CODE: 只出现一次; 退出码 0 成功 / 1 运行时错误 / 2 用法错误。

测试辅助(/testing)

自定义 AuthStore 驱动与内置驱动(memory / sqlite / postgres)共跑同一套契约:

import { authStoreContract } from '@weifuwujs/auth/testing'

authStoreContract('my-store', () => myAuthStore())
// 第三个参数可选:{ skip: true } 或 { skip: '未实现清理' }

契约覆盖:user CRUD 与唯一 email、session 生命周期与过期清理、reset token 单次消费与过期。

安全默认清单

| 项 | 默认 | | --- | --- | | 密码 | scrypt(scrypt$N$r$p$salt$hash),可插拔 + 惰性重算 | | 会话 | 只存 token 哈希;登录轮换;改密踢其他;disable/过期即时失效 | | cookie | HttpOnly + SameSite=Lax + Path=/ + Max-Age;Secure 按配置 | | 防枚举 | 错误凭据统一文案;forgot 恒 200;时序均衡(dummy hash) | | 限流 | 登录/注册/重置默认每 IP 每分钟 10 次(429 + retry-after) | | Origin | 默认同源;表单 POST 与 JSON 同校验 | | 重置 | token 单次、短 TTL(默认 15 分钟)、只存哈希 |

边界:单租户;不做 OAuth / 2FA / 邮箱验证 / 审计事件(P2),不做支付与后台模板。