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
24 changes: 24 additions & 0 deletions .changeset/28137-connector-grant-bearing-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@objectstack/spec': minor
'@objectstack/types': patch
'@objectstack/service-automation': patch
---

feat(spec)!: `oauth2` and `app-credential` join declarative connector-instance auth, and the runtime `oauth2` variant is retired (ADR-0143 S0, #28137)

Clause-②: yes (narrowing)
Metadata-protocol: narrowed

<!-- adr-0087: registered connector-auth-config-oauth2-retired -->

An upstream that takes no static credential (an OAuth 2.0 API, a Feishu, DingTalk or WeCom app) had no open-tier way to be declared: the instance `auth` stopped at four static kinds, and the runtime `authentication` shape had an `oauth2` variant that held a client secret and a refresh token inline and that nothing ran. ADR-0143 opens the grant-bearing kinds on the instance, with the secret as a reference.

- **`@objectstack/spec`**: `ConnectorInstanceAuthSchema` gains `oauth2` (D2.1), one strict shape per `grant` (`authorization_code` with PKCE always on, `client_credentials` for a confidential client), and `app-credential` (D2.2, `vendor` `feishu` / `dingtalk` / `wecom`, `appId`, a required `credentialRef`). The publish door refuses a missing `tokenUrl`, `clientId` or `authorizationUrl` at its key under `auth`, except on `provider: 'mcp'` over `http`, where the MCP authorization flow discovers them. An inline `clientSecret`, `refreshToken`, `accessToken` or `appSecret` is refused by name with the `credentialRef` prescription. `signed` is not a member (#26710). `ConnectorOAuth2Schema` is removed from `ConnectorAuthConfigSchema` (D7), and `ResolvedConnectorAuth` stays the four static kinds. The per-dispatch handle types (`ConnectorCredentialHandle`, `BorrowedConnectorCredential`, `BorrowedConnectorAuth`, `ConnectorCredentialRequest`) move into `@objectstack/spec/integration`, and `ConnectorProviderContext` gains an optional `credentialHandle` (D4). The D3 entry `connector-auth-config-oauth2-retired` names the removal for `os migrate meta`.
- **`@objectstack/types`**: re-exports the four handle types from `@objectstack/spec/integration`; the same names, structurally unchanged.
- **`@objectstack/service-automation`**: this server does not run the per-dispatch token resolution yet (ADR-0143 S1). A grant-bearing instance is held **degraded** with a reason that names its kind, and its upstream is never called without the credential. A datasource that borrows it through `external.connector` is refused with `422`, naming the kind.

**BREAKING** for a connector that writes `authentication: { type: 'oauth2', ... }`.

- **Before.** `authentication: { type: 'oauth2', authorizationUrl, tokenUrl, clientId, clientSecret, refreshToken }` was accepted, with the secret and the refresh token inline, and no runtime obtained or refreshed a token from it.
- **Now.** `ConnectorAuthConfigSchema` refuses it at `type`, with a prescription naming ADR-0143 D7 and the replacement. No first-party source declares it.
- **Fix.** Declare the grant on a declarative instance's `auth`, with the secret as a reference: `auth: { type: 'oauth2', grant: 'authorization_code', authorizationUrl, tokenUrl, scopes, clientId, credentialRef: 'VENDOR_CLIENT_SECRET' }`, or `grant: 'client_credentials'` without `authorizationUrl`. Code that registers a connector with a token it already holds passes `{ type: 'bearer', token }`.
77 changes: 64 additions & 13 deletions content/docs/automation/connectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -394,22 +394,67 @@ auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' },
— fails validation, and `auth: { type: 'bearer', token: … }` is not even a
legal shape (the declarative union has no `token` field to put a secret in).

### Where is `oauth2`?

Deliberately absent from declarative instances — **not** an oversight to work
around. The runtime auth shape has five variants (`none`, `bearer`,
`api-key`, `basic`, `oauth2`); the declarative shape stops at four. Static
credentials resolved from env/config are the open tier; the OAuth2
authorization-code/refresh **lifecycle** — token acquisition, rotation,
per-tenant connections — is the enterprise tier (ADR-0015). If your upstream
needs OAuth2, use an enterprise credential resolver or register the connector
from a plugin that manages its own tokens; don't try to emulate it by stuffing
a short-lived access token behind a `bearer` ref.
### Grant-bearing kinds: `oauth2` and `app-credential`

Some upstreams take no static credential: the token is obtained from a token
endpoint and refreshed as it expires. Two more `auth` kinds declare that, in
the open tier (ADR-0143). They are **grant-bearing**: the metadata carries the
endpoints, the client id and a `credentialRef` for the client or app secret,
never a token. Access and refresh tokens are runtime state and have no key here.

```ts
// OAuth 2.0 — a user grants access once (`authorization_code`, PKCE S256 always on):
auth: {
type: 'oauth2',
grant: 'authorization_code',
authorizationUrl: 'https://vendor.example.com/oauth/authorize',
tokenUrl: 'https://vendor.example.com/oauth/token',
scopes: ['crm.read'],
clientId: 'objectstack-prod',
credentialRef: 'VENDOR_CLIENT_SECRET', // omit for a public client
}

// OAuth 2.0 — server to server (`client_credentials`, a confidential client):
auth: {
type: 'oauth2',
grant: 'client_credentials',
tokenUrl: 'https://vendor.example.com/oauth/token',
resource: 'https://api.vendor.example.com',
clientId: 'objectstack-prod',
credentialRef: 'VENDOR_CLIENT_SECRET', // required for this grant
}

// A vendor app credential (Feishu, DingTalk, WeCom): app id + app secret → tenant token
auth: { type: 'app-credential', vendor: 'feishu', appId: 'cli_a1b2', credentialRef: 'FEISHU_APP_SECRET' }
```

What the publish door checks, each refusal located at its key under `auth`:

- **`oauth2`** needs `tokenUrl` and `clientId`, plus `authorizationUrl` on
`authorization_code`. The one exception is `provider: 'mcp'` over an `http`
transport, which may omit the endpoints and the client id: the MCP
authorization flow discovers the server's metadata and can register the
client. `client_credentials` has no authorization step, so
`authorizationUrl` is refused there, and it requires `credentialRef`.
A `credentialRef` without its `clientId` is refused. An inline
`clientSecret`, `refreshToken` or `accessToken` is refused by name, pointing
at `credentialRef`. There is no PKCE switch.
- **`app-credential`** takes `vendor` (`feishu`, `dingtalk` or `wecom`), `appId`
and a required `credentialRef`. An inline `appSecret` is refused by name.

A server that does not yet run the per-dispatch token resolution holds such an
instance **degraded**, with a reason that names the grant-bearing kind, and
never calls the upstream without its credential. A datasource that borrows
from it (`external.connector`) is refused with `422`. Until the server serves
them, declare a static kind (`bearer`, `api-key` or `basic`) to dispatch now;
don't emulate a grant by putting a short-lived access token behind a `bearer`
ref.

### The runtime shape: `authentication`

The `authentication` field is the **runtime** auth config — it carries the
resolved secret *inline* (`{ type: 'bearer', token }`), because it is supplied
The `authentication` field is the **runtime** auth config, with four static
variants (`none`, `bearer`, `api-key`, `basic`). It carries the resolved
secret *inline* (`{ type: 'bearer', token }`), because it is supplied
by **host code** (a plugin calling `engine.registerConnector`, or the
executors' hand-wired instance options reading `process.env`). That is a
different trust anchor than metadata: code review guards what enters it, and
Expand All @@ -418,6 +463,11 @@ leave `authentication` alone — provider-bound instances must use `auth`, and
catalog descriptors documenting a planned integration must still never carry a
live secret value.

The runtime shape's `oauth2` variant was removed in `@objectstack/spec` 18
(ADR-0143 D7): it held a client secret and a refresh token inline and nothing
ran its lifecycle. `authentication: { type: 'oauth2' }` is refused with that
prescription; declare the grant on an instance's `auth` instead, as above.

## Dispatching from a flow

A materialized instance is dispatched exactly like any other connector — by
Expand Down Expand Up @@ -449,6 +499,7 @@ retry):
|:---|:---|
| `provider` names no installed factory | **Hard boot error** naming the entry, the provider, and the plugin that supplies it — add the executor to `plugins:` |
| Invalid `providerConfig` (missing `baseUrl` / `spec` / `transport`, wrong shape) | **Hard boot error** from the provider factory |
| `auth` is a grant-bearing kind (`oauth2`, `app-credential`) on a server that does not yet run its per-dispatch resolution | **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` |
| `credentialRef` resolves to nothing | **Not fatal**: the instance registers **degraded**, naming the reference — set it (`PUT …/credential-refs/:ref`), and it materializes without a restart |
| The upstream refuses the credential (`401` / `403`) | **Not fatal**: the instance registers **degraded**. A stored credential is marked `rejected` and waits until it is set again (or a restart); an environment or vault credential is retried on the backoff |
| Instance `name` collides with a plugin-registered connector | **Hard boot error** — there is no silent precedence between the two worlds |
Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Protocol reference — every schema by module
navTitle: Protocol Reference
description: Every schema published by @objectstack/spec — 1549 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1555 schemas across 14 protocol modules
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand All @@ -25,7 +25,7 @@ counts are sums of the rows they head. Regenerate with
| [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
| [Integration Protocol](/docs/references/integration) | 1 | 12 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
| [Integration Protocol](/docs/references/integration) | 1 | 18 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
| [Kernel Protocol](/docs/references/kernel) | 29 | 151 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
| [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. |
| [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
Expand All @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **202** | **1549** | 14 protocol modules |
| **Total** | **202** | **1555** | 14 protocol modules |

---

Expand Down Expand Up @@ -190,13 +190,13 @@ Users and accounts, organizations, positions, SCIM provisioning.

## Integration Protocol

**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 12 schemas**
**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 18 schemas**

The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances.

| File | Schemas |
| :--- | :--- |
| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorRetryStrategy`, `ConnectorType`, `DeclarativeConnectorEntry`, `RetryConfig` |
| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorAppCredentialVendor`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAppCredentialAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorInstanceOAuth2Auth`, `ConnectorInstanceOAuth2AuthorizationCodeAuth`, `ConnectorInstanceOAuth2ClientCredentialsAuth`, `ConnectorOAuth2Grant`, `ConnectorRetryStrategy`, `ConnectorType`, `DeclarativeConnectorEntry`, `RetryConfig` |

---

Expand Down
Loading
Loading