From 82ae763772f81592fc5c4b60f46e3d7d28866979 Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Sun, 20 Sep 2026 16:27:24 +0800 Subject: [PATCH] fix(deepseek-account): rename login source parameter --- apps/desktop/src/account-backend.ts | 2 +- apps/desktop/tests/account-backend.spec.ts | 2 +- apps/desktop/tests/welcome-flow.e2e.ts | 2 +- docs/sign-in-readiness.i18n.yaml | 4 +-- docs/sign-in-readiness.md | 4 +-- docs/sign-in-readiness.zh.md | 4 +-- docs/subsystems/credentials.i18n.yaml | 4 +-- docs/subsystems/credentials.md | 4 +-- docs/subsystems/credentials.zh.md | 4 +-- packages/api/account-controller/src/index.ts | 4 +-- .../README.i18n.yaml | 4 +-- .../deepseek-account-platform/README.md | 2 +- .../deepseek-account-platform/README.zh.md | 2 +- .../deepseek-account-platform/src/index.ts | 12 ++++---- .../tests/account.spec.ts | 30 ++++++++++++------- .../credentials/deepseek-account/src/index.ts | 4 +-- .../extensions/tool-cordis/src/api-catalog.ts | 4 +-- 17 files changed, 51 insertions(+), 41 deletions(-) diff --git a/apps/desktop/src/account-backend.ts b/apps/desktop/src/account-backend.ts index 964675e654..0db791f631 100644 --- a/apps/desktop/src/account-backend.ts +++ b/apps/desktop/src/account-backend.ts @@ -78,7 +78,7 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo const call = async (method: string, args: Record = {}): Promise => accountView(await invoke({ namespace: 'account', method, args })) return { - state: () => call('getState'), start: locale => call('startSignIn', { locale, callbackOrigin: new URL(origin).origin, client: 'desktop' }), + state: () => call('getState'), start: locale => call('startSignIn', { locale, callbackOrigin: new URL(origin).origin, loginSource: 'desktop' }), cancel: attemptId => call('cancelSignIn', { attemptId }), signOut: () => call('signOut'), watch(listener, failed) { let closed = false diff --git a/apps/desktop/tests/account-backend.spec.ts b/apps/desktop/tests/account-backend.spec.ts index f0078fac26..9c25ae1026 100644 --- a/apps/desktop/tests/account-backend.spec.ts +++ b/apps/desktop/tests/account-backend.spec.ts @@ -22,5 +22,5 @@ it('uses account Remote commands without returning additional wire fields', asyn return Promise.resolve({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null, token: 'private' }) }, () => Promise.resolve('')) expect(await backend.start('en')).toEqual({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null }) - expect(requests).toEqual([{ namespace: 'account', method: 'startSignIn', args: { locale: 'en', callbackOrigin: 'http://127.0.0.1:1234', client: 'desktop' } }]) + expect(requests).toEqual([{ namespace: 'account', method: 'startSignIn', args: { locale: 'en', callbackOrigin: 'http://127.0.0.1:1234', loginSource: 'desktop' } }]) }) diff --git a/apps/desktop/tests/welcome-flow.e2e.ts b/apps/desktop/tests/welcome-flow.e2e.ts index d2e0d947c6..26ac0cb2f8 100644 --- a/apps/desktop/tests/welcome-flow.e2e.ts +++ b/apps/desktop/tests/welcome-flow.e2e.ts @@ -153,7 +153,7 @@ describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => { await expect.poll(async () => (await backend!.account.state()).attempt?.phase).toBe('waiting-browser') const response = await fetch(platform.callback(), { redirect: 'manual' }) expect(response.status).toBe(302) - expect(response.headers.get('location')).toBe(`${platform.origin}/dsh/authorized?result=test&locale=zh_CN&client_type=desktop`) + expect(response.headers.get('location')).toBe(`${platform.origin}/dsh/authorized?result=test&locale=zh_CN&login_source=desktop`) expect(await status()).toMatchObject({ hasApiKey: true, loggedIn: true }) await restart() expect(await status()).toMatchObject({ hasApiKey: true, loggedIn: true }) diff --git a/docs/sign-in-readiness.i18n.yaml b/docs/sign-in-readiness.i18n.yaml index 9b98b53deb..0fe6c21670 100644 --- a/docs/sign-in-readiness.i18n.yaml +++ b/docs/sign-in-readiness.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/sign-in-readiness.md -sign-in-readiness.md: 25244b1a5939c7b0f1337b23d2e2d14bfabe3125 -sign-in-readiness.zh.md: b19db05620af8cf733b12c49cc459834827f7e70 +sign-in-readiness.md: 0d13f38060a7ed7feeef48b3c56e6e5a6ffc3ec0 +sign-in-readiness.zh.md: 38672b53dec5a4deeb2b8f9830162347f343b508 diff --git a/docs/sign-in-readiness.md b/docs/sign-in-readiness.md index 25244b1a59..0d13f38060 100644 --- a/docs/sign-in-readiness.md +++ b/docs/sign-in-readiness.md @@ -15,7 +15,7 @@ Keep completed items and their evidence; do not delete the checklist. Before com ## Pending work and completion criteria - [x] **Loopback address policy (DSH / Platform / backend)**: User confirmed on 2026-09-15 that the backend supports localhost. Preserve the hostname and browser-visible port without conversion to an IP. DSH, Platform, and mock validation now accept exact localhost and reject lookalike domains. Evidence on 2026-09-15: 40 DSH account tests and lint passed; 72 Platform tests, two type checks, and lint passed, including successful exchange with the original URI and rejection after conversion to an IP. The callback validator also accepts [::1] and requires an explicit port under the updated backend protocol; real IPv6 listener/forwarding acceptance remains pending. -- [ ] **Real authorization API integration (backend / both applications)**: Verify auth_init client_type binding, authorize business codes, exchange token/authorized_url, auth_cancel verifier validation and idempotency, and invalidation of canceled requests and unexchanged codes against the real backend. Verify exchange cannot override desktop/web origin type. Mock implementation is not evidence of backend completion. +- [ ] **Real authorization API integration (backend / both applications)**: Verify auth_init login_source binding, authorize business codes, exchange token/authorized_url, auth_cancel verifier validation and idempotency, and invalidation of canceled requests and unexchanged codes against the real backend. Verify exchange cannot override desktop/web origin type. Mock implementation is not evidence of backend completion. - [ ] **Real DSH token usage and logout (backend / DSH)**: Verify current, balance, inference, and file endpoints with a real DSH token; account credentials must still accompany inference/file requests only at allowed api.deepseek.com destinations. Verify restart restoration, explicit Platform logout, local credential deletion, and remote revocation outcomes. Use a dedicated test account to avoid revoking a shared daily-use dev token. - [ ] **Latest Web tab integration (both applications)**: At localhost:8081, verify new-tab authorization, automatic closure on success and exchange failure, original-tab failure feedback, and explicit retry. Cover timeout, cancellation, blocked automatic closure, manually copied links, and localhost access through SSH with different local/remote ports and IPv4/IPv6 listener configurations. Confirm no second Web UI opens and Web completion has no desktop button. - [ ] **Native client and installer acceptance (DSH)**: With current artifacts, verify Host readiness before login, workspace loading after login, failure/timeout recovery, and window focus. Verify installed dsh://open registration, cold launch, and activation of an existing app on supported release platforms. Earlier manual checks do not replace acceptance of the latest changes. @@ -40,4 +40,4 @@ When completing an item, append the date, implementation location, and test comm 2026-09-16: macOS development `Harness Dev.app` was registered and verified through the real Platform completion-page button in Chrome. Both opening an already running window and cold-starting after quitting succeeded, preserving the account display. This does not replace installed release-platform acceptance. -- [ ] **Completion-page context (backend / both applications)**: The supplied backend auth_init schema does not store client_type (desktop/web) or locale. Confirm how authorized_url selects desktop open-app versus Web close-tab behavior and language. DSH continues sending both fields until a replacement is agreed; sending them alone does not prove backend support. +- [ ] **Completion-page context (backend / both applications)**: The supplied backend auth_init schema does not store login_source (desktop/web) or locale. Confirm how authorized_url selects desktop open-app versus Web close-tab behavior and language. DSH continues sending both fields until a replacement is agreed; sending them alone does not prove backend support. diff --git a/docs/sign-in-readiness.zh.md b/docs/sign-in-readiness.zh.md index b19db05620..38672b53de 100644 --- a/docs/sign-in-readiness.zh.md +++ b/docs/sign-in-readiness.zh.md @@ -15,7 +15,7 @@ ## 待办与完成条件 - [x] **Loopback 地址策略(DSH/Platform/后端)**:用户于 2026-09-15 明确后端支持 localhost,保留主机名及浏览器实际访问端口,不转换 IP。DSH、Platform 与 mock 已对齐精确 localhost 校验并拒绝仿冒域名。验收:2026-09-15,DSH 40 项账号测试及 lint 通过;Platform 72 项测试、两个类型检查及 lint 通过,覆盖原 URI 兑换成功、转换 IP 后兑换失败。新版后端协议下,回调校验也接受 [::1] 并要求显式端口;真实 IPv6 监听和转发仍待验收。 -- [ ] **真实授权接口联调(后端/两端)**:在真实后端验证 auth_init 的 client_type 绑定、authorize 业务码、exchange 的 token/authorized_url、auth_cancel 的 verifier 校验和幂等、取消后申请及未兑换 code 失效。验证 desktop 与 web 来源不能在兑换时被覆盖;mock 实现不算后端完成证据。 +- [ ] **真实授权接口联调(后端/两端)**:在真实后端验证 auth_init 的 login_source 绑定、authorize 业务码、exchange 的 token/authorized_url、auth_cancel 的 verifier 校验和幂等、取消后申请及未兑换 code 失效。验证 desktop 与 web 来源不能在兑换时被覆盖;mock 实现不算后端完成证据。 - [ ] **真实 DSH token 使用与退登(后端/DSH)**:验证 current、余额、推理和文件接口均可使用真实 DSH token;验证推理/文件请求仍仅在允许的 api.deepseek.com 地址附带账号凭证。验证重启恢复、主动退登调用 Platform logout、本地凭证删除与远端撤销结果。使用独立测试账号,避免撤销日常账号的共享 dev token。 - [ ] **最新 Web 标签页流程联调(两端)**:使用 localhost:8081 跑通新标签页授权、成功自动关闭、exchange 失败关闭、原页失败弹窗和手动重试;覆盖超时、取消、自动关页受限、手动复制链接,以及 SSH 本地/远端端口不同和 IPv4/IPv6 监听配置下的 localhost 访问。确认不再打开第二个 Web UI,Web 完成页不显示桌面按钮。 - [ ] **原生客户端与安装包验收(DSH)**:在最新产物验证登录前 Host ready、登录后主界面加载、失败/超时恢复和窗口聚焦;在支持的发布平台验证安装包 dsh://open 注册、冷启动及已运行时唤起。既有旧版本手测不能替代最新改动验收。 @@ -40,4 +40,4 @@ 2026-09-16:已注册 macOS 开发用 `Harness Dev.app`,并通过 Chrome 中真实 Platform 完成页按钮验证运行中唤起和完全退出后的冷启动,账号展示保持正常。这不替代正式安装包及其他发布平台验收。 -- [ ] **完成页上下文(后端/两端)**:提供的后端 auth_init schema 未保存 client_type(desktop/web)或 locale。确认 authorized_url 如何区分桌面打开应用与 Web 关闭标签页及语言。替代方案确定前 DSH 继续发送两字段;发送字段本身不代表后端支持。 +- [ ] **完成页上下文(后端/两端)**:提供的后端 auth_init schema 未保存 login_source(desktop/web)或 locale。确认 authorized_url 如何区分桌面打开应用与 Web 关闭标签页及语言。替代方案确定前 DSH 继续发送两字段;发送字段本身不代表后端支持。 diff --git a/docs/subsystems/credentials.i18n.yaml b/docs/subsystems/credentials.i18n.yaml index dbc81d0e83..8ffc27a0b4 100644 --- a/docs/subsystems/credentials.i18n.yaml +++ b/docs/subsystems/credentials.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/credentials.md -credentials.md: 75d592bc15d4de8da7225253e2a310e74b9f15fd -credentials.zh.md: 23df660253af3dc035e78cac4856ddb872b2e574 +credentials.md: f0d64c47cbe886941ed8ad8d3d41304dcc76a493 +credentials.zh.md: c216cc58505f9f300626e5943e479df19537a9fa diff --git a/docs/subsystems/credentials.md b/docs/subsystems/credentials.md index 75d592bc15..f0d64c47cb 100644 --- a/docs/subsystems/credentials.md +++ b/docs/subsystems/credentials.md @@ -288,10 +288,10 @@ abstract getBalance(): Promise * Join an active attempt or start browser authorization. * @param locale - active UI language for a new attempt; joining retains its original language. * @param callbackOrigin - browser-accessible loopback HTTP origin, including any SSH local port. - * @param client - initiating UI, used to return from a failed exchange. + * @param loginSource - initiating UI, used to return from a failed exchange. * @returns the initial snapshot without waiting for browser approval. */ -abstract startSignIn(locale: string, callbackOrigin: string, client: 'web' | 'desktop'): Promise +abstract startSignIn(locale: string, callbackOrigin: string, loginSource: 'web' | 'desktop'): Promise /** * Cancel only the named attempt; committing attempts settle before returning. diff --git a/docs/subsystems/credentials.zh.md b/docs/subsystems/credentials.zh.md index 23df660253..c216cc5850 100644 --- a/docs/subsystems/credentials.zh.md +++ b/docs/subsystems/credentials.zh.md @@ -288,10 +288,10 @@ abstract getBalance(): Promise * Join an active attempt or start browser authorization. * @param locale - active UI language for a new attempt; joining retains its original language. * @param callbackOrigin - browser-accessible loopback HTTP origin, including any SSH local port. - * @param client - initiating UI, used to return from a failed exchange. + * @param loginSource - initiating UI, used to return from a failed exchange. * @returns the initial snapshot without waiting for browser approval. */ -abstract startSignIn(locale: string, callbackOrigin: string, client: 'web' | 'desktop'): Promise +abstract startSignIn(locale: string, callbackOrigin: string, loginSource: 'web' | 'desktop'): Promise /** * Cancel only the named attempt; committing attempts settle before returning. diff --git a/packages/api/account-controller/src/index.ts b/packages/api/account-controller/src/index.ts index 17535ef5ca..685412244d 100644 --- a/packages/api/account-controller/src/index.ts +++ b/packages/api/account-controller/src/index.ts @@ -32,11 +32,11 @@ export class AccountController extends TypertRemoteService { * Begin browser sign-in. * @param locale - active UI language for a new attempt. * @param callbackOrigin - browser-accessible loopback HTTP origin. - * @param client - initiating UI, used to return from a failed exchange. + * @param loginSource - initiating UI, used to return from a failed exchange. * @returns a new or already-active login attempt. */ @Remote - startSignIn(locale: string, callbackOrigin: string, client: 'web' | 'desktop'): Promise { return this.ctx.deepseekAccount.startSignIn(locale, callbackOrigin, client) } + startSignIn(locale: string, callbackOrigin: string, loginSource: 'web' | 'desktop'): Promise { return this.ctx.deepseekAccount.startSignIn(locale, callbackOrigin, loginSource) } /** * Cancel the named local attempt. * @param attemptId - attempt to cancel. diff --git a/packages/credentials/deepseek-account-platform/README.i18n.yaml b/packages/credentials/deepseek-account-platform/README.i18n.yaml index f850ceea36..7315a4b6c1 100644 --- a/packages/credentials/deepseek-account-platform/README.i18n.yaml +++ b/packages/credentials/deepseek-account-platform/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/credentials/deepseek-account-platform/README.md -README.md: db337c7f999c1ce8055e3a427813189e84d53194 -README.zh.md: 510ee10be0d58dc8c4e6b4d78a8ce1f7a837e80a +README.md: 248f679dd7b27af35eafa59579df7538db213b23 +README.zh.md: 36c3f861d60f2c02cca76feabed85862630ead98 diff --git a/packages/credentials/deepseek-account-platform/README.md b/packages/credentials/deepseek-account-platform/README.md index db337c7f99..248f679dd7 100644 --- a/packages/credentials/deepseek-account-platform/README.md +++ b/packages/credentials/deepseek-account-platform/README.md @@ -30,7 +30,7 @@ The profile projection maps `id_profile.picture` to `avatarUrl`, using null when getProfile / getBalance sends the stored grant in the x-dsh-auth-token header to GET /auth-api/v0/users/current and GET /api/v0/users/get_user_summary on platformOrigin. The grant issuer must match that origin. Host projects only the account UID, profile name, avatar URL, phone or email exactly as masked by Platform, and normal_wallets / bonus_wallets currency/balance strings; response tokens and other fields are discarded. Bonus wallets remain separate from recharge balances. Credential changes and disposal invalidate in-flight queries. -Configure platformOrigin, allowLoopbackHttp, requestTimeoutMs, and attemptTimeoutMs in the plugin row. HTTP is accepted only for explicitly enabled loopback development. The provider registers /oauth/callback on the existing Host webServer before auth_init, validates state, exchanges an S256 PKCE code once, and commits a grant before redirecting to auth_exchange.biz_data.authorized_url. Browser destinations default to the configured platform origin and always require the fixed /dsh/authorize or /dsh/authorized path. The completion URL sets `client_type` to the initiating client (`web` or `desktop`) and preserves other Platform-supplied query parameters. Device identity is a separate random UUID record shared by processes using the same credential store; device_model reports OS and architecture. +Configure platformOrigin, allowLoopbackHttp, requestTimeoutMs, and attemptTimeoutMs in the plugin row. HTTP is accepted only for explicitly enabled loopback development. The provider registers /oauth/callback on the existing Host webServer before auth_init, validates state, exchanges an S256 PKCE code once, and commits a grant before redirecting to auth_exchange.biz_data.authorized_url. Browser destinations default to the configured platform origin and always require the fixed /dsh/authorize or /dsh/authorized path. The completion URL sets `login_source` to the initiating client (`web` or `desktop`) and preserves other Platform-supplied query parameters. Device identity is a separate random UUID record shared by processes using the same credential store; device_model reports OS and architecture. Platform URLs have one configuration owner: `platformOrigin`. Authorization requests, browser validation, completion redirects, usage, and top-up destinations all use that origin. Set it in a private `$DSH_HOME/cordis.patch.yml`; deployment addresses do not belong in source control. HTTPS is required except for loopback HTTP with `allowLoopbackHttp: true`. Model and file token destinations use the separate `inferenceOrigin` configuration. diff --git a/packages/credentials/deepseek-account-platform/README.zh.md b/packages/credentials/deepseek-account-platform/README.zh.md index 510ee10be0..36c3f861d6 100644 --- a/packages/credentials/deepseek-account-platform/README.zh.md +++ b/packages/credentials/deepseek-account-platform/README.zh.md @@ -30,7 +30,7 @@ getPlatformSession 仅在已存授权的 issuer 与 platformOrigin 一致时导 getProfile / getBalance 将保存的授权 token 通过 x-dsh-auth-token 请求头,向 platformOrigin 上的 GET /auth-api/v0/users/current 和 GET /api/v0/users/get_user_summary 发起请求。授权签发来源必须与该来源一致。Host 只投影账号 UID、资料名称、头像 URL、由 Platform 脱敏的手机号或邮箱(原样保留),以及 normal_wallets / bonus_wallets 的币种和余额字符串,丢弃响应 token 与其他字段。赠送钱包不计入充值余额。凭证变化和销毁会使进行中的查询失效。 -在插件行配置 platformOrigin、allowLoopbackHttp、requestTimeoutMs 和 attemptTimeoutMs。HTTP 仅用于显式启用的本机开发。提供者先在现有 Host webServer 注册 /oauth/callback,再调用 auth_init;校验 state、使用 S256 PKCE 授权码兑换一次,并在跳转 auth_exchange.biz_data.authorized_url 前提交授权记录。浏览器地址默认要求匹配配置的平台来源,并始终要求固定的 /dsh/authorize 或 /dsh/authorized 路径。完成页地址将 `client_type` 设为发起登录的客户端类型(`web` 或 `desktop`),并保留平台返回的其他查询参数。设备标识是独立的随机 UUID 记录,由使用同一凭证存储的进程共享;device_model 报告操作系统和架构。 +在插件行配置 platformOrigin、allowLoopbackHttp、requestTimeoutMs 和 attemptTimeoutMs。HTTP 仅用于显式启用的本机开发。提供者先在现有 Host webServer 注册 /oauth/callback,再调用 auth_init;校验 state、使用 S256 PKCE 授权码兑换一次,并在跳转 auth_exchange.biz_data.authorized_url 前提交授权记录。浏览器地址默认要求匹配配置的平台来源,并始终要求固定的 /dsh/authorize 或 /dsh/authorized 路径。完成页地址将 `login_source` 设为发起登录的客户端类型(`web` 或 `desktop`),并保留平台返回的其他查询参数。设备标识是独立的随机 UUID 记录,由使用同一凭证存储的进程共享;device_model 报告操作系统和架构。 开放平台 URL 统一由 `platformOrigin` 配置管理。授权请求、浏览器地址校验、完成跳转、用量和充值入口都使用该来源。将配置放在私有的 `$DSH_HOME/cordis.patch.yml`,部署地址不进入源码仓库。默认要求 HTTPS;只有本机 HTTP 可通过 `allowLoopbackHttp: true` 显式启用。模型及文件 token 的目标由独立的 `inferenceOrigin` 配置管理。 diff --git a/packages/credentials/deepseek-account-platform/src/index.ts b/packages/credentials/deepseek-account-platform/src/index.ts index d8eda3c8ff..bea62a3558 100644 --- a/packages/credentials/deepseek-account-platform/src/index.ts +++ b/packages/credentials/deepseek-account-platform/src/index.ts @@ -69,7 +69,7 @@ interface Attempt { done: Promise running: Promise origin: string - client: 'web' | 'desktop' + loginSource: 'web' | 'desktop' disposeCallback?: () => Promise callback?: ServerResponse initialProfile?: { token: string; value: AccountDetails['profile'] } @@ -238,7 +238,7 @@ export class PlatformAccount extends DeepSeekAccount { return result.data.token } - override async startSignIn(locale: string, callbackOrigin: string, client: 'web' | 'desktop'): Promise { + override async startSignIn(locale: string, callbackOrigin: string, loginSource: 'web' | 'desktop'): Promise { if (this.removing !== undefined) await this.removing const origin = loginOrigin(callbackOrigin) if (this.closed) throw new PlatformAuthError('protocol') @@ -254,7 +254,7 @@ export class PlatformAccount extends DeepSeekAccount { if (this.attempt !== previous) return this.getState() } const attempt: Attempt = { - origin, client, + origin, loginSource, locale: locale.toLowerCase().split(/[-_]/)[0] === 'zh' ? 'zh_CN' : 'en_US', view: { id: randomUUID() as SignInAttemptId, phase: 'initializing' }, controller: new AbortController(), done: Promise.resolve(), running: Promise.resolve(), @@ -398,7 +398,7 @@ export class PlatformAccount extends DeepSeekAccount { const redirectUri = `${attempt.origin}/oauth/callback` const init = initialization.safeParse(await this.request('auth_init', { code_challenge: challenge, code_challenge_method: 'S256', state, redirect_uri: redirectUri, locale: attempt.locale, - client_type: attempt.client, + login_source: attempt.loginSource, }, signal), { reportInput: true }) if (!init.success) { console.info('[deepseek-account] payload rejected', { @@ -437,7 +437,7 @@ export class PlatformAccount extends DeepSeekAccount { throw new PlatformAuthError('protocol') } const completionUrl = new URL(browserUrl(result.data.authorized_url, this.origin, '/dsh/authorized', this.rewriteBrowserOrigin)) - completionUrl.searchParams.set('client_type', attempt.client) + completionUrl.searchParams.set('login_source', attempt.loginSource) attempt.completionUrl = completionUrl.href signal.throwIfAborted() if (result.data.user != null) { @@ -476,7 +476,7 @@ export class PlatformAccount extends DeepSeekAccount { } private finishFailedCallback(attempt: Attempt): void { - if (attempt.client === 'web') { + if (attempt.loginSource === 'web') { const nonce = randomBytes(16).toString('base64url') const message = attempt.locale === 'zh_CN' ? '登录失败,请关闭此标签页并在原页面重试。' : 'Sign-in failed. Close this tab and try again in the original tab.' diff --git a/packages/credentials/deepseek-account-platform/tests/account.spec.ts b/packages/credentials/deepseek-account-platform/tests/account.spec.ts index 36980be92c..407a56f89b 100644 --- a/packages/credentials/deepseek-account-platform/tests/account.spec.ts +++ b/packages/credentials/deepseek-account-platform/tests/account.spec.ts @@ -171,8 +171,9 @@ it('stores a grant before redirecting, restores account presence, and signs out expect((await fetch(f.callback('wrong'))).status).toBe(400) const response = await fetch(f.callback(), { redirect: 'manual' }) expect(response.status).toBe(302) - expect(f.init().client_type).toBe('desktop') - expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN&client_type=desktop`) + expect(f.init().login_source).toBe('desktop') + expect(f.init()).not.toHaveProperty('client_type') + expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN&login_source=desktop`) expect((await f.account.getState()).status).toBe('credential-stored') expect(await readFile(join(f.home, 'credentials.yaml'), 'utf8')).toContain('dsh_mock_test') expect(await f.account.getPlatformSession()).toEqual({ origin: f.origin, token: 'dsh_mock_test' }) @@ -230,23 +231,32 @@ it('maps both returned browser pages to the development origin across the login expect((await f.wait('waiting-browser')).attempt?.authorizeUrl).toBe(`${f.origin}/dsh/authorize?authorize_id=test`) const response = await fetch(f.callback(), { redirect: 'manual' }) expect(response.status).toBe(302) - expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=a%2Fb&locale=zh_CN&client_type=desktop`) + expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=a%2Fb&locale=zh_CN&login_source=desktop`) expect((await f.account.getState()).status).toBe('credential-stored') }) it.each([ ['web', ''], ['desktop', ''], - ['web', '&client_type=desktop&client_type=desktop'], - ['desktop', '&client_type=web&client_type=web'], -] as const)('redirects the %s login completion with its client type when Platform returns %s', async (client, query) => { + ['web', '&login_source=desktop&login_source=desktop'], + ['desktop', '&login_source=web&login_source=web'], +] as const)('redirects the %s login completion with its login source when Platform returns %s', async (client, query) => { const f = await fixture() f.exchangeResponse({ authorized_url: `${f.origin}/dsh/authorized?result=a%2Fb&locale=zh_CN${query}` }) await f.account.startSignIn('en', f.callbackOrigin, client) await f.wait('waiting-browser') const response = await fetch(f.callback(), { redirect: 'manual' }) expect(response.status).toBe(302) - expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=a%2Fb&locale=zh_CN&client_type=${client}`) + expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=a%2Fb&locale=zh_CN&login_source=${client}`) +}) + +it('preserves Platform business client_type in the completion URL', async () => { + const f = await fixture() + f.exchangeResponse({ authorized_url: `${f.origin}/dsh/authorized?client_type=DSH` }) + await f.account.startSignIn('en', f.callbackOrigin, 'web') + await f.wait('waiting-browser') + const response = await fetch(f.callback(), { redirect: 'manual' }) + expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?client_type=DSH&login_source=web`) }) it.each([undefined, 'https://other.example/dsh/authorized', '/dsh/authorized'])('rejects an invalid exchange completion URL %s before storing a token', async (authorizedUrl) => { @@ -454,7 +464,7 @@ it('closes the failed Web authorization tab without redirecting and keeps the sh expect(response.headers.get('content-security-policy')).toContain(`script-src 'nonce-${nonce}'`) await expect(`${page.match(/

(.*?)<\/p>/)![1]}\n`) .toMatchFileSnapshot('./expected/web-exchange-failure.txt') - expect(f.init().client_type).toBe('web') + expect(f.init().login_source).toBe('web') expect(await f.wait('failed')).toMatchObject({ status: 'signed-out', attempt: { errorCode: 'protocol' } }) expect(f.count()).toBe(1) expect(await (await fetch(`${f.callbackOrigin}/health`)).text()).toBe('alive') @@ -462,7 +472,7 @@ it('closes the failed Web authorization tab without redirecting and keeps the sh await f.account.startSignIn('en', f.callbackOrigin, 'web') await f.wait('waiting-browser') const success = await fetch(f.callback(), { redirect: 'manual' }) - expect(success.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN&client_type=web`) + expect(success.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN&login_source=web`) expect(f.count()).toBe(2) }) @@ -491,7 +501,7 @@ it('completes login through a local TCP forward using the browser port rather th await f.wait('waiting-browser') expect(f.init().redirect_uri).toBe(`${forwardedOrigin}/oauth/callback`) const response = await fetch(f.callback(), { redirect: 'manual' }) - expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN&client_type=web`) + expect(response.headers.get('location')).toBe(`${f.origin}/dsh/authorized?result=test&locale=zh_CN&login_source=web`) expect((await f.account.getState()).status).toBe('credential-stored') }) diff --git a/packages/credentials/deepseek-account/src/index.ts b/packages/credentials/deepseek-account/src/index.ts index cfd5c99718..465bbedf03 100644 --- a/packages/credentials/deepseek-account/src/index.ts +++ b/packages/credentials/deepseek-account/src/index.ts @@ -42,10 +42,10 @@ export abstract class DeepSeekAccount extends Service { * Join an active attempt or start browser authorization. * @param locale - active UI language for a new attempt; joining retains its original language. * @param callbackOrigin - browser-accessible loopback HTTP origin, including any SSH local port. - * @param client - initiating UI, used to return from a failed exchange. + * @param loginSource - initiating UI, used to return from a failed exchange. * @returns the initial snapshot without waiting for browser approval. */ - abstract startSignIn(locale: string, callbackOrigin: string, client: 'web' | 'desktop'): Promise + abstract startSignIn(locale: string, callbackOrigin: string, loginSource: 'web' | 'desktop'): Promise /** * Cancel only the named attempt; committing attempts settle before returning. * @param id - attempt identity from this Host. diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index dde811632d..a56e94240d 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -882,9 +882,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'balance outcome, or null if signed out or the grant changed during the query.', }, { - signature: 'abstract startSignIn(locale: string, callbackOrigin: string, client: \'web\' | \'desktop\'): Promise', + signature: 'abstract startSignIn(locale: string, callbackOrigin: string, loginSource: \'web\' | \'desktop\'): Promise', description: 'Join an active attempt or start browser authorization.', - parameters: [{ name: 'locale', description: 'active UI language for a new attempt; joining retains its original language.' }, { name: 'callbackOrigin', description: 'browser-accessible loopback HTTP origin, including any SSH local port.' }, { name: 'client', description: 'initiating UI, used to return from a failed exchange.' }], + parameters: [{ name: 'locale', description: 'active UI language for a new attempt; joining retains its original language.' }, { name: 'callbackOrigin', description: 'browser-accessible loopback HTTP origin, including any SSH local port.' }, { name: 'loginSource', description: 'initiating UI, used to return from a failed exchange.' }], returns: 'the initial snapshot without waiting for browser approval.', }, {