Repository navigation
feat(connectors): rest and openapi serve oauth2 and app-credential instances through the credential handle; Feishu, DingTalk and WeCom exchanges (ADR-0143 S2) - #28399
Conversation
…xchanges and WeCom early invalidation Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwa6RkD7A2gK6rYRNT
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwa6RkD7A2gK6rYRNT
…openapi suites Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwa6RkD7A2gK6rYRNT
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwa6RkD7A2gK6rYRNT
📓 Docs Drift CheckThis PR changes 3 package(s): 30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 24fe2545d3396597f706cde6d07e5a45b92f497e && git checkout 24fe2545d3396597f706cde6d07e5a45b92f497e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 125ad062d2eaf49687a89489766a284ba60aa618 be639274b21ed3515272bbc67c3dc9cddaa58cbf && git checkout -B drift-repro 125ad062d2eaf49687a89489766a284ba60aa618 && git merge --no-ff be639274b21ed3515272bbc67c3dc9cddaa58cbf
node scripts/docs-audit/affected-docs.mjs --json 125ad062d2eaf49687a89489766a284ba60aa618
|
|
domain:server#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:server#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #26702 (the slice plan ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The two built-in HTTP providers consume the handle through the one shared call rule with the organization of the run, authentication stays one-way, the OpenAPI document only withholds and fills, the three vendor exchanges are implemented as documented with the secret never carried into a refusal, the app-credential handle and the WeCom wrap are D4's refresh-once rule and nothing more, the materializer serves on opted-in providers and degrades by name elsewhere, the pins hold under sixteen ablations, and every latest check on this head is green or skipped. Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-tesla · session_01L9qibz3ZUbZ8Qcn1rDw9yc 维护者速读
席位意见:契约复审 PASS(6543092852),16 处消融全部先红后恢复;文档与技能文本的过期由席位另立后续卡(skills/** 属 Tier H)。可以直接落地。 席位核对ACCEPT · seat
|
Requested by Tesla · project thread
Part of #26702
Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs: follow-up owed — content/docs/automation/connectors.mdx, skills/objectstack-automation/references/integration-recipes.md
维护者速读
rest和openapi连接器现在能真正调用需要 OAuth 的外部系统了;飞书、钉钉、企业微信的「应用凭证」实例也不再被挂起,平台会自己换取并续期访问令牌。oauth2或app-credential的实例只能降级、无法调用。本 PR 让业务流程可以直接调用这些系统。oauth2/app-credential的实例(原来都是降级状态),静态凭证(bearer / api-key / basic)的实例行为不变;回滚即还原本 PR,这些实例回到降级。packages/services/service-automation/src/oauth2-grant/app-credential.ts(三家厂商的换取方式与企业微信令牌提前失效的处理)、packages/connectors/connector-rest/src/rest-connector.ts(每次请求取令牌、401 刷新一次)、packages/connectors/connector-openapi/src/openapi-connector.ts(读取文档的security)。pnpm --filter @objectstack/connector-rest exec vitest run src/rest-provider-credential-handle.test.ts,预期 10 个用例全部通过,其中包括企业微信令牌提前失效后自动重新换取并重试一次的端到端用例。What changes
ADR-0143 S2, first half: the providers slice for
restandopenapi, and the D2.2app-credentialexchanges. S1 (#28317) servedoauth2only on a provider registered with{ credentialHandle: true }, and none of the built-ins was; everyapp-credentialinstance was held degraded. Themcpprovider is the second half of S2 and comes in its own pull request (see Acceptance notes).@objectstack/connector-rest,@objectstack/connector-openapi{ credentialHandle: true }, and both factories passctx.credentialHandleto the connector.callWithBorrowedCredentialin the name of the run's organization (ctx.automation.tenantId): the handle is asked on each request, an upstream401refreshes once and retries once, and a second401is the seam's503naming the instance. The token goes where the handle says:Authorization: Bearer, a vendor header, or a query parameter.withoutBorrowedCredential) before it propagates.openapireadssecurity(document level and per operation):securityis[]is anonymous: the handle is not asked and no token is sent.apiKeyscheme names is filled by the platform and is dropped from the action's input schema.oauth2scheme'sflowsstay documentation: the instance's ownauthnames the token endpoint. The OpenAPI types gainsecurity,flows,bearerFormat,openIdConnectUrlandmutualTLS.@objectstack/types(the seam's helpers). Their vitest configs alias it to source, ascheck:test-source-aliasrequires.@objectstack/service-automationoauth2-grant/app-credential.tsis the shared transport for the D2.2 exchanges, one entry per vendor, each as its docs describe it:client_credentialsataccounts.feishu.cn, form-encoded; sent as a bearer token.oauth2/accessTokenAPI with a JSON body; sent inx-acs-dingtalk-access-token.cgi-bin/gettoken; sent as theaccess_tokenquery parameter.createAppCredentialHandleimplementsConnectorCredentialHandlefor an instance: the token is held in memory only, refreshed inside the 30 s skew margin, and concurrent asks and refreshes share one exchange. The token request never follows a redirect and has a timeout.429or5xxisCONNECTOR_UPSTREAM_UNAVAILABLE, with nocause(a transport error may carry the request URL). A refusal, or a2xxwithout a token, is a503naming the vendor's error code only, never the vendor's text or the secret.retryOnVendorTokenRefusal: WeCom answers an early-invalidated token inside an ordinary answer (errcode40014/42001), not with a401. A WeCom instance's handlers treat it as the401rule does: refresh once for the run's organization, repeat the call once, and a second refusal is the503. Feishu and DingTalk answer401, so the provider's rule covers them and their handlers are not wrapped.plugin.ts, the materializer guard: anapp-credentialinstance is served on a provider that asks the credential handle (with no staticauthin the provider context, and the app secret never in it) and held degraded, naming the provider, on one that does not. One reason builder now serves both grant-bearing kinds.Why
ADR-0143 opens outbound connector auth in the open tier. After S1 the token machinery existed but no built-in provider used it, so an
oauth2orapp-credentialinstance could be declared and never dispatched. The ADR's acceptance needs "a Feishu (or DingTalk) instance declared withapp-credentialdispatches throughopenapiorrest, and its token renews without a restart"; this PR covers that, and the WeCom early-invalidation unit case the ADR names.Tests
connector-rest/src/rest-provider-credential-handle.test.ts(10): handle asked per request per organization, no-organization ask, 401 refresh-once, second 401 → 503, a handle refusal sends nothing, header vs query placement, transport-error scrub, exclusivity, the plugin opt-in, and a WeComapp-credentialinstance end to end throughAutomationServicePlugin+ConnectorRestPlugin(early invalidation → one re-exchange, one retry).connector-openapi/src/openapi-provider-credential-handle.test.ts(7): per-request ask and bearer, 401 rules, anonymous operation, scheme-filled parameter dropped from the input, the no-handle control, exclusivity with a signer, the plugin opt-in.service-automation/src/oauth2-grant/app-credential.test.ts(19): each vendor's request and answer, refusals per vendor, a tokenless2xx, no answer /429/5xx, no redirect, cache + skew + single flight, refresh single flight, WeCom retry for both codes, second refusal, the control, unwrapped vendors.service-automation/src/connector-materialization.test.ts: the S0 "holds app-credential degraded" case is replaced by three: served on an opted-in provider with no static auth and the secret not in the context, held on a provider that does not opt in, held naming the reference when the secret does not resolve.Ablations — 16, each red and each restored (
scripts/ablation-replace.mjs, tree clean after each)callWithBorrowedCredential→ a get-only callways.length > 1→> 2anonymous = falsecontinueaccess_tokenback as an input)> 1→> 2credentialHandle: falsecredentialHandle: falseredirect: 'error'inflight ??=→inflight =app-credentialon any providerablation-dist-preflightabsent → exit 0, then present after restore)Verification
pnpm --filter @objectstack/connector-rest exec vitest run: 5 files, 43 passed.@objectstack/connector-openapi: 7 files, 89 passed.@objectstack/service-automationfull suite: 243 files, 3084 passed (on1838861a; later commits touch only the changeset, two vitest configs and two type casts in a test, after whichapp-credential.test.tsis 19/19 again).pnpm --filter <pkg> run typecheck(withcheck:test-typecheck): exit 0 for connector-rest, connector-openapi and service-automation.pnpm lintscoped to the touched packages: exit 0.dispatch-gates --commands: 73 families; all exit 0 after two fixes in this branch (check:test-source-aliasnamed the new@objectstack/typesimport → aliased;check:plugin-teardown-shape --self-testandcheck:dual-build-cjs-loadsexited 3 until the pinned commit was fetched and the whole tree built).--ranreconcile: 73 derived, 73 run, 0 NOT-MEASURED, 0 unrun.Acceptance notes
mcpprovider is not in this PR. S2 is split by provider (the order's size rule): this PR isrest+openapi+ theapp-credentialtransport, about 1,350 lines; themcpauthorization flow (protected-resource and authorization-server metadata, dynamic client registration, PKCE, the pre-registeredclientIdform) follows in a second PR. Until then anoauth2orapp-credentialinstance onmcpstays degraded, as today.api.dingtalk.com, token inx-acs-dingtalk-access-token). The legacyoapi.dingtalk.comAPIs, which take the token in the query, are not served by this exchange.401; WeCom's in-body codes40014/42001are handled by the wrapper. Other WeCom codes are returned to the flow as the upstream's answer.external.connector) still refuses grant-bearing kinds with422;def.authenticationstaysnonefor handle-authenticated instances; the connection door handlers andcapabilities.tsare untouched (security: a dedicated org-scoped manage_connector_connections capability for connecting OAuth2 connector instances (before ADR-0143 S4's Console flow) #28327).content/docs/automation/connectors.mdxand the integration-recipes skill reference still sayapp-credentialand the built-in providers are not served; they are outside this card's file surface and are declared owed above.Human Minimum
packages/specholds no runtime logic. (packages/specis not touched.)pnpm lintpasses.main, one pull request..changeset/26702-connector-providers-oauth.md), and both declaration lines are filled in here and in the changeset.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01L9qibwa6RkD7A2gK6rYRNT