Skip to content

feat(auth): 身份导入默认不设密码(passwordPolicy: 'none') - #2820

Merged
os-zhuang merged 1 commit into
mainfrom
claude/sys-user-bulk-import-passwords-iv68s9
Jul 11, 2026
Merged

feat(auth): 身份导入默认不设密码(passwordPolicy: 'none')#2820
os-zhuang merged 1 commit into
mainfrom
claude/sys-user-bulk-import-passwords-iv68s9

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

设计讨论结论(#2766 后续):导入的职责是身份,不是凭据。验证码/魔法链接登录 + Console 已有的无密码检测(hasLocalPassword()/set-password 引导,SSO 入职同款流程)意味着导入根本不需要生成密码。

变更

  • 新增 passwordPolicy: 'none' 并设为默认:建号时不传 password(better-auth 可选密码 → 无 credential 记录),零密码材料生成/返回/分发。用户经手机 OTP / 魔法链接 / 重置链接首登,Console 检测到无密码后引导 set-initial-password
  • invite 不再生成丢弃式密码:改为同样无凭据建号 + 发送邀请。依据:better-auth 1.6.23 的 reset-password 对无凭据账号会自动创建 credential 记录api/routes/password.mjs 已核实),语义完全等价且更干净。
  • temporary 不变:保留为"无邮件/短信基建部署"的显式兜底。
  • 行为变化说明:passwordPolicy 原为必填(缺失 400),现在缺失 = none
  • 文档(authentication.mdx)与 changeset 同步更新。

测试

  • plugin-auth 全套 372 测试通过(新增 4 个 none 策略用例:createUser 请求体断言无 password 键、无强制改密打戳、无邀请发送、不依赖邮件/短信服务;原"缺失策略 400"用例改为"未知策略 400 + 缺省即 none")。
  • tsc --noEmit 干净;check-role-word 通过。

三种策略的最终定位

策略 定位 密码
none(默认) 有 OTP/魔法链接渠道的常规入职 无——用户首登后自设(Console 引导已存在)
invite 需要主动推送入职通知 无——邀请链接/短信 OTP 首登自设
temporary 无邮件/短信基建的兜底 服务端生成,一次性返回 + 强制改密

Ref #2766, #2782(向导适配器将把 none 作为默认选项)。

🤖 Generated with Claude Code

https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o


Generated by Claude Code

…edentials at import time

Import provisions identity, not credentials. The new default 'none'
policy creates credential-less accounts (better-auth optional-password
create): no password material is generated, returned, or distributed.
Users first sign in through a channel (phone OTP / magic link / reset
link) and the Console's existing hasLocalPassword() → set-initial-password
flow nudges them to set one afterwards.

'invite' also stops minting a throwaway password — better-auth's
reset-password creates the credential record on first set (verified in
1.6.23 dist), so it is now 'none' + the invitation send. 'temporary' is
unchanged as the no-infrastructure fallback.

passwordPolicy was previously required (400 when omitted); omitted now
means 'none'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 11, 2026 2:55am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 11, 2026 03:01
@os-zhuang
os-zhuang merged commit 5bced2f into main Jul 11, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/sys-user-bulk-import-passwords-iv68s9 branch July 11, 2026 03:01
os-zhuang pushed a commit that referenced this pull request Jul 11, 2026
…Console wizard entry

