Skip to content

Commit 380a107

Browse files
docs(automation): connectors page describes the served rest/openapi credential handle and app-credential exchanges (#28419)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoHgewSpchhjBvPu7yhxUUJx)_ Part of #26702 Clause-②: no Metadata-protocol: none Skills-docs: the skills half (skills/objectstack-automation/references/integration-recipes.md) is #28299, the skills lane's card; this PR touches no skills/** file. ## 维护者速读 - 改了什么:只改文档 `content/docs/automation/connectors.mdx`,把连接器页面里已经过时的说法改正:`rest` 和 `openapi` 现在会使用凭证句柄,飞书、钉钉、企业微信的应用凭证换取也已经在运行;只有 `mcp` 仍列为暂不支持。 - 为什么改:#28399 合入后,页面仍写着「应用凭证的厂商换取尚未运行」「内置 rest / openapi 不使用凭证句柄」,这两句现在是错的,会误导作者把实例声明成静态凭证。 - 风险与回滚:纯文档,不发布任何包,不影响运行时;回滚即还原本 PR。 - 先看:「How the server serves them」开头新增的应用凭证段落和三家厂商的表格,以及下方「What is not served yet」段落。 - 两分钟验证:`pnpm check:doc-authoring && pnpm check:doc-anchors`,预期退出码 0。 - 需要你做的:无,席位审阅并落地(本 PR 需要 `skip-changeset` 标签)。 ## What changes `content/docs/automation/connectors.mdx`, three places, all in the grant-bearing auth section and the failure table: - **"How the server serves them"** now opens with what serves both grant-bearing kinds: the credential handle the built-in `rest` and `openapi` providers ask on every request (`registerConnectorProvider(…, { credentialHandle: true })`), and what an `openapi` document's `security` decides for such an instance (`security: []` is anonymous; an `apiKey` scheme's parameter is filled by the platform and left out of the input schema). - **New `app-credential` paragraph and table**: the fixed per-vendor exchange (Feishu, DingTalk v1.0, WeCom) and where each token rides on a call; the token belongs to the app and nothing is connected; it is held in memory only and obtained afresh inside the 30 s skew or once on a `401`; one exchange is shared by concurrent requests; WeCom's in-answer `errcode` `40014` / `42001` obtains the token afresh once and repeats the call once, and a second such answer fails it; no redirect is followed; no answer / `429` / `5xx` is `CONNECTOR_UPSTREAM_UNAVAILABLE`, a refused app id or secret is a `503` naming the instance and at most the vendor's error code. The existing `oauth2` text follows unchanged, now introduced as "An `oauth2` instance's access token is …". - **"What is not served yet is held degraded"** no longer lists `app-credential` or the `rest` / `openapi` providers. It now lists a grant-bearing instance on a provider that does not ask the handle (the built-in `mcp` provider, or a plugin provider registered without the option), the `mcp` instance that leaves its endpoints to discovery, and the non-https `oauth2` endpoint rule, as before. - **Failure-modes table**: the grant-bearing row now reads "on a provider that does not ask the credential handle (the built-in `mcp` provider, for now)". Untouched by design: the `(404)` hunk near line 404 that draft #28346 edits, and every `skills/**` file. Every fact was read from the merged code: `packages/services/service-automation/src/oauth2-grant/app-credential.ts` (exchanges, `present`, skew via `BORROWED_CREDENTIAL_SKEW_MS = 30_000`, `retryOnVendorTokenRefusal`), `plugin.ts` (`credentialHandleNotAppliedReason`), `connector-openapi/src/openapi-connector.ts` (the `security` rules), and `connector-credential-source.ts` (datasource lending still `422`). ## Tests None: a docs-only change. The docs gates below judge it. ## Verification - `dispatch-gates --commands`: 43 families derived for this one path; all exit 0. `check:docs` and `check:skill-examples` exited 1 / 3 at first because `packages/spec/json-schema` was older than `src` in this worktree (a build prerequisite, not a finding); after `pnpm --filter @objectstack/spec build` both exit 0. - `--ran` reconcile: 43 derived, 43 run, 0 NOT-MEASURED, 0 unrun. - No changeset: nothing published changes, so the PR carries `skip-changeset`. ## Acceptance notes - `mcp` stays listed as not served: its half of ADR-0143 S2 is a separate PR that waits on the discovery-organization ruling. - The `oauth2` revoke sentence still says the discovery that would supply a revocation endpoint is ADR-0143 S2; that remains true until the `mcp` half lands, so it is left as is. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` is not touched.) - [x] 2. Every name I added uses the casing for its kind. (No names added.) - [x] 3. No formatter ran; the docs gates pass. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset: this one publishes nothing (`skip-changeset`); both declaration lines are filled in above. - [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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L9qibwa6RkD7A2gK6rYRNT Co-authored-by: Claude <noreply@anthropic.com>
1 parent df34d37 commit 380a107

1 file changed

Lines changed: 40 additions & 7 deletions

File tree

‎content/docs/automation/connectors.mdx‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,40 @@ What the publish door checks, each refusal located at its key under `auth`:
470470
- **`app-credential`** takes `vendor` (`feishu`, `dingtalk` or `wecom`), `appId`
471471
and a required `credentialRef`. An inline `appSecret` is refused by name.
472472

473-
**How the server serves them.** An `oauth2` instance is served through a
474-
**credential handle** the provider asks on every request: the access token is
473+
**How the server serves them.** Both kinds are served through a
474+
**credential handle** the provider asks on every request. The built-in `rest`
475+
and `openapi` providers ask it; a provider says it does by registering with
476+
`registerConnectorProvider(…, { credentialHandle: true })`. For an `openapi`
477+
instance the document's `security` decides only where the token is *not*
478+
sent: an operation whose effective `security` is `[]` is anonymous and never
479+
carries it, and a header or query parameter an `apiKey` scheme names is the
480+
platform's to fill, so it is left out of the action's input schema, as
481+
`Authorization` is.
482+
483+
An **`app-credential`** instance runs its vendor's exchange from the `appId`
484+
and the app secret its `credentialRef` names. The exchange is fixed per
485+
`vendor`, and nothing about it is authored:
486+
487+
| `vendor` | Token request | Where the token rides on a call |
488+
|:---|:---|:---|
489+
| `feishu` | `POST https://accounts.feishu.cn/oauth/v3/token` (`client_credentials`) | `Authorization: Bearer` |
490+
| `dingtalk` | `POST https://api.dingtalk.com/v1.0/oauth2/accessToken` | header `x-acs-dingtalk-access-token` |
491+
| `wecom` | `GET https://qyapi.weixin.qq.com/cgi-bin/gettoken` | query parameter `access_token` |
492+
493+
The token belongs to the app, so a run's organization selects nothing, and
494+
there is nothing to connect. It is kept in memory only, never stored (the app
495+
secret re-derives it at any time), and obtained afresh when it is within 30
496+
seconds of its expiry, or once when the upstream answers `401`. Concurrent
497+
requests share one exchange. WeCom can invalidate a token before it expires
498+
and says so inside an ordinary answer (`errcode` `40014` or `42001`) rather
499+
than with a `401`: such an answer obtains the token afresh once and repeats
500+
the call once, and a second such answer fails the call. No exchange follows a
501+
redirect. An exchange that gets no answer, a `429` or a `5xx` fails the
502+
dispatch `CONNECTOR_UPSTREAM_UNAVAILABLE`, retried on the existing backoff; a
503+
vendor that refuses the app id or secret fails it with `503`, naming the
504+
instance and at most the vendor's error code, never the vendor's text.
505+
506+
An **`oauth2`** instance's access token is
475507
obtained from `tokenUrl` (the client authenticating with HTTP Basic, or by
476508
`client_id` alone as a public client), cached in memory only, refreshed when
477509
it is within 30 seconds of its expiry, and refreshed once more, with one
@@ -527,10 +559,11 @@ a rotating refresh token is never spent twice.
527559
disconnect is a response to a leak.
528560

529561
What is not served yet is held **degraded** with a reason that names it, and
530-
never called without its credential: `app-credential` (its vendor exchanges
531-
are not run yet), an `oauth2` instance on a provider that does not yet ask the
532-
credential handle (the built-in `rest`, `openapi` and `mcp` providers do not
533-
yet), an `mcp` instance that left its endpoints to discovery, and an `oauth2`
562+
never called without its credential: an `oauth2` or `app-credential`
563+
instance on a provider that does not ask the credential handle (the built-in
564+
`mcp` provider does not yet, nor does a plugin's provider registered without
565+
`{ credentialHandle: true }`), an `mcp` instance that left its endpoints to
566+
discovery, and an `oauth2`
534567
instance whose `tokenUrl` or `authorizationUrl` is not `https://` (plain
535568
`http://` is accepted only to `localhost`, a `127.0.0.0/8` address or
536569
`[::1]` — not a `*.localhost` name, which the system resolver sends to DNS —
@@ -663,7 +696,7 @@ retry):
663696
|:---|:---|
664697
| `provider` names no installed factory | **Hard boot error** naming the entry, the provider, and the plugin that supplies it — add the executor to `plugins:` |
665698
| Invalid `providerConfig` (missing `baseUrl` / `spec` / `transport`, wrong shape) | **Hard boot error** from the provider factory |
666-
| `auth` is a grant-bearing kind (`oauth2`, `app-credential`) this server does not serve yet (`app-credential`; `oauth2` on a provider that does not ask the credential handle) | **Not fatal**: the instance registers **degraded**, naming the kind, and the upstream is never called without its credential; a datasource borrowing it is refused with `422` |
699+
| `auth` is a grant-bearing kind (`oauth2`, `app-credential`) on a provider that does not ask the credential handle (the built-in `mcp` provider, for now) | **Not fatal**: the instance registers **degraded**, naming the kind, and the upstream is never called without its credential; a datasource borrowing it is refused with `422` |
667700
| An `authorization_code` instance nobody connected, or whose grant the vendor refused | **Not fatal**: the instance registers **degraded**, "not connected" or "reconnect required" — connect it (`POST …/connectors/:name/connection`), and it materializes without a restart |
668701
| The token endpoint is unreachable or answers `5xx` / `429` | **Not fatal**: the dispatch fails `CONNECTOR_UPSTREAM_UNAVAILABLE` and is retried on the existing backoff; the connection stays connected |
669702
| `auth` is `signed` on a provider that does not say it applies the request signer | **Hard boot error** naming the instance, the scheme and the provider: its calls would go out unsigned — declare it through `rest` or `openapi` |

0 commit comments

Comments
 (0)