@activescott/auth

中文文档
5.9.0发布于 2026/10/19MITTypeScriptES Module构建来源已验证近 30 天下载 9347

框架不可知性认证, 提供方模式用于魔法链接, OAuth, 以及更多

中文使用文档
文档目录19 节
  1. 为什么直接的,没有密码的认证?
  2. 特征
  3. 文档实例( E)
  4. 安装
  5. 盒子里的东西
  6. 数据模型
  7. 最小形状
  8. 会话缓存
  9. 挑战斯托尔
  10. 保护虐待
  11. 一个被封锁的呼叫者看到成功
  12. 调试
  13. 窗体符号
  14. 托管的机器人检查
  15. 启动闸门
  16. 管理子路径
  17. 日志
  18. 等待名单
  19. 许可证

@ activescott/ auth (英语)

npm version License: MIT

框架不可知的直接认证,刻意小:单用魔法链接和一次性代码通过电子邮件和短信,以及密码(WebAuthn). 没有第三方身份提供者。 在节点和边缘运行时间(如Cloudflare Workers)上运行.

这个包是核心: Auth 类,JWT-cookie会话管理,以及提供者/存储界面. 它不单独处理任何具体的认证方法——将其与一个或多个提供者软件包和(可选的)框架适配器配对:

用于生产 frumblefeed.com (英语) 财务报告和审定财务报表 (原始内容存档于2018-10-21). dinkerbellbot.com。 。 。 .

为什么直接的,没有密码的认证?

每个人都有一个电子邮件地址或电话号码。 没有人想要另一个密码。 而许多用户对"与Google/Apple/Microsoft签约"犹豫不决,因为它与第三方分享他们的签入活动. 这个库主要研究一个人验证的方法 直接 用你的应用程序:

  • 用户的摩擦度最低. 没有创建、忘记或重置的密码, 也没有需要第三方的账户 。 现代平台自动填充我们发送的代码,所以签名是: 输入您的电子邮件,输入您的操作系统提供的代码.
  • 对你最轻松. 没有OAuth应用程序注册,没有身份提供仪表板,没有额外服务. SMTP 服务器和您的数据库是唯一的依赖性 .
  • 私人设计. 循环中没有任何第三方身份提供商——大技术不会在用户在你的应用上签名时得知(或那).
  • 故意地小取来. 这不是一个与Outh-professor auuth 库合作的作品——像这样的项目很好地服务于这一优势 更好的认证限制范围 使得它很容易掉入一个新的应用.

Passkeys更进一步地推开同一想法:防打钓鱼,没有共同的秘密,仍然没有第三方.

特征

  • ✅ 电子邮件魔法链接 ——单用,服务器后置的登录链接,并有一个确认步骤,即电子邮件安全扫描仪无法消耗(参见 财务问题) (中文(简体) ). 生产中.
  • ✅ 电子邮件一次性代码 ——每个登录的电子邮件中也包含一个带有iOS/macOS AutoFill支持的数字代码,因此用户可以输入代码而不是切换到收件箱分页.
  • ✅ 带上自己的数据库 3个小商店接口(IdentityStore, (中文(简体) ), UserStore, (中文(简体) ), ChallengeStore;用Prisma, Drizzle,生生SQL,Redis,任意使用.
  • ✅ 边缘准备, 冬季TC兼容 核心项目 ——标准获取 Request编号 :Response,网络晶体,以及 jose 用于会话 JWTs; 无节点的 API, 因此它运行在 Cloudflare Workers, Deno, Bun, 以及 WinterTC 相通的运行时间 .
  • ✅ 反应路由器 v8 适配器 —— 说 createAuthHandlers, (中文(简体) ), requireAuth, (中文(简体) ), optionalAuth, (中文(简体) ), getSession, (中文(简体) ), logout。 。 。 .
  • ✅ 短消息一次性代码 - 供应商中立的供应商,配有Twilio通讯运输(RCS-ready)和 网络OTP 自动填充支持.
  • ✅ 主机核查(无美国A2P 10DLC) - 同一个短信提供商接受 VerificationTransport 由供应商生成、发送和检查密码. TwilioVerifyTransport twilio套装中的船舶:没有可购买的号码,没有品牌或竞选注册——每个签入的费用为~4–6x.
  • ✅ 侵权保护,默认 ——每个IP和每个接收者的费率限额,最小的填充时间检查,被屏蔽的尝试被记录,被屏蔽的呼叫者得到的响应与成功发送会产生的相同. 可选包添加主机检查( Q)转弯) (中文(简体) ).
  • ✅ 密码( WebAuthn) ——在签入时添加一个密码,然后在无用户名下签名,使用触摸ID,Face ID,Windows Hello, 1Password, iCloud keychain,或安全密钥;有条件的UI(passkey自動填充)支持. 通过 @simplewebauthn/server;包含零依赖性浏览器客户端.

