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

@unieai/uad-unieai-web-gate

v0.1.21

Published

Browser sign-in gate: the /auth routes, the server-rendered sign-in page, and the WebServer request guard behind them

Readme

@unieai/uad-unieai-web-gate

English | 中文

浏览器登录闸门:/auth/* 路由、其后由服务端渲染的登录页,以及决定一个请求能否进入分发的 webServer 请求守卫。

身份来自 UnieAI Copilot 网页产品,走 RFC 8628 设备码授权。核准时该产品只返回一份凭证:用于其自身 /api/desktop/* 的 API key。

两个产品刻意分开。Copilot 是 SaaS;本桌面端是在本机运行自己 agent 的个人应用,只向该产品索取身份、方案与模型凭证,不接受它派来的任何工作。因此这趟登录不会产生任何能让该产品反向进入这台机器的东西。

为什么是守卫,而不是应用内的登录画面

client/runtime 在插件阶段就打开 WebSocket 下行并发出第一个 RPC,早于 React 挂载。此后渲染的任何东西都只是一块挂在已经开演的舞台前的幕布,而非浏览器调用方根本看不到那块幕布。守卫改在两个分发入口执行,因此一个决定即覆盖应用外壳、插件包注册表、/api 与两条下行。

认证不等于授权

本主机运行着一个持有 bash 与文件系统工具的 agent,因此放行所有有效账号,等于把任意代码执行权交给任何能在该产品注册的人。所以仅有身份并不构成放行:第一个完成登录的账号取得本实例的归属,之后的账号一律拒绝,除非 allowedUserIds 明确列出。刻意不提供「任何账号皆可」的模式。

允许名单属于部署,绝不属于随包出货的 bundle:在人人都会安装的包里写死账号,会让它只认那几个账号,于是其他每一份安装都没有人登得进去——而且重启和升级都没用,因为允许名单是配置,不是它所覆盖的那个内存中的认领。verify-cordis-config 因此拒绝 packages/bundle/*/cordis.patch.yml 里出现允许名单;共享实例在自己的 patch 层里声明它的账号。

认领会在最后一个会话登出时释放,而且仅在那时——只要还有另一个浏览器登录着,这台机器就仍然属于那个账号。认领的存在是为了让一台机器只服务一个账号,不是为了让它永远只服务那一个:登出是一个人在说他用完了这台机器,而活得比登出更久的认领,会把他自己的下一次登录当成「另一个账号」拒绝掉,并且除了重启 Host 之外没有任何办法清除。配置好的 allowedUserIds 不是认领,永远不会被释放——明确列出账号的部署是认真的,任何浏览器操作都不该扩大它。

启动路由

GET /auth/bootstrap 是桌面端的启动答复:刚加载完的应用关于自己账号所需要的一切,装在一个 body 里,在本 host 上取好。

它之所以存在,是因为浏览器过去要分别问四个问题——账号、providers、可用模型、可挂载的 MCP 服务器——各走各的路由、各按各的时机,而每一个到了这里照样会变成一次或多次产品调用。放在这一侧预取严格更优,理由是两件关于「东西在哪」的事实,而不是偏好:本 host 本就握着会话与 API key,而且它可以在浏览器存在之前就开始。设备码授权落地的那一刻预取就开始了,而那时浏览器还在从登录页跳往应用的路上。

答复是 { status, parts, pending }。其中每一部分都逐字等同于该部分自己的路由会给出的答复——parts.account 就是一份 /auth/account 的 body,parts.providers 就是一份 /auth/providers 的 body,依此类推;两者由同一批函数构建,因此不可能产生分歧。每一部分都到齐时 status 为 ready,只要有一部分没到就是 partial,并由 pending 点名。抵达了产品但失败的部分是一个存在的部分,携带该路由自己的失败 body:取到的失败与从未落地的部分是两件不同的事,只有后者才算 pending。

| 界限 | 数值 | 作用 | |---|---|---| | 作答截止 | 2 秒 | 冷启动预取最多等这么久。超过即以已到齐的部分作答,并继续取余下的。 | | 缓存寿命 | 30 秒 | 一次完成的预取被从内存作答多久。它覆盖的是一次跳转,而不是一个数据存储。 | | 上游上限 | 15 秒 | 何时放弃一次永不作答的产品读取,以免为该账号一直占着一个死掉的 socket。 |

