Skip to content

feat: first-run launch-token onboarding for unconfigured instances - #117

Merged
TecFancy merged 133 commits into
mainfrom
development
Oct 26, 2026
Merged

TecFancy merged 133 commits into
mainfrom
development

Conversation

@TecFancy

@TecFancy TecFancy commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

改了什么和为什么

把 development 上的首次引导通道提升到 main。全新安装、未配置凭据的实例,dsh 启动时打开的 /?token=<launchToken> 不再停在 Access token 输入框,而是进入 /auth/setup 向导:token 模式生成并保存访问令牌,password 模式创建第一个管理员。配置一旦成立窗口立即关闭,同一条 URL 之后只会回到登录页。

实现细节与取舍见 #116 与 docs/decisions/implemented/2026-10-26-first-run-launch-bootstrap.zh.md。冻结的 guard.ts / wrapServer / Gate 词表未改动。

实际跑过的命令与结果

  • npm run verify 退出码 0;138 个测试文件、1174 个用例通过;覆盖率 94.06 / 88.55 / 93.82 / 96.6。
  • 隔离实例(全新 DSH_HOME + profile,dsh 0.2.1-rc.3)端到端通过;打包产物解包后的 lib/ 与仓库逐字节一致。
  • 真实 Chromium:launch URL 落在 First run 向导,cookie 属性正确,console 0 条消息;向导完成后新 session 访问旧 URL 落到正常登录页。
  • PR feat: first-run launch-token onboarding for unconfigured instances #116 的 CI 全过(ubuntu 与 windows 的 hygiene / quality / build 六项)。

风险与回滚

  • launch token 会出现在 /auth/bootstrap?token= 的 Location 里,与原始请求 URL 同级暴露;这类 302 带 referrer-policy: no-referrer。
  • 引导通道假设单进程;多副本无粘滞路由时回落登录页;旧版 dsh 行为与 0.17.4 一致。
  • 回滚:revert 本次提升的 squash 提交即可,没有新增配置项。

关联 issue

Closes #115。

A blank session (New Session, no input/response yet) has current set in the session snapshot AND a hidden session header, so both sign-out entries were suppressed there. The hero shell.overlay entry now shows when the current session is undefined OR still blank (byId[id].blank), hiding only for a real non-blank session (header button takes over).
Replace the stale sidebar-foot screenshots with two real captures from production: the blank new-session page (floating top-right sign-out, dsh workspace collapsed) and a real English conversation (sign-out right of the Session log). Update README.md and README.zh.md to show the new top-right placement; remove the obsolete logout-sidebar-*.png files.
dsh plugin add installs the package into the profile's node_modules; the dsh-auth bin is not added to PATH, so README and deployment docs now call it through the profile (pnpm --dir ... exec dsh-auth) and gain a Troubleshooting section. Fixes #44.
…sh-Proxy deny-list (#47)

* feat(proxy): authenticated local proxy bin

* feat(guard): X-Dsh-Proxy deny-list for host-native methods

* docs: local proxy and deny-list deployment sections (zh+en) with systemd example

* test(proxy): cover CLI parsing and upgrade header/token branches

* docs: README local-proxy section and English design doc
dsh only auto-discovers project skills from .agents/skills/ (and
.dsh/skills/); the skills/ directory was never picked up despite the
AGENTS.md claim. Move both skills there and fix the description.
- scripts/run-gates.mjs (shared from the framework): named scenarios
  derived from the verify chain; auto-detects the change surface and
  runs the minimal evidence set, fail-closed to the full set
- pre-push runs npm run gates -- --base @{u} instead of only type-check
- CI: split the monolithic check job into hygiene/quality/build
  parallel jobs (no path filters - full coverage always)
- docs/development.md: document gates and the new pre-push
# Conflicts:
#	README.zh.md
#	lib/client.js
#	lib/client.js.map
#	lib/client/index.d.ts
#	lib/client/index.d.ts.map
#	lib/proxy-headers.d.ts
#	lib/proxy-headers.d.ts.map
#	lib/proxy-headers.js
#	lib/proxy-headers.js.map
#	lib/proxy.d.ts.map
#	lib/proxy.js
#	lib/proxy.js.map
#	src/client/index.tsx
#	src/client/logout-action.test.tsx
#	src/proxy-headers.test.ts
#	src/proxy-headers.ts
#	src/proxy.ts
refactor: layered src with barrels + framework verify gates + decision records
The wrapServer disposer used to clear the route tables and write back a
snapshot taken at wrap time. Reloading dsh-auth-gate alone (a config
change in the Plugins page) therefore dropped routes other plugins had
registered after the wrap, resurrected routes whose owner had been
disposed, and made that owner's next load throw "duplicate exact route".

Unwrap now swaps an entry back to its original handler only while it
still holds a guard this wrap produced (tracked in per-wrap WeakMaps).
Deleted entries stay deleted, foreign replacements are left alone, and
post-wrap routes survive with the guard removed.

Adds a real cordis + webserver reload repro and per-entry unit tests,
records the change as decision D27 (superseding impl-m1 D6's snapshot
clause), and updates the code-review skill rule that asked for an exact
pre-wrap restore.
`engines.dsh` and the `@deepseek-ai/dsh-storage-domain` peer both end in
`>=0.2.0-rc.1` now; the three caret alternatives stay as the record of the
host lines that were actually verified. A hand-added alternative always
lags a dsh release, and a rejected bundle is skipped silently, so the login
gate would disappear rather than fail loudly. The guard test now asserts the
floor covers every later host version, and the readme, specs and D27 record
the trade-off. No source change (D27).
…rollback)