提供者接口( E)AuthProvider)是扩展点。 实施一个新的供应商不需要修改这个核心包.

文档实例( E)

Full docs——快速起步,架构图,自定义提供方指南,e2e测试模式,FAQ——和一个可运行的React Router框架-mode实例,其中Playwright测试在单曲中直播:

→ https://github.com/活塞斯科特/奥斯** 联合国

其余的README涵盖这个核心包本身出口和预期的内容.

安装

npm install @activescott/auth

盒子里的东西

导出目的
Auth圆相相. 给提供者的路由认证请求, 管理会话 cookie .
SessionManager独立的JWT会话签名人/验证人(很少直接需要).
AuthProvider接口每个提供者工具( E)initiate, (中文(简体) ), verify, (中文(简体) ), canHandle选项 handleAction 页:1.
IdentityStore, (中文(简体) ), UserStore用于插入数据库的接口.
ChallengeStore, (中文(简体) ), InMemoryChallengeStore为短期单一用途挑战而储存(见下文).
AbuseConfig, (中文(简体) ), RateLimitStore, (中文(简体) ), InMemoryRateLimitStore对起始终点的滥用保护——默认(见下文).
InitiateGate有关谁可以开始登录或链接的您自己的政策(见下文).
createWaitlist, (中文(简体) ), waitlistNotificationEmail等待名单,经管理员批准,建在大门上 onVerified (见下文).
BotCheckProvider, (中文(简体) ), createFormToken, (中文(简体) ), FORM_TOKEN_FIELDBot检查接口和登录表的反机器人字段.
generateOtpCode, (中文(简体) ), hashOtpCode, (中文(简体) ), verifyOtpCode检察官办公室有能力提供商使用的一次性编码公用事业.
AuthUser, (中文(简体) ), Identity, (中文(简体) ), Session, (中文(简体) ), AuthResult, (中文(简体) ), AuthInitResult核心数据类型.
AuthErrors, (中文(简体) ), getAuthErrorMessage, (中文(简体) ), AUTH_ERROR_CODES结构化错误帮助.

再加上 @activescott/auth/admin 子路径: 管理员仪表板数据和管理员许可列表检查(参见 管理子路径) (中文(简体) ).

数据模型

你带了三个适配器 IdentityStore, (中文(简体) ), UserStore,以及 ChallengeStore - 读出/写出你的数据库。 库处理挑战,饼干,提供者的路由,以及会话验证.