有三件事它刻意不做。它绝不为没有有效会话的请求查阅缓存——会话先被解析,因此已失效的会话无论取好了什么都答以 signed-out。它绝不把为某个账号取好的东西交给另一个账号——缓存以账号 id 为键,并在该账号最后一个会话消失时丢弃。它也绝不服务于那些单独的路由:/auth/account 与 /auth/providers 是刷新路径,在保存或新建之后被读取,用缓存作答只会报出读者正想越过的那个状态。

没有会话的浏览器完全不花代价:没有产品调用,没有等待,{ status: 'signed-out' }。这件事比看上去重要——未登录是运行这台桌面端的正常方式,而本地 agent 并不需要产品。

账号路由

GET /auth/account 是唯一一条并非为登录而存在的路由。账户设置区块需要用户的方案与剩余用量,而这些调用所需的凭据正是本闸门会话表中持有的 API key。该 key 不得进入页面,因此由浏览器询问本 host、本 host 询问产品:该路由解析会话,以该 key 作为 bearer 调用 /api/desktop/me、/usage 与 /invite,并回答 { status: 'signed-out' }、{ status: 'signed-in', snapshot } 或 { status: 'failed', message }。三者都不含该 key。

失败时的 message 为英文。本 host 不知道读者的语言,因此它是给直接调用方看的诊断信息;消费本路由的浏览器半边 @unieai/uad-client-unieai-account-gateway 会改用自己的本地化文案。

快照中的 user.avatarUrl 来自下面的资料路由,而非 /api/desktop/me——后者不回报照片。没有头像的账号收到的答复中根本没有该字段:空 src 会渲染成一张坏图,而缺失才会画出首字母标记。

快照还携带 stats——概览区的五个数字,以及热力图背后的按日序列——在同一次调用中读自 /api/desktop/stats。它随快照同行、而不是只留给 /auth/stats,是因为浏览器的账号网关只读一个端点:没有出现在这个答复里的数字,就是概览区画不出来的数字。与其他附加分节一样,读取失败会让它「缺席」而不是归零,因为零是关于一个账号的断言,而产品没有作答不是。

invites 在一直传递的 inviteCount 之外,还携带推荐记录本身——inviteeEmail、status、createdAt、inviteUrl——来自同一次 /api/desktop/invite 调用,不增加任何成本。每一行都按字段名逐个构建,因此产品日后新增的列没有落脚处;inviteUrl 是刻意转发的,因为兑换链接是该操作的产物、是本人要转交给别人的东西,而不是本桌面端代其持有的凭证。

资料路由

GET/POST /auth/profile 是显示名称与头像的同一道缝,也正是它让桌面端的「账户」分区能够更改这两者,而不只是展示。GET 代理 /api/desktop/profile,回答 { status: 'signed-out' }、{ status: 'signed-in', profile } 或 { status: 'failed', message };POST 代理该路由的 PATCH,回答 { status: 'saved', profile } 或 { status: 'failed', message }。与 /auth/account 一样,会话的 API key 不出现在其中任何一个答复里,只朝产品方向花费。

此处有四条性质:

  • 保存回报的是存下的内容,而不是请求的内容。 产品接受的 POST 之后会有一次回读,因此去掉首尾空白的名称、重新编码的照片会以产品保留的样子到达页面。
  • 拒绝会说明自己是哪一种。 产品用一句写给直接调用方看的英文拒绝 PATCH,因此闸门辨认出是它三项检查中的哪一项失败,并回答 { status: 'failed', reason },取值为 name-required、avatar-format 或 avatar-payload——这就是 /auth/providers 的做法,只不过这条路由的拒绝是以散文而非代码抵达的。闸门自身对「未指定显示名称」的形状拒绝也用同一个 name-required,因此读者无需知道是哪一侧发现的。本版本无法归位的句子仍然是一次无解释的失败,而不是被猜出来的。
  • 头像的三种意图保持可分。 data: URL 设置照片,null 清除照片,缺失的 image 保持不动。把缺失并入 null 会在每次只改名称的保存中删掉头像。
  • 此处不做任何校验。 合法的显示名称与合法的头像由 app/api/desktop/profile 判定,在本 host 上再写一份规则只会与它相左。

本路由自己加的唯一约束是请求体的缓冲上限(12 MiB),因为头像以 base64 内联传输。产品不限制图片大小,因此这是传输限制而非校验规则——它比桌面端自身 512px 裁切的产物高出一个数量级。

Provider 路由

GET/POST /auth/providers 是账号 API Provider 的同一道缝,桌面版正是经由它显示与网页版「API Provider 设置」页相同的那份列表。GET 代理 /api/desktop/providers,回答 { status: 'signed-out' }、{ status: 'signed-in', providers } 或 { status: 'failed', message };POST 代理创建,回答 { status: 'created', provider } 或 { status: 'refused', reason }。

