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
36 changes: 36 additions & 0 deletions .changeset/28864-oauth2-installer-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
'@objectstack/spec': minor
---

feat(spec): an `oauth2` `authorization_code` connector instance can take its OAuth client from the administrator who installs it — `clientSource: 'installer'`

Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6566728470

A vendor accepts an OAuth client only at the callback URLs its owner registered, and the platform's callback is fixed per deployment. A `clientId` written into a connector definition is the publisher's client, so an app installed on a deployment its publisher never saw could not connect there. The `authorization_code` instance shape gains one optional key that says the client comes from the installer instead:

```ts
connectors: [{
name: 'esign',
label: 'E-Signature',
type: 'api',
provider: 'openapi',
providerConfig: { spec: 'https://api.vendor.example.com/openapi.json' },
auth: {
type: 'oauth2',
grant: 'authorization_code',
authorizationUrl: 'https://account.vendor.example.com/oauth/authorize',
tokenUrl: 'https://account.vendor.example.com/oauth/token',
scopes: ['signature'],
clientSource: 'installer',
},
}]
```

- **What it means.** The administrator of each deployment registers their own client with the vendor, against that deployment's callback, and enters its id and secret on the instance's connection. The definition names no client and no secret.
- **What it refuses.** A `clientId` beside `clientSource` is refused at publish, located at `auth.clientId`: the definition would name two clients. A `credentialRef` without its `clientId` is refused as before, and its message now names this form. An unknown value is refused at `auth.clientSource`. On `grant: 'client_credentials'` the key is refused with guidance, because that grant keeps no connection to enter a client on.
- **What it does not change.** The endpoints stay declared, or discovered on `provider: 'mcp'` over http: the installer owns the client, not the vendor's authorization server. An instance that names a pre-registered `clientId` without the key parses exactly as before. That form is kept for an app whose author registers the callback of every deployment that installs it, such as an organization's own app.
- **Not yet served.** The automation service does not read the key yet. The connection door that takes the installer's client, and the form that asks for it, land in follow-up changes. Until then an instance that declares it on a non-MCP provider is held degraded, with a reason naming the missing `clientId`, and is never called without a client. On `provider: 'mcp'` over http the service runs the MCP authorization flow as it does for an instance that leaves its client to dynamic registration. The key's reference row carries `[PLANNED — not enforced]` until then, and the liveness ledger's `connector/auth` row records it as `planned`.

ADR-0143 D2.1 carries the amendment (2026-11-02).
28 changes: 16 additions & 12 deletions content/docs/references/integration/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,11 @@ const result = ConnectorSchema.parse(data);
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required, except where the client comes from elsewhere: `provider: 'mcp'` over http may register one dynamically, and with `clientSource: 'installer'` the administrator enters it on the connection, so it is not named here. |
| **grant** | `'authorization_code'` | ✅ | An administrator connects once; the platform refreshes the grant per dispatch. |
| **authorizationUrl** | `string` | optional | Authorization endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client. Never an inline secret. |
| **clientSource** | `Enum<'installer'>` | optional | [PLANNED — not enforced] Where the OAuth client comes from. `'installer'`: the administrator of each deployment registers their own client with the vendor, against that deployment's callback, and enters its id and secret on the instance's connection, so the definition names no `clientId` and no `credentialRef`. Omit it to name a pre-registered `clientId` here, or to let `provider: 'mcp'` over http register a client. Not served yet: the automation service reads it once the server half of the ADR-0143 D2.1 amendment lands. Until then, off `provider: 'mcp'` over http, an instance that declares it is held degraded for a missing `clientId`; on `provider: 'mcp'` over http the provider registers a client dynamically instead of taking the installer's. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client, and with `clientSource: 'installer'`, where the administrator enters the secret on the connection. Never an inline secret. |

### Nested Shape: `Connector.auth[option 5][grant='client_credentials']`

Expand All @@ -280,7 +281,7 @@ const result = ConnectorSchema.parse(data);
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **grant** | `'client_credentials'` | ✅ | No user step: the client exchanges its own credentials for a token. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret; declared together with `clientId` (a `client_credentials` client is confidential). Never an inline secret. |

Expand Down Expand Up @@ -561,10 +562,11 @@ This schema accepts one of the following structures:
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required, except where the client comes from elsewhere: `provider: 'mcp'` over http may register one dynamically, and with `clientSource: 'installer'` the administrator enters it on the connection, so it is not named here. |
| **grant** | `'authorization_code'` | ✅ | An administrator connects once; the platform refreshes the grant per dispatch. |
| **authorizationUrl** | `string` | optional | Authorization endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client. Never an inline secret. |
| **clientSource** | `Enum<'installer'>` | optional | [PLANNED — not enforced] Where the OAuth client comes from. `'installer'`: the administrator of each deployment registers their own client with the vendor, against that deployment's callback, and enters its id and secret on the instance's connection, so the definition names no `clientId` and no `credentialRef`. Omit it to name a pre-registered `clientId` here, or to let `provider: 'mcp'` over http register a client. Not served yet: the automation service reads it once the server half of the ADR-0143 D2.1 amendment lands. Until then, off `provider: 'mcp'` over http, an instance that declares it is held degraded for a missing `clientId`; on `provider: 'mcp'` over http the provider registers a client dynamically instead of taking the installer's. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client, and with `clientSource: 'installer'`, where the administrator enters the secret on the connection. Never an inline secret. |