一个 Identity 是一个 (provider, identifier) 配对(例如:. ("email", "[email protected]")联系到你的一个 User 记录。 一个用户可以拥有多个身份——电子邮件,电话,和密码都使用同一个表格.

Identity.metadata 实值 供应商拥有的国家,与您的申请不透明:坚持未修改的(JSON/JSONB一栏),并如所储存的那样返回。 拥有每个身份状态的供应商保存在那里——密码提供者存储每个证书的公用钥匙和签名柜台——和无国籍供应商存储 {}。它可能包含敏感材料,因此保护它,如证书数据(休息时加密是一种合理的默认). IdentityStore.update(id, {metadata, verifiedAt}) 替换存储的元数据批发; 提供者依赖它, 因此它是一种必需的方法.

最小形状

import { Auth, InMemoryChallengeStore } from "@activescott/auth"
import { EmailProvider } from "@activescott/auth-provider-email"

const auth = new Auth({
  session: {
    secret: process.env.JWT_SECRET!,
    maxAge: "30d",
    cookieName: "session",
    cookie: { secure: true, sameSite: "lax", path: "/" },
  },
  identityStore, // your impl
  userStore, // your impl
  challengeStore: new InMemoryChallengeStore(), // DB-backed in production
  providers: [new EmailProvider({ ... })],
})

那就打电话 auth.handleRequest(request) 从您框架的路由层( 或使用框架适配器),以及 auth.verifySession(request) 检查受保护路线上的会话饼干.

会话缓存

verifySession 每个已校验的会话在内存中保存了两分钟,所以一个页面的加载器会每页询问谁在成本上签名一对商店读取,而不是每个加载器读取一对. 你为此付出的代价是沉闷:在你阻断或删除某人后最多两分钟内,他们的请求仍然得到证实.

session.cacheTtlMs 那个窗口是毫秒,和 0 关闭缓存,所以每个请求读取您的商店:

session: {
  secret: process.env.JWT_SECRET!,
  maxAge: "30d",
  cookieName: "session",
  cookie: { secure: true, sameSite: "lax", path: "/" },
  cacheTtlMs: 0, // block a user, and their next request is signed out
}

随着缓存的被打开,它最多维持了一万个会话,并驱逐了最古老的过去,因此,在扫荡之间爆发的签注无法无限制地增长. 任何事物都无法在实例之间共享:一个条目只会重复程序商店刚才说的话.

挑战斯托尔

每一次登录尝试都以服务器侧面的挑战为后盾:魔法链接和一次性代码存储被散列的秘密,一个尝试计数器,以及一个过期;密码仪式记录了WebAuthn的挑战,因此可以完全赎回一次. 这个州就住在 challengeStore,这就是为什么它必须成为 Auth 相会.

InMemoryChallengeStore 用于单一服务器进程(和dev/例)的正确性。 * 重新启动后失去挑战,没有在各种情况下分担挑战——多机构部署采用四方法 ChallengeStore 界面对共享存储。 SQL的执行大致如下:

CREATE TABLE challenges (
  id TEXT PRIMARY KEY,
  type TEXT NOT NULL,
  identifier TEXT NOT NULL,
  hashed_code TEXT,
  data JSONB,
  attempts INT NOT NULL DEFAULT 0,
  max_attempts INT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at TIMESTAMPTZ NOT NULL
);

与 incrementAttempts 作为 UPDATE challenges SET attempts = attempts + 1 WHERE id = $1 RETURNING attempts (递增必须是原子——它执行猜想限制),周期性 DELETE ... WHERE expires_at < now()。 。 。 .

保护虐待

这个 initiate 端点会把邮件和短信发送到任何来电者提交的地址,这使得它们成为了一种吸引人的方式,可以将邮件轰炸第三方,或者在弹出时烧掉您发送的名声. 保护是 默认 ——您不需要配置或执行任何东西:

层默认
每个客户端IP每分钟3分,然后每小时10分
每个收件人每小时3个,然后每天10个
最少填表时间2 秒( 只当表格显示一个符号时执行, 下面)
柜台存储InMemoryRateLimitStore
阻断反应与成功发送完全相同

被阻断的尝试总是被记录下来(console.warn)与原因,提供者,IP,请求的标识符,和规则,所以滥用爆发是可见的:

[auth] blocked initiate: reason=identifier_rate_limited provider=email ip=203.0.113.7 [email protected] rule=3/3600s retryAfter=2841s

一个被封锁的呼叫者看到成功

通过设计一个被压抑或被打上花旗的请求,得到一个真正的发送将产生的反应——同样的302回 ?sent=1,或同为 {success: true, message} -减去挑战饼干。 无所寄也无所存也. 这使得机器人无法绘制 地址或IP被挤压 如果你愿意回来 429 RATE_LIMITED (仅用于部署API的合理),设置 abuse.respondWith: "rateLimited"。 。 。 .

一个块在两种模式中都有不同的回答: 机器人检查报告 incomplete (一个在提交表格时尚未发出其符号的主机部件)获得 BOT_CHECK_INCOMPLETE,作为302的背面 ?error=BOT_CHECK_INCOMPLETE 或四百个 这个判决仅出自请求,从未出自标识,所以它告诉呼叫者他们没有发送的任何东西,它让一个在缓慢的连接上的真正用户知道再次尝试. 它仍然被记录 并传递给 onBlocked 作为 bot_check_failed。 。 。 .

调试

const auth = new Auth({
  // ...
  abuse: {
    perIp: [
      { windowSeconds: 60, max: 3 },
      { windowSeconds: 3600, max: 10 },
    ],
    perIdentifier: [{ windowSeconds: 3600, max: 3 }],
    store: myRedisRateLimitStore, // multi-instance: share the counters
    onBlocked: (event) => logger.warn(event, "auth abuse blocked"),
  },
})

abuse: { enabled: false } 关掉一切.

InMemoryRateLimitStore 每个过程的计数,所以多进制部署可以有效地将每个限制乘以例数. 实施单一方法 RateLimitStore 界面对 Redis (INCR + 键 EXPIRE)或您要共享计数器的数据库.

客户IP来自 cf-connecting-ip,则 x-forwarded-for (最右侧跳;设置) abuse.clientIp.trustedProxyHops 如果一个以上的代理附件),那么 x-real-ip。这些信头是可以伪造的,除非在您的应用程序前有一个代理重写它们——供应 abuse.clientIp.getClientIp 当您的运行时间显示对等地址时 。 当无法确定IP时,会跳过每个IP的限制,仍然适用每个接受者的限制.

窗体符号

最小形式填充时间检查需要在您的登录格式中隐藏一个字段 :

import { createFormToken, FORM_TOKEN_FIELD } from "@activescott/auth"

// in the route/loader that renders the form:
const formToken = await createFormToken(process.env.JWT_SECRET!)
<input type="hidden" name="authFormToken" value="{formToken}" />

令牌是已签名的渲染时间戳; 服务器拒绝更快到达的提交 minFormFillSeconds必须签字——没有签字的时间戳只是要伪造的另一个领域。 与 没有 指示符是允许的,所以添加字段是可选的,可以在稍后推出. 一个比一日更古老的标志也被允许,而不是被拒绝——在标签中留下的登录页是人类.

托管的机器人检查

Cloudflare Turnstile, h Captcha, 朋友住在自己的包里, 所以你只安装你使用的供应商:

import { TurnstileBotCheck } from "@activescott/auth-botcheck-turnstile"

abuse: {
  botChecks: [new TurnstileBotCheck({ secretKey: process.env.TURNSTILE_SECRET_KEY! })],
}

执行 BotCheckProvider (单位:千美元){ id, verify({ request, body, ip, providerId }) })来加入自己.

启动闸门

只邀请的β、允许列表或被封禁的域名是一条关于 谁 可能会发出签名信息。 放进去 gate.onInitiate 而不是在您的认证路径的序言中:

const auth = new Auth({
  // ...
  gate: {
    async onInitiate({ provider, identifier, mode, request }) {
      if (mode === "link") return "allow" // already signed in
      if (await invites.has(provider, identifier)) return "allow"
      return { redirect: "/waitlist" }
    },
  },
})