PATCH/DELETE /auth/providers/<id> 是单一行的同一道缝:注册在集合路径之下的前缀路由,代理 /api/desktop/providers/<id>,回答 { status: 'updated', provider }、{ status: 'deleted' }、{ status: 'refused', reason, fields } 或 { status: 'failed', message }。其他动词一律 405,更深的路径一律 404,因此 id 永远只有一段。

这两条路由上有两条规则,并针对整份序列化答复做了测试。与 /auth/account 一样,会话的 API key 绝不出现在其中。此外,任何 Provider 自身的凭据也绝不回传:产品的桌面投影对任何行都不携带凭据,ProviderSummary 也没有存放它的字段。凭据只朝一个方向移动——POST,或使用者刚重新输入 apiKey 的 PATCH,把 key 送往产品,那才是要花用它的存储。未指名 apiKey 的 PATCH 不会送出任何 key:缺席正是告诉产品保留既有凭据的方式,送出空字符串则会在改名时把它抹掉。

reason 原样转发产品自己的标识符(prefix_taken、byo_provider_limit_reached、managed_provider_readonly、not_found 等)而非文句,因为只有浏览器知道读者的语言,fields 则在旁携带被拒绝的字段名。平台托管的行能改动什么——只有各模型的启用选择与整体启用旗标,且永远不得删除——由产品在行的旁边决定;本主机不留第二份该规则,只转发执行它的 409。

Model 路由

GET /auth/models 是同一道缝,取的是账号在网页产品上有权运行的模型:也就是产品自身选单所依据的那份并集——账号自有 Provider 中已选取的模型、其群组所授予的模型,以及全局模型。它代理 /api/desktop/models,回答 { status: 'signed-out' }、{ status: 'signed-in', models } 或 { status: 'failed', message }。与 /auth/providers 一样,会话的 API key 绝不出现在其中,任何条目也不携带 Provider 凭据——EntitledModel 既没有存放凭据的字段,也没有存放端点的字段。

这些模型可以运行,但不是从这里运行。 任何条目都不携带 Provider 的 base URL 或凭据,将来也不会:那些留在产品侧。让这份列表不只是一个账号可见性界面的,是产品自己的中继 POST /api/desktop/v1/chat/completions——由桌面 API key 认证,并在产品侧解析上游、执行方案配额、计量该回合。@unieai/uad-llm-unieai-cloud 通过 ctx.unieaiGate 读取同一份列表,并把它注册成一条指向该中继的 llm 路由,因此一个权限模型恰好在存在闸门会话可为其认证时变得可选。

刻意只读:完全没有写入方向。一个账号可运行哪些模型,是由它的 Provider、它的群组与平台决定的,而其中没有任何一项是桌面版靠指名一个模型就能改变的。

邀请路由

POST /auth/invite 发出一封推荐邀请:请求体是 { email },答复为 { status: 'sent', url? }、{ status: 'refused', reason }、{ status: 'failed' } 或 { status: 'signed-out' }。reason 是产品自己的标识符(invalid_email、self_invite、already_invited),原样转发,因此一个本版本不认识的原因仍能抵达一个也许认识它的页面。

刻意没有 GET 半边。账号已经发出的邀请随 /auth/account 同行,而浏览器为了余额与计数本来就在读它;再来一条列表路由,就是同一批记录的第二个来源。

这里不决定什么是合法地址,也不决定哪个地址是账号自己的。那些是产品的规则;闸门只拒绝根本没有指定地址的请求体,并以产品自己的 invalid_email 回报,好让页面无论是哪一侧发现的都只有一行文案。

统计路由

GET /auth/stats 是同一道缝,取的是账号的个人活动,并提供 /auth/account 只带一份副本的那整条记录:totalTokens、peakDayTokens、longestTaskMinutes、currentStreakDays、longestStreakDays 与 daily——有用量的日期按升序排列,没有用量的日期是缺席而非零。它代理 /api/desktop/stats,回答 { status: 'signed-out' }、{ status: 'signed-in', stats } 或 { status: 'failed', message }。会话的 API key 不出现在其中任何一个里。

与产品路由一致,只有个人范围:网页产品自己的 /api/user/stats 还带一块组织面板,而本桌面端没有组织界面。

MCP 路由

GET /auth/mcp 列出账号可挂载的 MCP 服务器。它是所有 /auth/* 路由中唯一一条其答复是对产品所发内容的刻意「收窄」的路由,因为产品在这里发下了凭证:每台服务器都带一枚按用户签发的 bearer,每次读取都重新铸造,大约只管一小时。

浏览器拿到的是 McpServerView——id、label、origin、expiresAt、tools——一个根本没有 token 成员的类型,遵循 lib/desktop/providers.ts 剔除 apiKey 的同一条原则:将来想要发送该 bearer 的修改,必须先加上这个字段,而那是评审看得见的改动。端点被收窄为其 origin,理由与产品自己把 MCP 条目公布为 origin 的理由相同——远端 MCP URL 的路径或查询串里经常带着令牌。

expiresAt 会随答复同行,因为它是关于服务器的事实而非凭证:一个在授权失效后仍把服务器显示为已连接的插件页,展示的是一件已不再为真的事。

技能路由

GET /auth/skills 列出账号在网页产品里保存的技能,GET /auth/skills/<slug> 则把其中一个作为一整份 SKILL.md 回答。列表是给页面的,文件是给 host 的,这个切分就是设计本身。技能是模型会照着做的指令,所以落进技能目录里的字节必须来自账号,而不是来自浏览器:页面把 slug 交给 host,host 透过 ctx.unieaiGate.accountSkill 读出文件再写下去。

slug 是一个目录名,不只是一个标识符。 它会被接到这台机器上的技能目录后面,所以一个带分隔符、带前导点、或者是那两个会指向别处的名字(.、..)的值,在这里和在产品那边都会被拒绝,而列表里带着这种值的行会被丢掉而不是显示出来。两个地方检查同一条规则,是因为这一侧才是路径最终被拼出来的地方。

复制就是复制:文件成了那台机器的,在那边改它对账号没有任何影响,再复制一次就覆盖回去。两边都不记录哪台机器有哪个技能。

文件路由把 404 与「读取失败」分开,因为这两者引出的句子不同:账号已经没有这个技能,意思是刚才读到的那份列表已经过期了;而读取失败是值得重试的。空文件被当成失败拒绝——一个没有正文的技能文件教不了模型任何事,却会把一个原本有内容的覆盖掉。

Host 侧的闸门服务

ctx.unieaiGate 是闸门的另一半:不是路由,而是让 host 插件代已登录账号行事的那道缝。它公开 productUrl、session()——账号及其 API key——以及三个代理读取 mcpServers()、entitledModels() 与 accountSkill(),返回的是浏览器投影收窄之前、产品原样发来的内容。@unieai/uad-unieai-mcp-supervisor 借第一个挂载账号的 MCP 服务器;@unieai/uad-llm-unieai-cloud 借第二个构建账号可运行的模型路由;@unieai/uad-host-apiproxy 里的 skill.install 借第三个把技能文件写下来——这也正是那条路由只收一个 slug、不收任何内容的原因。

该服务之所以存在,是因为会话表是本 host 上唯一持有产品凭证的地方,而需要凭证的 host 插件不得伸手进那张表。unieai-gate/session 在一次登录产生会话时、以及最后一个会话因登出而消失时发出——闲置过期不会发出,因为那是在读取时判定的,因而没有任何东西在它发生的那一刻观察到它。代账号持有资源的消费方按自己的节奏重读。

配置

| 字段 | 默认值 | 含义 | |---|---|---| | productUrl | https://agent.unieai.com | 本桌面端登录的网页产品。 | | enforce | false | 守卫是否真的拒绝流量。默认关闭,以便组合先挂载闸门并在 /auth/login 走通流程再决定启用:默认打开一道尚未验证的栅栏,会把操作者锁在一台正在使用的机器之外。 | | allowedUserIds | [] | 允许进入的账号。为空则交由 claimFirstLogin 决定。 | | claimFirstLogin | true | 空名单是否由第一次成功登录者取得归属。 | | idleTimeoutMs | 12 小时 | 浏览器会话的闲置寿命。 |

Model Experience

无,本包只对浏览器流量把关并提供一份登录文档;此处没有任何内容进入模型请求。

KV Cache effect

无;本包既不组装也不发送供应商请求。

Known Limitations and Deferred Work

  • 会话保存在内存中 —— 主机重启会让所有浏览器登出。服务单一操作者的桌面端重新认证只需一个动作,因此持久化存储推迟到确有部署需要会话跨重启存活时再做。
  • enforce 默认为关 —— 闸门随组合挂载但不拦截,需要关闭机器的部署自行打开该开关。这是刻意的分阶段选择,而非建议:一个可达且 enforce: false 的实例,只受其前方任何东西的保护。
  • 登录页内联重述了自己的设计取值 —— 它服务于可能无法访问插件注册表的访客,因此不能引用 ui-theme。它照 shadcn/ui 的 LoginForm 区块布局,其 Tailwind 类在此解成字面值,因为本仓库既不带 Tailwind 也不带组件库;那份配色与尺寸在这里重复,必须随主题一同更新。区块中的邮箱输入、各家登录按钮与条款行是刻意不做的 —— 桌面端没有自己的用户库,选哪一家登录发生在网页产品自己的页面上,而本部署没有可供链接的条款页。
  • /auth/account 每次调用都读取产品 —— 没有缓存,也没有重验证窗口,因此反复重新加载的浏览器每次都会发出同样的四次产品调用。要加缓存就需要一条失效规则,而本部署没有可依据的来源。/auth/bootstrap 是唯一的例外,而它被限定在一次启动前后的那几秒里,并不是那条缺失的规则。
  • 无论有没有人来读,启动预取都会跑 —— 一次登录会立刻把缓存暖上,因此一次之后并没有应用加载的设备码授权,仍要花掉一轮产品调用的扇出。它是每次登录一次而非每次请求一次,也正是它让随后那次读取通常免费。
  • 启动答复中的某一部分最多可能有一个缓存寿命那么旧 —— 预取后 30 秒内的重新加载由内存作答,因此这几秒里在另一窗口消耗掉的额度不会反映出来。各界面自己的刷新读取不受影响;只有启动答复如此。
  • 启动答复没有推送的另一半 —— 没有任何东西会告诉浏览器它还在等的那一部分已经到了。客户端按自己的节奏再问一次,此后那一部分就归该界面自己的重试手势管。
  • 一次资料保存要花两次产品调用 —— PATCH 加上回报存下内容的那次回读。产品的 PATCH 只回答它保留的名称与图片,不含邮箱地址,因此仅凭它作答会给页面一份残缺的资料。
  • 产品的拒绝原因被丢弃 —— app/api/desktop/profile 以英文说明拒绝(Name is required、Unsupported avatar format),那是给直接调用方看的诊断信息,因此本路由回答自己的英文诊断,由浏览器半边本地化。若要像 /auth/providers 那样转发结构化原因码,需要产品先发布一套。
  • 12 MiB 的请求体上限只属于本 host —— 产品不限制头像大小,因此介于该上限与产品实际可接受范围之间的图片,只会在此处被拒。
  • /auth/models 本身不喂养任何东西 —— 该浏览器路由是可见性界面;让这些权限变得可运行的模型路由改为通过 ctx.unieaiGate 读取同一份列表,因为 host 插件无法出示浏览器 cookie。两者因此各自读取产品,并可能在其中一方的刷新窗口内彼此不一致。
  • /auth/models 每次调用都读取产品 —— 与 /auth/account 一样,没有缓存,也没有重验证窗口。
  • 无法归位的资料拒绝会丢失原因 —— 闸门靠匹配产品的三句拒绝措辞来说明是哪一种拒绝,因此产品新增第四项检查、或改写了措辞,都会让答复以一次无解释的失败抵达页面,直到这份清单被更新。产品没有为这些拒绝发布代码,这正是这里在匹配散文的原因。
  • /auth/invite 回报的是这次发送,而不是列表 —— 它新增到的那批记录要从 /auth/account 重读,因此想展示新邀请的页面必须重新读取快照。
  • /auth/stats 与 /auth/mcp 每次调用都读取产品 —— 与 /auth/account 一样,没有缓存,也没有重验证窗口。两者中更昂贵的是 /auth/mcp:每次读取都会在产品侧铸出新的 bearer,因此一个轮询它的页面正在铸造自己从不使用的凭证。
  • /auth/mcp 报告的是授权而非挂载 —— 它回答的是产品会允许该账号挂载什么,而不一定是本 host 实际打开了什么。被 MCP 监管者跳过或连接失败的服务器,在这里与一台正在提供工具的服务器看起来完全一样。
  • 闲置过期是惰性的 —— 会话的闲置寿命在读取会话时才判定,因此 ctx.unieaiGate 既不会察觉也不会宣告某个会话失效的那一刻,unieai-gate/session 也不会为此发出。
  • 未刷新或撤销 API key —— 它在会话存续期间持有、登出时丢弃;在到期前续期的工作推迟给消费它的账号界面。