You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c779611
Browse filesBrowse the repository at this point in the historyBrowse files
feat(automation): connector connection read answers redirectUri; a client-less reconnect reuses the kept installer client (#29701)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8NQ1AWCdjDL4Yzk8V1Cf7d6)_Fixes#29463
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:连接器连接读取接口(`GET
/api/v1/automation/connectors/NAME/connection`)现在返回本部署的 OAuth 回调地址
`redirectUri`;安装方自带客户端的实例,重连时不填客户端就沿用已保存的那个。
- 为什么改:管理员要先把回调地址登记到厂商那里才能拿到客户端,控制台此前读不到这个地址;重连时控制台也拿不回已存的密钥,只能让人重新输入。
- 风险与回滚:只读接口多一个可选字段,没有配置基础地址时字段缺省、不报错;发起连接的 503
不变。重连沿用仅限同一组织的已存客户端,撤销后不再沿用。回滚即撤回本 PR。
- 先看:`packages/services/service-automation/src/plugin.ts` 的
`derivedConnectorOAuthRedirectUri`、`getConnectorConnection`、`keptInstallerClient`
三处;其次 `connector-connection-door.test.ts` 新增的四个用例。
- 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run
--maxWorkers=2 src/connector-connection-door.test.ts`,预期 34 个用例全部通过。
- 需要你做的:无,席位落地。
## What changes
**Ask 1: the connection read answers `redirectUri`.**
`ServiceAutomationPlugin` gets one derivation,
`derivedConnectorOAuthRedirectUri()`: the `connectorOAuthRedirectUri`
plugin option, else `/api/v1/automation/connectors/oauth/callback` under
`OS_AUTH_URL`, else under `OS_BASE_URL`, else `undefined`.
`connectorOAuthRedirectUri()` (the begin path) now calls it and keeps
its `503` byte-for-byte when it answers `undefined`.
`getConnectorConnection` sets `redirectUri` on its answer whenever the
derivation answers, whatever `status` says, and leaves the key out
otherwise (never a refusal, never the request's origin), as the
`ConnectorConnectionState.redirectUri` TSDoc landed by #29618 promises.
A `client_credentials` instance has no connection: its read stays the
existing `400 VALIDATION_ERROR`, so it never carries the key. The revoke
and callback answers are unchanged.
**Ask 2: a client-less reconnect reuses the kept installer client.**
`installerClientOfBegin` is now async and takes the organization: a
begin naming no client on a `clientSource: 'installer'` instance reads
the organization's connection (`keptInstallerClient`) and, when it keeps
an installer client, authorizes, exchanges and re-stores as that client.
With none kept (a first connect, another organization, or after a
revoke) it is refused `400` with the same text as before. A begin naming
a client replaces the kept one, as before. A store that cannot read or
decrypt the kept client refuses with the store's existing `503`, never
"keeps none".
**Docs:** `content/docs/automation/connectors.mdx` names `redirectUri`
on the status read (the one place a client, the Console included, reads
it from) and says a reconnect keeps the stored client unless it brings a
new one.
## Why
An administrator registering a vendor OAuth client for a `clientSource:
'installer'` instance needs this deployment's redirect URI before any
client exists, and the Console could only guess it from
`window.location`. And the Console never reads a stored secret back, so
a reconnect had to re-enter it (card Ask 2, ruling B of #28702).
## Tests
- `connector-connection-door.test.ts` (service-automation), 34/34:
- new: the read answers the option, then `OS_AUTH_URL` over
`OS_BASE_URL`; no base (and a scheme-less base) leaves the key out while
the read still answers and a begin is still `503`; the URI shown equals
the `redirect_uri` a connect names; a connected read still carries it.
- new: a `client_credentials` instance's read is refused `400
VALIDATION_ERROR` and carries no redirect URI.
- new: a reconnect without a client after a connect with one names the
stored `client_id`, exchanges as it (HTTP Basic), re-stores the same
encrypted client at revision 2; another organization connecting first,
and the same organization after a revoke, are still refused `400`.
- new: a kept public client is reused as it is (id in the form, no
secret).
- updated: the first test's not-connected read now also expects
`redirectUri` (the boot passes the plugin option). The existing tests
pinning the old refusal ("a connect without a client is refused 400")
and replacement ("a reconnect replaces it") are unchanged and still
pass: Ask 2 changes neither.
- `connector-installer-oauth-client.dogfood.test.ts` (real HTTP door on
the showcase), 4/4: SERVED asserts the read's `redirectUri` is the
deployment origin plus the callback path; CONNECTED asserts the begin's
`redirect_uri` equals the read's `redirectUri`, and that a `POST` with
no body after the connect is `200` naming the stored `client_id`, never
the secret.
## Acceptance notes
- The `ConnectorConnectionState.redirectUri` TSDoc in
`packages/spec/src/contracts/automation-service.ts` still says "Declared
ahead of the service that answers it: until the automation service sets
it, no answer carries it." That sentence is false once this lands; spec
edits are `domain:spec`'s, so it is left to that lane (also the matching
bullet in the unreleased `.changeset/29574-connector-redirect-uri.md`).
- `redirectUri` is set on the read leg only, as the TSDoc names; the
revoke and callback answers do not carry it.
- With no data engine, a client-less begin on an installer instance now
answers the store's `503` instead of the `400` (it can no longer tell
whether a client is kept).
- The Console does not show the URI yet: #28869 item 2 is the consumer.
## Verification
<details><summary>Local runs at d7e2726</summary>
- `pnpm --filter @objectstack/service-automation typecheck`: exit 0.
- `pnpm --filter @objectstack/service-automation test`: Test Files 262
passed, Tests 3398 passed, lock VERDICT command-exit 0.
- Single dogfood file under the lock after building the dogfood closure:
Test Files 1 passed, Tests 4 passed.
- Ablation (via `scripts/ablation-replace.mjs`, src-resolved relative
import, restored by the tool, `git diff HEAD` empty after): read leg
`return state;` turns 2 tests red (the first test and the precedence
test); `if (kept) return kept;` replaced by `void kept;` turns both
reuse tests red.
- `dispatch-gates --commands`: 90 families, all exit 0 (three first
answered exit 3 PREREQUISITE and were re-run green after `git fetch
origin 621a487` and building client-react, studio, embedder-openai,
knowledge-memory, knowledge-ragflow, service-cluster-redis and
service-knowledge); `--ran` reconciliation: 90 derived, 90 run, 0
NOT-MEASURED, 0 UNRUN.
- `pnpm lint`: exit 0.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
Card thread session_01JQwxbXC31EnuxG3osEW4Fc, dispatched by
`domain:server#1` · os-warren.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbXC31EnuxG3osEW4Fc
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbXC31EnuxG3osEW4Fc)_
<!-- signed: domain:server#1 · os-warren ·
session_01JQwxbXsK9j25sd7eVwW96G -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(automation): the connector connection read answers the deployment's OAuth `redirectUri`, and a client-less reconnect of an installer-client instance reuses the kept client
6
+
7
+
Clause-②: yes (widening)
8
+
Metadata-protocol: none
9
+
10
+
Two answers an administrator connecting an `oauth2``authorization_code` instance with `clientSource: 'installer'` needs from the connector connection door (`/api/v1/automation/connectors/NAME/connection`):
11
+
12
+
-**`GET` answers `redirectUri`.** The connection read (`getConnectorConnection`) now sets `ConnectorConnectionState.redirectUri`, the redirect URI the administrator registers with the vendor before any client exists, whatever the connection's `status`. It is the same derivation a begun connect uses: the automation plugin's `connectorOAuthRedirectUri` option, else `/api/v1/automation/connectors/oauth/callback` under `OS_AUTH_URL`, else under `OS_BASE_URL`. With no base to derive it from, the key is absent and the read still answers its status; it is never guessed from the request. A begun connect in that case is refused `503` as before.
13
+
-**A reconnect without a client keeps the stored one.** A `POST` that names no client, on an instance that takes the installer's client, now reuses the client the organization's connection keeps (the secret is never read back, so the Console cannot resend it). With no kept client (a first connect, or after a revoke) it is refused `400 VALIDATION_ERROR` as before, and a `POST` that names a client replaces the kept one as before.
it('the read answers the redirect URI to register with the vendor: the option, else OS_AUTH_URL, else OS_BASE_URL; with no base it is absent, never a refusal',async()=>{
expect(err).toMatchObject({status: 400,code: 'VALIDATION_ERROR',message: expect.stringContaining('does not use the oauth2 authorization_code grant')});
it("a reconnect without a client reuses the one the organization's connection keeps; another organization's, or a revoked one, is never reused",async()=>{
0 commit comments