门开进去了 handleRequest,在提供者解析、正常化并验证标识符之后([email protected] 以 [email protected],在创建或发送任何信息之前,电话号码为E.164。 一个畸形的标识符首先被提供商拒绝,所以大门从不对其中一个采取行动. mode 实值 "signin" 或者说 "link" (见 连接身份(三); request 是启动请求的复制件,用于信头或饼干.

返回一个:

  • "allow" ——照常发送.
  • { redirect: "/waitlist" } ——不发送任何信息,用302个地址回答,每个呼叫者.
  • { error: AuthErrors.invalidCredentials({ reason: "Invite only" }) } – 不发送, 像其它启动一样失败 : 浏览器表单帖会回到提交页面 ?error=<code>,一个接线员得到的错误作为JSON.

发射失败的门 Per-IP和受虐者滥用限制在出道门前运行,因此被节制的请求仍然得到沉默"同意"的回答.

任何人都可以开始一个他们不拥有的地址的登录,所以一个写东西或告诉别人标识符的规则属于可选的 gate.onVerified 换句话说 它在提供者验证了标识符之后运行,在创建会话之前,使用相同的 provider, (中文(简体) ), identifier, (中文(简体) ), mode,以及 request (核查请求)加 user 财务报告和审定财务报表 identity 已解决的核查和 isNewUser它返回同样的决定: "allow" 创建会话, { redirect } 以302的回答,没有会话, { error } 无法像错误代码一样进行校验 。 它看到电子邮件和短信被验证;密码签入则跳过.

内置的电子邮件和短信提供商查询大门。 与 gate 设定, new Auth 如果为启动路由服务的任何提供者没有声明 consultsInitiateGate: true ——旧的供应商包否则会跳过您的政策而无踪迹. 自定义提供者选择通过调用 context.gate?.check({ provider, identifier, mode }) 一旦其标识符有效,在有结果时返回结果,并设置 consultsInitiateGate = true。 。 。 .

管理子路径

@activescott/auth/admin 管理仪表板是用来建造的,没有框架:

导出目的
createAdminData(auth){ listUsers, describeConfig } 在你的店铺上, 作为简单的系列行.
createAdminPredicate(admins)从已划定的字符串、数组或你自己的上游构建允许列表检查.
isAdminUser(auth, user)检查一个用户的回答 AUTH_ADMIN_IDENTIFIERS。 。 。 .

许可列表为电子邮件地址和E.164电话号码,以逗号或白空分隔,与 每个 用户身份。 因此,一个被允许的上市地址即使通过短信系统签字也承认其拥有者。 空的或缺失的许可名单是不允许任何人进入的.

import { isAdminUser } from "@activescott/auth/admin"

const session = await auth.verifySession(request)
if (!session || !(await isAdminUser(auth, session.user))) {
  return new Response("Not Found", { status: 404 })
}

isAdminUser 读取环境允许列表,并在每次呼叫中加载用户的身份。 当列表来自别处时,用 createAdminPredicate(admins) 并亲自将用户的身份交给用户(identityStore.findByUserId(user.id)) (中文(简体) ). 反应路由器适配器的仪表板使用这些,所以一个你自己打开的页面,仪表板同意谁是管理员.

日志

重新定向目的地从 ?redirectTo=字段和 Referer 头曰. 指定另一个来源或另一个方案的名称被拒绝, 并使用一个配置的路径取而代之, 否则无法为您的应用程序所识别: stale link 或代理重写 Referer 仅当用户在出乎意料的地方降落时才出现. 给图书馆找个地方说:

const auth = new Auth({
  // ...
  logger: console, // or { warn: (message, context) => log.warn(context, message) }
})

每个衰落的目的地记录一个WARN,命名它来自的参数和该值的出处. 数值本身从未被记录,因为一个神奇链接的URL在其查询中带有一键:

[auth] redirect destination declined, using fallback { source: 'redirectTo', fallback: '/', reason: 'other-origin', origin: 'https://other.example' }

logger 是可选的,当它不存在时,没有记录。 供应商作为 AuthContext.logger,框架适配器读取 Auth 实例auth.getLogger()),所以在这里配置它覆盖了整个流量. 虐待街区是分开的: console.warn附加时 abuse.onBlocked 如果你设置它.

等待名单

createWaitlist 是手批准新用户的应用程序的门。 每个人都得到一个代码。 一旦他们核实,一个没有核定账户的用户就会变成PENDING,并登入你的等待列表页面,没有会话;管理员通过它听到 notify;一个管理员批准或挡住仪表板。 经批准的用户照常签入.

状态在你的数据库里 隐藏在 ApprovalStore它的价值, "PENDING" | "APPROVED" | "BLOCKED",匹配 enum 应用程序通常已经具备:

import {
  createWaitlist,
  waitlistApprovalEmail,
  waitlistNotificationEmail,
} from "@activescott/auth"

const emailOptions = {
  appName: "Fernfiles",
  domain: "fernfiles.com",
  from: "[email protected]",
}

export const waitlist = createWaitlist({
  // Only needed so onApproved below can look up an address to email
  identityStore,
  approvalStore: {
    getApprovalStatus: async (userId) =>
      (await db.user.findUnique({ where: { id: userId } }))?.approvalStatus ??
      null,
    setApprovalStatus: async (userId, approvalStatus) => {
      await db.user.update({ where: { id: userId }, data: { approvalStatus } })
    },
  },
  waitlistUrl: "/waitlist",
  blockedUrl: "/login?error=blocked", // defaults to waitlistUrl
  // App rules that skip the waitlist. Never consulted for BLOCKED users.
  autoApprove: ({ identifier }) => autoApproved.has(identifier),
  notify: (notice) =>
    transporter.sendMail({
      ...waitlistNotificationEmail(notice, emailOptions),
      to: adminEmails,
    }),
  // Tell the user when an admin approves them
  onApproved: ({ identities }) => {
    const to = identities.find((i) => i.provider === "email")?.identifier
    if (to) {
      return transporter.sendMail({
        ...waitlistApprovalEmail(emailOptions),
        to,
      })
    }
  },
  logger: console,
})

const auth = new Auth({ /* ... */ gate: waitlist })

等待列表仅在核实后进行操作,因此开始为他人地址签名不会产生用户,发送管理员什么也没有. 校验步骤创建了用户,因为它没有等待列表;然后等待列表记录他们的状态. notify 当用户加入等待列表时, 点火一次( w)reason: "waitlisted"以及何时 autoApprove 让某人进入( l)reason: "auto-approved"); 被记录的投出,不会出错。 电子邮件是简单的和通用的,目的:app名称、域名、发送者和链接 /admin/users (单位:千美元)adminPath 改变它。 发送它是你的,所以核心 不依赖邮件.