---

Expand All @@ -580,7 +582,7 @@ This schema accepts one of the following structures:
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **grant** | `'client_credentials'` | ✅ | No user step: the client exchanges its own credentials for a token. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret; declared together with `clientId` (a `client_credentials` client is confidential). Never an inline secret. |

Expand All @@ -599,10 +601,11 @@ This schema accepts one of the following structures:
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required, except where the client comes from elsewhere: `provider: 'mcp'` over http may register one dynamically, and with `clientSource: 'installer'` the administrator enters it on the connection, so it is not named here. |
| **grant** | `'authorization_code'` | ✅ | An administrator connects once; the platform refreshes the grant per dispatch. |
| **authorizationUrl** | `string` | optional | Authorization endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client. Never an inline secret. |
| **clientSource** | `Enum<'installer'>` | optional | [PLANNED — not enforced] Where the OAuth client comes from. `'installer'`: the administrator of each deployment registers their own client with the vendor, against that deployment's callback, and enters its id and secret on the instance's connection, so the definition names no `clientId` and no `credentialRef`. Omit it to name a pre-registered `clientId` here, or to let `provider: 'mcp'` over http register a client. Not served yet: the automation service reads it once the server half of the ADR-0143 D2.1 amendment lands. Until then, off `provider: 'mcp'` over http, an instance that declares it is held degraded for a missing `clientId`; on `provider: 'mcp'` over http the provider registers a client dynamically instead of taking the installer's. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client, and with `clientSource: 'installer'`, where the administrator enters the secret on the connection. Never an inline secret. |


---
Expand All @@ -617,7 +620,7 @@ This schema accepts one of the following structures:
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **grant** | `'client_credentials'` | ✅ | No user step: the client exchanges its own credentials for a token. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret; declared together with `clientId` (a `client_credentials` client is confidential). Never an inline secret. |

Expand Down Expand Up @@ -870,10 +873,11 @@ Connector type
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required, except where the client comes from elsewhere: `provider: 'mcp'` over http may register one dynamically, and with `clientSource: 'installer'` the administrator enters it on the connection, so it is not named here. |
| **grant** | `'authorization_code'` | ✅ | An administrator connects once; the platform refreshes the grant per dispatch. |
| **authorizationUrl** | `string` | optional | Authorization endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client. Never an inline secret. |
| **clientSource** | `Enum<'installer'>` | optional | [PLANNED — not enforced] Where the OAuth client comes from. `'installer'`: the administrator of each deployment registers their own client with the vendor, against that deployment's callback, and enters its id and secret on the instance's connection, so the definition names no `clientId` and no `credentialRef`. Omit it to name a pre-registered `clientId` here, or to let `provider: 'mcp'` over http register a client. Not served yet: the automation service reads it once the server half of the ADR-0143 D2.1 amendment lands. Until then, off `provider: 'mcp'` over http, an instance that declares it is held degraded for a missing `clientId`; on `provider: 'mcp'` over http the provider registers a client dynamically instead of taking the installer's. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret of a pre-registered client (requires `clientId`). Omit for a public client, and with `clientSource: 'installer'`, where the administrator enters the secret on the connection. Never an inline secret. |

### Nested Shape: `DeclarativeConnectorEntry.auth[option 5][grant='client_credentials']`

Expand All @@ -883,7 +887,7 @@ Connector type
| **tokenUrl** | `string` | optional | Token endpoint. Required (except `provider: 'mcp'` over http, which discovers it). |
| **scopes** | `string[]` | optional | Scopes to request; sent space-separated. |
| **resource** | `string` | optional | Resource indicator (RFC 8707) — the API the token is for, where the authorization server asks for one (some call it the audience). |
| **clientId** | `string` | optional | OAuth2 client id (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **clientId** | `string` | optional | OAuth2 client id of a pre-registered client (not a secret). Required (except `provider: 'mcp'` over http, where the provider may register a client dynamically). |
| **grant** | `'client_credentials'` | ✅ | No user step: the client exchanges its own credentials for a token. |
| **credentialRef** | `string` | optional | Secrets-layer reference resolved to the client secret; declared together with `clientId` (a `client_credentials` client is confidential). Never an inline secret. |

Expand Down
Loading
Loading