The detailed Bulk Import section already documents the none/invite/temporary
policies, but the API-reference one-liner still said "invite or temporary"
(the pre-#2820 default) and nothing pointed at the Console import wizard
shipped in objectui (framework#2782). Adds none as the default to the
endpoint reference and a short paragraph on the Users-list Import flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
os-zhuang added a commit that referenced this pull request Jul 11, 2026
The detailed Bulk Import section already documents the none/invite/temporary
policies, but the API-reference one-liner still said "invite or temporary"
(the pre-#2820 default) and nothing pointed at the Console import wizard
shipped in objectui (framework#2782). Adds none as the default to the
endpoint reference and a short paragraph on the Users-list Import flow.


Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang pushed a commit that referenced this pull request Jul 27, 2026
… and correct download filename (#3504)

The approval inbox timeline showed a nameless "附件" chip that did nothing on
click, and even the downloaded file was named after the opaque URL token. Two
root causes, both in the framework:

approvals — `sys_approval_action.attachments` (a `Field.file`) stores rich
descriptors `{ id, name, url, mimeType, size }`, but `rowFromAction` mapped them
with `.map(String)`, collapsing each to "[object Object]". `ApprovalActionRow.
attachments` is now `ApprovalActionAttachment[]`; the descriptor carries name +
url, so consumers label/open attachments without reading the system `sys_file`.

storage — presigned downloads served `application/octet-stream` with no
`Content-Disposition`. `getSignedUrl`/`getPresignedDownload` now take
`PresignedDownloadOptions { filename, contentType, disposition }`; the REST
download routes pass the `sys_file` name+mime; the local adapter carries them in
the token and `_local/raw` emits an RFC 5987 Content-Disposition; S3 bakes the
same into the signed URL. Default `inline` preserves in-browser preview.

Verified end-to-end in app-showcase. Refs objectui #2820.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang pushed a commit that referenced this pull request Jul 27, 2026
…wnload filename (#3504) (#3505)

* fix(approvals+storage): surface decision attachments with name, open, and correct download filename (#3504)

The approval inbox timeline showed a nameless "附件" chip that did nothing on
click, and even the downloaded file was named after the opaque URL token. Two
root causes, both in the framework:

approvals — `sys_approval_action.attachments` (a `Field.file`) stores rich
descriptors `{ id, name, url, mimeType, size }`, but `rowFromAction` mapped them
with `.map(String)`, collapsing each to "[object Object]". `ApprovalActionRow.
attachments` is now `ApprovalActionAttachment[]`; the descriptor carries name +
url, so consumers label/open attachments without reading the system `sys_file`.

storage — presigned downloads served `application/octet-stream` with no
`Content-Disposition`. `getSignedUrl`/`getPresignedDownload` now take
`PresignedDownloadOptions { filename, contentType, disposition }`; the REST
download routes pass the `sys_file` name+mime; the local adapter carries them in
the token and `_local/raw` emits an RFC 5987 Content-Disposition; S3 bakes the
same into the signed URL. Default `inline` preserves in-browser preview.

Verified end-to-end in app-showcase. Refs objectui #2820.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(spec): regenerate API surface snapshot for the two added interfaces

Additive only (0 breaking): ApprovalActionAttachment + PresignedDownloadOptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(approvals): correct the attachment read-shape rationale and share the id rule

Follow-up on this PR's own change, after ADR-0104 D3 wave 2 landed on main.

The fix in this PR is right; its stated cause was inverted. It says the
`sys_approval_action.attachments` column "stores rich descriptors — a fileId is
resolved to a full descriptor on write". The code says otherwise:
approval-service.ts writes `input.attachments` verbatim (a fileId string[]),
and there is no write-side descriptor resolution anywhere in the repo. What
actually happens is that the column stores an opaque sys_file id — the stored
form of every media field — and the ObjectQL read path expands it into
`{ id, name, size, mimeType, url }` on the way out. That resolver was already
in this PR's base commit, so the descriptors observed in listActions came from
the read path, not from storage.

Left as written, that inverted account would have been a false statement about
storage sitting in the protocol contract, in the changeset, and in a regression
test comment — and PR-5a has since made the stored form explicitly an id, so it
would also have been contradicted by the schema. Corrected in all three places.

Also names the three read forms the normalizer actually handles, rather than
one: the expanded value (normal), a bare id (nothing to expand it into), and a
legacy inline blob written before the cutover, whose keys are snake_case
(`file_id`, `mime_type`). The last of those had no test; it has one now. The
id-token test reuses `isFileIdToken` from @objectstack/spec/data — the
platform's single arbiter of "opaque id or URL?" — so this normalizer and the
engine's read resolver cannot drift apart on that question, and a non-id string
is now surfaced as a url rather than mislabelled an id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd

* docs(storage): record the presigned-download options in the hand-written contract docs

The drift check flagged these: both storage-service pages transcribe
IStorageService by hand, and this PR adds an optional PresignedDownloadOptions
argument to getSignedUrl / getPresignedDownload. Left alone they would describe
a signature the code no longer has — the same declared-vs-actual gap the rest of
this PR is about, one level up in the docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants