Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/26702-connector-providers-oauth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/connector-rest': minor
'@objectstack/connector-openapi': minor
'@objectstack/service-automation': minor
---

feat(connectors): the built-in `rest` and `openapi` providers serve `oauth2` and `app-credential` instances through the per-dispatch credential handle, and the Feishu, DingTalk and WeCom app-credential exchanges run (ADR-0143 S2, part of #26702)

Clause-②: yes (widening)
Metadata-protocol: semantic

ADR-0143 S1 (#28317) served an `oauth2` instance only on a provider that asks the credential handle, and none of the built-in providers did; every `app-credential` instance was held degraded. Now:

- **`rest` and `openapi` ask the handle.** Both plugins register their provider with `{ credentialHandle: true }`. A grant-bearing instance on either one is served: every request asks the handle for the run's organization, sends the token where the handle places it (an `Authorization: Bearer` header, or a vendor's header or query parameter), refreshes once and retries once on an upstream `401`, and a second `401` is the dispatch's `503` naming the instance. A thrown transport error has the token removed from its message first. A handle beside a static credential or a request signer is refused when the instance is built: a request is authenticated one way.
- **`openapi` reads the document's `security`.** An operation whose effective `security` is `[]` is anonymous: the handle is not asked and no token is sent. On a grant-bearing instance, a parameter that an `apiKey` scheme the operation requires names is filled by the platform and is no longer an input of the action. An `oauth2` scheme's flows stay documentation: the instance's own `auth` names the token endpoint.
- **`app-credential` exchanges run** (`@objectstack/service-automation`), each as its vendor documents it: Feishu's `client_credentials` token at `accounts.feishu.cn` (sent as a bearer token), DingTalk's v1.0 `accessToken` API (sent in `x-acs-dingtalk-access-token`; the legacy `oapi` APIs that take `access_token` in the query are not served), and WeCom's `gettoken` (sent as the `access_token` query parameter). The token is held in memory only, refreshed inside the 30 second skew margin, and concurrent asks share one exchange. The token request never follows a redirect. No answer, `429` or `5xx` is `CONNECTOR_UPSTREAM_UNAVAILABLE`; a refusal is a `503` naming the vendor's error code only, never the secret.
- **WeCom early invalidation.** WeCom answers an invalidated token inside an ordinary answer (`errcode` `40014` or `42001`). A WeCom instance's actions treat that as the `401` rule does: the token is obtained afresh once and the call is repeated once; a second refusal is the `503`.
- **The guard.** An `app-credential` instance is served on a provider that asks the credential handle and held degraded, naming the provider, on one that does not. `mcp` does not ask the handle yet, so an `oauth2` or `app-credential` instance on `mcp` stays degraded.
3 changes: 2 additions & 1 deletion packages/connectors/connector-openapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
},
"dependencies": {
"@objectstack/core": "workspace:*",
"@objectstack/spec": "workspace:*"
"@objectstack/spec": "workspace:*",
"@objectstack/types": "workspace:*"
},
"devDependencies": {
"@objectstack/service-automation": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ export interface ConnectorRegistrySurface {
>,
): void;
unregisterConnector(name: string): void;
registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;
/**
* `options.credentialHandle: true` says the factory asks
* `ConnectorProviderContext.credentialHandle` on every request, so the host
* serves grant-bearing instances (`oauth2`, `app-credential`) through it
* (ADR-0143 D4). An older host ignores the third argument and keeps
* holding those instances degraded.
*/
registerConnectorProvider(
providerKey: string,
factory: ConnectorProviderFactory,
options?: { readonly credentialHandle?: boolean },
): void;
}

/**
Expand Down Expand Up @@ -81,6 +92,8 @@ export class ConnectorOpenApiPlugin implements Plugin {
automation.registerConnectorProvider(
OPENAPI_PROVIDER_KEY,
createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl }),
// ADR-0143 D4 — the transport asks the credential handle per request.
{ credentialHandle: true },
);
ctx.logger.info("ConnectorOpenApiPlugin: registered 'openapi' connector provider");
}
Expand Down
8 changes: 4 additions & 4 deletions packages/connectors/connector-openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
*
* Generate an ObjectStack {@link Connector} from a declarative OpenAPI 3.x
* document (ADR-0023). One operation becomes one connector action; a single
* generic handler drives a self-contained static-auth HTTP transport (mirroring
* generic handler drives a self-contained HTTP transport (mirroring
* `@objectstack/connector-rest`). The generated connector is an ordinary
* `type: 'api'` connector — registered via `engine.registerConnector` with no
* new engine surface.
*
* Open-source scope: static auth only (`none` / `api-key` / `basic` / `bearer`),
* credentials supplied by the caller. Managed OAuth2, credential vaulting, and
* per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).
* Static auth (`none` / `api-key` / `basic` / `bearer`), a request signer
* (ADR-0143 D2.3), or a per-dispatch credential handle for the grant-bearing
* kinds `oauth2` and `app-credential` (ADR-0143 D4, open tier per D1).
*/

export {
Expand Down
Loading
Loading