Brings in the guard unwrap rollback fix (#107) and the 0.17.1 release. That
fix's decision record already holds D27, so the corridor-floor record written
on 2026-10-07 is renumbered D28 and its references follow.
The host half moves into src/host/ with FSD's lower three layers
(shared, entities, features) and the client half takes the five-layer
layout, both checked by Steiger, matching dsh-lingualab. Entries and
assembly stay at the src/ root; integration tests move to test/.
Once implemented it supersedes the directory parts of D5 and D11.
The build never emptied lib/, so outputs of moved or deleted sources
stayed tracked (lib/features/session/, lib/skill-install.*), and the
CI parity diff cannot see leftovers. scripts/clean-lib.mjs now removes
lib/ first; the rebuild deletes 36 stale or test-only files.

tsconfig.build.json excludes only *.test.ts, so test helpers under src/
were compiled into lib/ and shipped. The 14 integration tests and their
4 helpers move to test/integration/, and the admin and password-login
harnesses move to test/ beside the existing ones. Vitest, ESLint,
run-gates, the slice-check whitelist, the development docs and the
bundled skills follow the new paths. Step 1 of the D29 plan.
Move src/client into shared, entities/account, four features
(change-password, admin-users, logout, nav-icon) and the
account-section widget, each slice behind an index.ts barrel.
index.tsx stays at the client root; there is no pages or app layer.

lint:fsd now runs Steiger over both halves, and slice:check only
accepts client files under the five layer folders plus index.tsx and
its test. lib/ is rebuilt from the new layout.
Move D29 to implemented/ with its text describing the landed layout
and register it in docs/decisions.md. D5 (layered src with barrels)
and D11 (HTTP endpoint layer) are superseded by D29 and move to
archived/ unchanged; their hashes seed archived/.manifest.json. D9
stays implemented because its injection rule still applies.

The old src-refactor-plan docs gain a note that D29 supersedes them.
tsconfig.client.build.json excluded only *.test.tsx, so the five
client *.test.ts files emitted .d.ts and .d.ts.map into lib/client/
and shipped with the package. Exclude *.test.ts as well.

bundle:check now fails if any *.test.* or *.spec.* file appears
anywhere under lib/, so the leak cannot come back unnoticed.
A top-level GET that arrives cross-site (Sec-Fetch-Site: cross-site,
Sec-Fetch-Dest: document) carries the gate's Lax cookie but not dsh's
SameSite=Strict session cookie, so dsh answered 401 and a reload kept the
cross-site context. For requests the gate allows, serve a static bounce page
that re-navigates via location.replace(location.href); the retry is
same-origin, so the Strict cookie is sent and the bounce cannot loop.
development's D29 (cross-site navigation bounce) shipped first, so the FSD layering record is renumbered to D30. cross-site-bounce moves into src/host/shared/guard/ and its integration test into test/integration/.
The route paths, the password-changed notice key and the URLs built from
them were written more than twenty times across both halves, with two
separate LOGIN_PATH definitions on the host side. src/protocol/index.ts
now defines them once (pure constants and functions, no imports). The
host enters through src/host/shared/protocol/ and the client through
src/client/shared/api/protocol.ts. slice:check allows only those two
bridges to import the contract.

The p11 source pin for the client redirect now asserts that the client
uses the contract constant instead of comparing literals. Records D31
and updates the structure docs.
scripts/check-host.mjs checks what the dsh host relies on and no type
checker sees: the package name, the cordis.patch.yml Loader row, the
client registration id and the host entry name agree; every manifest
target exists after a build; every require() in lib/client.js and every
CLIENT_EXTERNALS entry is a module the shell seeds (mirrors dsh
PLATFORM_MODULES, logged as debt in .rsp/TechDebt.md); no route literal
lives outside src/protocol, every registration names a protocol
constant, every protocol path is registered, and every route the READMEs
mention exists.

--self-test runs eighteen broken fixture trees, one per rule, and fails
if any of them passes. The gate runs in verify after bundle:check, in
the CI build job and in the build scenario of run-gates.
Extend the D31 cross-half contract from route paths to the request,
response and error bodies of the /auth endpoints. src/protocol/payloads.ts
holds them as pure types (StatusResponse, AdminUserRow, UsersResponse,
the password-change and admin-reset request/response/error bodies,
UserRole, SessionKind, PasswordRule, Unchecked<T>), re-exported by
src/protocol/index.ts and reached through the two existing bridges.

Host: the status, users, reset and password-change writers are typed
with the contract bodies; form fields are read through the typed
formField<Request>() helper; the user and session entities take
UserRole, SessionKind and PasswordRule from shared/protocol, so no @x
import is needed.

Client: the hand-written admin-types.ts mirror is deleted.
AccountStatusView derives from StatusResponse, AdminUserRow comes from
shared/api, form and result types move to features/admin-users/model,
request bodies are built from contract-typed objects via encodeForm,
and parsers read keys through Unchecked<T>. Runtime behaviour is
unchanged.

Docs: D31 records, the decisions index, the D30 placement table and
docs/specs/development(_zh).md describe the payload half.
)

* feat: add first-run launch-token onboarding for unconfigured instances

* docs: document the first-run bootstrap channel and rewrite quick start

* build: regenerate lib/client.js with the locked rolldown
@TecFancy
TecFancy merged commit f73a13e into main Oct 26, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 首次安装插件直接重启后,用户无法登录

1 participant