@qilin-agent/accounts-local
v3.1.11
Published
Local browser accounts for the Web surface: one account file under the harness home, scrypt password hashes, signed HttpOnly session cookies, the /api/auth endpoints, and the transport's account-session gate
Readme
description: "Web 表层的本地浏览器账户:harness home 下的账户文件、scrypt 密码哈希、签名的 HttpOnly 会话 cookie、/api/auth 端点,以及覆盖应用文档与其他所有 /api 请求的账户会话门禁。" kind: "package-reference"
@qilin-agent/accounts-local
English | 中文
概述
在 Web 部署中挂载本包,浏览器必须先登录才能到达 harness。首位访问者初始化管理员账户;此后每个浏览器都用用户名或该账户携带的邮箱地址登录。账户会话门禁只把应用文档与 /api 交给本部署签发的会话,没有会话的浏览器会被送到公开 landing 页。账户文件是本部署的名册,admittedAccounts 把一份部署收窄到它列名的账户,这正是共享该文件的多个运行时之间的分隔。
目录
使用本包
把本插件与 qilin-client-connection 及一个 dist 服务器组合在一起,Web 表层就会在交出应用之前要求账户。它注入 connection 服务。
最小配置
- name: '@qilin-agent/accounts-local'
config:
registration: closed| 字段 | 默认值 | 含义 |
|---|---|---|
| enabled | true | 要求受门禁的 index 路径与每个非公开 /api 请求都携带账户会话 |
| registration | 'open' | 匿名访问者是否可以创建额外账户 |
| sessionMaxAgeDays | 7 | 浏览器会话的绝对有效期,单位为天 |
| sessionCookieSecure | false | 给会话 cookie 加 Secure;在公网地址上终止 TLS 的部署需要打开它 |
| loginMaxAttempts | 10 | 同一标识符在窗口内累计多少次失败后,后续尝试必须等待 |
| loginAttemptWindowSeconds | 900 | 这些失败计入的窗口长度,也是被拒调用方被告知要等待的时长 |
| qilinHome | $QILIN_HOME,其次 ~/.qilin | 存放 auth/accounts.json 的 harness home |
| admittedAccounts | 文件中的每个账户 | 本部署接纳的账户标识;未列名的账户既不能登录,其已签发的会话也不再有效 |
生成的配置目录是每个受支持字段及其 JSDoc 的穷尽式真源。
登录流程
请求受门禁 index 路径的匿名浏览器会被重定向到公开 landing 页并带上 ?next=<请求路径>,绝不会直接落到凭据表单。landing 页读取 GET /api/auth/status,把入口按钮指向本部署能提供的文档:账户文件里还没有账户时是首次运行页,否则是登录页,两者都会带上请求的路径。该文档渲染表单并提交凭据;成功响应本身已经携带会话 cookie,因此浏览器随后导航到经过校验的 next 目标或应用入口路径。因为门禁位于提供该文档的操作里,直接导航无法绕过任何客户端检查;退出登录会把浏览器送回 landing 页。同一 authority 与标识符的反复失败会让后续尝试等待:从第一次失败开始计数,窗口过去或登录成功都会清零,而不指向任何账户的标识符照同样方式计数,因此这道限制不是「名字探测」。
端点
每个端点都以 cache-control: no-store 返回 JSON,每个拒绝都是 { "error": { "code", "message" } }。
| 端点 | 应答 |
|---|---|
| GET /api/auth/status | 200 { enabled, needsSetup, registrationOpen, authenticated, user };user 是 { id, username, email, createdAt } 或 null,未填邮箱的账户其 email 为 null |
| POST /api/auth/setup | 200 { user } 加会话 cookie;已存在账户时 409 already-initialized |
| POST /api/auth/register | 200 { user } 加会话 cookie;403 registration-closed;409 username-taken 或 email-taken |
| POST /api/auth/login | 200 { user } 加会话 cookie;标识符未知、密码错误或账户未被本部署接纳时 401 invalid-credentials;同一标识符在窗口内的失败达到 loginMaxAttempts 后为 429 too-many-attempts 并带 Retry-After |
| POST /api/auth/logout | 204,并清除该 authority 的 cookie |
| POST /api/auth/change-password | 200 { user } 加新的会话 cookie;没有会话时 401 unauthorized,当前密码错误时 401 invalid-credentials;409 username-taken 或 email-taken |
setup 与 register 读取 { username, email?, password };login 读取 { identifier, password },用户名与该账户的邮箱地址都可作为标识符;change-password 读取 { currentPassword, newPassword, username?, email? },并更换其中给出的每个身份字段,地址为空字符串即清除已存地址。用户名为 3–32 位字母、数字、点、下划线或短横线且以字母或数字开头,存储与查找前会去除空白并转为小写,因此同一账户不会被拼成两种写法;地址同样处理,密码至少 8 个字符。不是 JSON 对象的请求体、缺失的凭据字段、形式不合法的用户名或地址,以及没有指明 authority 的请求,会分别以 400 invalid-body、invalid-username、invalid-email、password-too-short 或 invalid-authority 拒绝。
账户会话门禁
enabled 为 true 时,插件在 ctx.connection.session 安置唯一的 ConnectionSessionAuthority,两个执行点都由 Connection 拥有。index 请求经 authority 的 authorizeIndex:会话有效时提供文档,其他任何请求都得到上面的 302。在共享 /api 路由上,isPublicApiRequest 放行 /api/auth/ 前缀,使浏览器能够登录;verify 要求其他所有请求都携带账户会话——缺少会话的请求在通过 connection 包的 Host 与 Origin 校验后得到 401。enabled: false 会保留全部六个端点,在状态读取中报告 enabled: false,并让启动令牌 cookie 继续掌管 index 路径与 /api 路由。
接纳名单
admittedAccounts 列出本部署服务的账户。装载时会拒绝这样的名单:没有列出任何账户、列出了账户文件里没有的标识、或没有把 registration 关掉——未被列名的账户根本无法使用这份部署,所以开放注册只会把账户交给一个进不来的访问者。拒绝覆盖两个方向:POST /api/auth/login 对不属于本部署的账户给出与密码错误完全相同的应答;已经签发的会话也不再为未被接纳的账户命名账户,尽管其签名仍然有效。因此一份共享的账户文件可以服务多个运行时:qilin tenants serve 为每个账户组合一个运行时,各自只接纳自己。
理解实现
本节说明插件拥有什么,并指出实现它们的文件;可观察行为已在使用本包中说明。
设计理念
本包是一个函数插件。apply 打开解析后 harness home 的账户文件,在该账户文件旁加载或创建该部署的会话签名密钥(auth/session-secret.json,以 0600 建在 0700 目录内),构建 cookie 所有者,在 ctx.connection.fetch 上注册六个路由,并在门禁启用时把会话 authority 安置到 ctx.connection.session——该席位只接受一个所有者,第二次安置会让安装方插件加载失败。它不提供任何 Kylin 服务:没有别的东西读取账户,组合通过端点与门禁到达这个表层。
账户文件
$QILIN_HOME/auth/accounts.json 是一份带版本的文档,为每个账户保存一条记录:不透明的 id、规范化地址、编码后的 scrypt 哈希、创建时间与凭据代数。每次变更都经 @qilin-agent/atomic-write 以 0600 模式写入完整后继内容,然后才在内存中发布,因此写入失败时运行中的服务器仍停留在上一份账户集合。文件缺失即空账户集合——首次注册初始化的就是这个状态;而本构建未曾写入的文件会让加载失败,而不是被迁移。存储层可创建账户、替换账户、用 setPassword 为某个账户重新设置密码,以及移除某个账户;qilin tenants add、passwd、remove 是针对该集合的运维命令。移除某个账户时,若其租户覆盖层仍列名该账户,该运行时会在加载时明确失败,而不是继续服务一个文件里已不存在的账户。
密码与会话
存储的哈希是自描述字符串(scrypt$N$r$p$salt$key),带逐账户 salt、代价 2^15、块大小 8、并行度 1 与 32 字节密钥;校验按存储的参数重新派生并以恒定时间比较。会话 cookie 是 HMAC-SHA256 签名,按其签发时的请求 authority 命名,并携带该 authority、账户 id、凭据代数,以及不超过 sessionMaxAgeDays 的绝对签发与过期区间。它是 host-only、Path=/、HttpOnly、SameSite=Strict,并在设置 sessionCookieSecure 时带 Secure——在公网地址上终止 TLS 的部署需要它,因为该地址背后的运行时说的是 HTTP。密码或邮箱变更会递增账户的凭据代数,因此此前签发的所有 cookie 即使签名仍能验证,也不再指向任何账户。
源码地图
| 文件 | 职责 |
|---|---|
| src/index.ts | Config、账户文件与会话密钥加载、路由注册、门禁安置 |
| src/routes.ts | 六个 /api/auth 端点、输入校验与错误信封 |
| src/gate.ts | authority 的判定:index 重定向、公开 API 前缀、会话校验 |
| src/accounts.ts | 持久账户文件:解析、原子替换、凭据代数更新 |
| src/password.ts | scrypt 哈希与恒定时间校验 |
| src/session.ts | 签名的会话 cookie:签发、清除、authority 绑定、有效期 |
| src/paths.ts | AUTH_API_PREFIX、LOGIN_PATH 与 SETUP_PATH |
| src/validation.ts | 邮箱规范化与密码最小长度 |
| tests/auth-surface.spec.ts | 真实组合:公开文档、受门禁入口、端点、/api 门禁、启动令牌交接 |
| tests/accounts.spec.ts | 账户文件解析、变更,以及写入失败后留下的状态 |
| tests/password.spec.ts | 哈希编码、校验,以及对外来存储值的拒绝 |
| tests/session.spec.ts | cookie 属性、签名、authority 绑定与有效期 |
进一步探索
当包级约定不够用时阅读以下内容:先看拥有门禁的 transport,再看挂载本行的组合。
- qilin-client-connection——会话席位、入口路径与
/api请求策略。 - frontend-static——提供应用文档与公开文档的 dist 服务器。
- qilin-web-app——挂载本行并声明其文档表的随附组合。
- qilin-home-paths——
$QILIN_HOME与~/.qilin的解析。 - 本地账户决策——为什么门禁在服务端,以及为什么注册默认开放。
- 生成配置目录——每个受支持配置字段及其源声明。
模型体验
无,因为账户只为浏览器表层设置门禁,本包不注册任何面向模型的内容。
KV Cache 影响
无;该包既不组装也不发送提供方请求。
已知限制与延期工作
这些限制说明一个账户保护什么、不保护什么。它们是当前包约束,不是任务积压。
- 账户集合属于单个 harness home——每个账户都能到达与首个账户相同的 Session、凭据与文件,因此第二个账户只是同一套 harness 的另一把钥匙,而不是独立租户;不同的人需要各自的
QILIN_HOME与端口。admittedAccounts把一份部署收窄到该文件的一个子集,它分隔的是运行时而不是账户。 - 接纳分隔的是运行时,不是操作系统用户——所有运行时都以同一个操作系统用户运行,因此某个租户内的 shell 或文件工具能读到另一个租户的目录与共享凭据。
- 注册默认开放——在账户集合仍有空位时,任何能到达该端口的人都可以创建拥有完整 harness 访问权限的账户;绑定到 loopback 之外的部署要设置
registration: closed。随附的qilin web命令绑定 loopback 并拒绝--host 0.0.0.0。 - 登录限流按 authority 与标识符,而不是按部署——同一键在
loginAttemptWindowSeconds内失败达到loginMaxAttempts次后,它要等到窗口结束。轮换标识符的攻击者遇不到共享限制,因为本部署不保留按 authority 汇总的计数。 - 没有账户删除或密码找回——没有任何端点会删除账户或找回遗忘的密码;操作者需要编辑或删除
$QILIN_HOME/auth/accounts.json,删除该文件会让部署回到首次运行状态。 - 会话 cookie 是 bearer 凭据——它是
HttpOnly; SameSite=Strict,仅在设置sessionCookieSecure时带Secure;在公网地址上终止 TLS 的部署需要它,而浏览器以明文 HTTP 到达的部署也只能以明文回传。 - 会话密钥丢失或被替换会终结所有会话——名册旁的
auth/session-secret.json是唯一的签名密钥,且该部署的每个运行时都读这同一份文档,因此一个运行时签发的会话会被下一个接受;替换它会作废所有已签发的 cookie,而账户本身保留。 - 账户文件只能向前迁移——版本 1 的文档会被读成「用户名即其所存地址」的账户,但来自更新构建的文档会让插件加载失败,且在下次变更之前不会有任何东西改写版本 1 的文件。看起来像地址的迁移后用户名,必须由运营者手工修改才能换成合法用户名。
开发备注
无。
