|
1 | 1 | --- |
2 | 2 | "@executor-js/sdk": patch |
| 3 | +"@executor-js/plugin-mcp": patch |
| 4 | +"@executor-js/plugin-openapi": patch |
| 5 | +"@executor-js/plugin-graphql": patch |
3 | 6 | --- |
4 | 7 |
|
5 | | -Require evidence before a connection is marked permanently expired, and let the health probe refresh before it answers `expired`. |
| 8 | +Stop recording a permanent **Expired** verdict without evidence, and stop the refresh races that produced one. |
6 | 9 |
|
7 | | -A refresher whose grant is refused now reads the stored refresh token again. When a peer instance rotated that token while the request was in flight, the call adopts the access token the peer persisted and records no rejection. Previously the loser of a concurrent refresh wrote `oauthReauthRequiredAt` onto a connection that still held a valid rotated refresh token. Every surface then answered `expired` without probing, and no tool call could refresh it again: only a re-authorization recovered it. The record is also skipped when `expires_at` moved forward during the grant, which is the same peer success read from the row. |
| 10 | +A refresher whose grant is refused now reads the stored refresh token again; when a peer instance rotated it during the request, the call adopts the access token that peer persisted and records nothing. Previously the loser of a concurrent refresh wrote `oauthReauthRequiredAt` onto a connection that still held a valid rotated refresh token, and every surface then answered `expired` without probing — no tool call could refresh it again, only a re-authorization recovered it. The record write is also skipped when `expires_at` moved forward during the grant, which is the same peer success read from the row. |
8 | 11 |
|
9 | | -`isPermanentTokenRejection` no longer reads 408, 425, or 429 as a definitive refusal. One rate-limited minute at a token endpoint therefore no longer ends a grant. Those statuses now behave like a 5xx response, and the next call retries. |
| 12 | +`isPermanentTokenRejection` no longer reads 408, 425, or 429 as a definitive refusal, so one rate-limited minute at a token endpoint no longer ends a grant; those statuses behave like a 5xx and the next call retries. A refresh response that omits `expires_in` (RFC 6749 makes it optional) no longer erases `expires_at`: the mint records the advertised lifetime in `provider_state.oauthTokenLifetimeMs` and a refresh derives the expiry from it, instead of disabling proactive refresh for the rest of the connection's life. |
10 | 13 |
|
11 | | -`connections.checkHealth` re-mints the token once and probes again before it answers `expired` for an OAuth connection. A revoked token, an idle timeout shorter than the advertised lifetime, or a null `expires_at` therefore no longer shows a working connection as dead. |
| 14 | +`connections.checkHealth` re-mints once and probes again before it answers `expired` for an OAuth connection, so a revoked token, an idle timeout shorter than the advertised lifetime, or a null `expires_at` no longer shows a working connection as dead until a tool call heals it. The plugin is asked first with or without a declared health-check spec, so a plugin whose probe needs no spec (MCP lists tools) gives its OAuth connections a real verdict; only a plugin that answers `unknown` falls back to the credential-only verdict, and that verdict now reports `expired` when a credential value resolves to nothing. |
12 | 15 |
|
13 | | -A connection whose integration declares no probe operation is now asked of the plugin first. A plugin that can answer without a spec — MCP lists its tools — gives a real verdict for its OAuth connections, which the credential-only branch never reached. Only when the plugin itself answers `unknown` does the credential-only verdict replace it, and that verdict is now computed from the values the probe already resolved, so nothing refreshes twice. A credential that resolves to nothing reads as `expired` there too, matching the plugins and heal-on-use. |
| 16 | +The MCP liveness probe takes the invocation pool's lease instead of dialling a second connection, bounded by the shared 15s discovery deadline; a probe of a stdio server no longer starts a second child process, which single-instance servers (Chrome DevTools MCP, Playwright MCP, `docker run -i`) refused — reporting a live, serving connection as broken on every page mount. A 403 scope shortfall on a probe reads `degraded` instead of `expired`, from either an RFC 6750 `WWW-Authenticate` challenge or a body marker. The GraphQL probe no longer reads a transport failure's prose as a dead credential (`connect EACCES: permission denied` on a socket is not an authentication verdict). |
14 | 17 |
|
15 | | -A refresh response that omits `expires_in` no longer erases `expires_at`. RFC 6749 makes the field optional, so an authorization server that advertised a lifetime on the code exchange and omitted it on refresh used to disable proactive refresh for the rest of the connection's life. The mint now records the advertised lifetime in `provider_state.oauthTokenLifetimeMs`, and a refresh without `expires_in` derives the expiry from it. |
16 | | - |
17 | | -A 403 scope shortfall on a probe now reads as `degraded` rather than `expired`: the credential authenticated, the grant is too narrow, and the remedy is a new consent rather than a reconnect. |
18 | | - |
19 | | -The test authorization server's MCP resource endpoint (`serveOAuthTestServer` at `/mcp`) now speaks the JSON-RPC protocol honestly: it answers the request's own id, answers `tools/list` with an empty catalog, and stays silent for notifications. The previous canned reply used a fixed id, so any client that completed the handshake waited forever for its `tools/list` response and every catalog sync or liveness probe against the endpoint timed out at the discovery deadline — a limitation invisible while OAuth health checks never dialled, and exposed once they do. |
| 18 | +The test authorization server's `/mcp` resource endpoint now speaks JSON-RPC honestly — the request's own id, an empty catalog for `tools/list`, silence for notifications — where the old canned reply used a fixed id and left every completed handshake waiting forever for its `tools/list` response. |
0 commit comments