onApproved 当管理者批准尚未批准的用户时,通过 approve 或者说 handleAdminAction,带有用户身份,可以选择地址(仅短消息用户没有电子邮件身份)。 它不会开火 autoApprove,其用户已签名. waitlistApprovalEmail 创建信件,链接到 /login (单位:千美元)signInPath 改变它。 投球被记录 批准站.

无记录状态的用户计数为未获批准. 在打开等待列表前标出已存在的用户 .

大门只看到电子邮件和短信的签注,而且只有在他们核实时才会看到. Passkey 签入 和用户在他们签署 需要每个要求检查后被屏蔽; waitlist.redirectFor(userId) 返回给已核准用户和 URL 发送给其他用户的无效。 使用 React 路由器适配器 :

createAuthHandlers(auth, {
  // ...
  onSessionVerified: async ({ user }, request) => {
    const to = await waitlist.redirectFor(user.id)
    if (to) return logout(request, to)
  },
})

对于仪表板, 提供批准和屏蔽按钮 AdminUsersPage是因为 rowActions 作为表格张贴 userId 财务报告和审定财务报表 intent (单位:千美元)"approve" 或者说 "block")到您的管理路线, 在这条路线的动作呼吁 waitlist.handleAdminAction(await request.formData()) 在检查了拨打者之后,他是一个管理员. waitlist.approve(userId) 财务报告和审定财务报表 waitlist.block(userId) 从你自己的代码做同样的。 表格本身没有CSRF标志;它依赖于会话饼干是 SameSite=Lax 或更严格,如示例配置。 这个 示例应用程序 有整个流量.

许可证

麻省理工学院

版本与发布记录

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

版本发布日期距今
5.9.0latest2026/10/1918 天前
5.8.02026/10/1126 天前
5.7.02026/09/301 个月前
5.6.12026/09/291 个月前
5.6.02026/09/201 个月前
5.5.02026/09/201 个月前

关于 @activescott/auth 的常见问题

@activescott/auth 是什么?

框架不可知性认证, 提供方模式用于魔法链接, OAuth, 以及更多

如何安装 @activescott/auth?

在项目目录运行 npm install @activescott/auth,或使用 pnpm add / yarn add / bun add。

@activescott/auth 支持 TypeScript 吗?

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

@activescott/auth 支持 ESM 还是 CommonJS?

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

@activescott/auth 可以商用吗?

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

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

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

@activescott/auth 还在维护吗?

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