Introduces a `Connection` resource on the core SDK (`ctx.connections`) that owns live OAuth access/refresh tokens and the plugin-specific providerState needed to refresh them. Plugins register a `ConnectionProvider` alongside their secret providers; source auth configs become thin pointers (`{kind: "oauth2", connectionId}`) instead of inlining secret/token state. - core/sdk: ConnectionId brand, ConnectionRef, CreateConnectionInput, ConnectionRefreshError, ConnectionRefreshInput, executor.connections (get/list/remove/accessToken), plugin.connectionProviders hook. - core/api: /scopes/:id/connections endpoints + handler group, secrets list filter that hides connection-owned rows. - storage: new `connection` table (drizzle migrations for cloud + local). - plugins (openapi, google-discovery, mcp): migrated onto ctx.connections, widened stored-auth schemas to carry the full OAuth config so re-sign-in is reproducible without the prior Connection row existing. - react: `/connections` page with removal; signIn slot on SourcePlugin; per-plugin SignInButton mounted on the source detail top bar with dynamic "Sign in" / "Reconnect" copy driven by `connectionsAtom`. - openapi store: tolerant oauth2 decode at the row boundary so legacy rows written before the schema widening surface without OAuth attached instead of crashing the source list. - oauth2 plugin: removes the dead standalone refresh module now that refresh lives behind ctx.connections.
Showing the changes to packages/plugins/openapi/src/api (2 of 73 files). Show all
Unified · Split
| @@ -40,6 +40,9 @@ const UpdateSourcePayload = Schema.Struct({ · 39 unmodified lines | ||
| 40 | 40 | name: Schema.optional(Schema.String), |
| 41 | 41 | baseUrl: Schema.optional(Schema.String), |
| 42 | 42 | headers: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), |
| 43 | + // Set after a successful re-authenticate to rewrite the source's | |
| 44 | + // OAuth2Auth pointer to the freshly minted connection. | |
| 45 | + oauth2: Schema.optional(OAuth2Auth), | |
| 43 | 46 | }); |
| 44 | 47 | |
| 45 | 48 | const UpdateSourceResponse = Schema.Struct({ |
| @@ -60,10 +63,9 @@ const AddSpecResponse = Schema.Struct({ · 14 unmodified lines | ||
| 60 | 63 | // --------------------------------------------------------------------------- |
| 61 | 64 | |
| 62 | 65 | // Shared identity fields for both OAuth2 flows. `tokenScope` names which |
| 63 | −// executor scope will own the minted tokens (typically the per-user scope). | |
| 64 | −// The token secret ids are pre-decided so the source's stored `OAuth2Auth` | |
| 65 | −// can reference the same ids across every user — per-user values shadow | |
| 66 | −// org-level fallbacks via secret fall-through on read. | |
| 66 | +// executor scope will own the resulting Connection (typically the per-user | |
| 67 | +// scope). Token secret ids are minted by the SDK from the Connection id — | |
| 68 | +// callers never pick them. | |
| 67 | 69 | const StartOAuthIdentityFields = { |
| 68 | 70 | displayName: Schema.String, |
| 69 | 71 | securitySchemeName: Schema.String, |
| @@ -71,7 +73,6 @@ const StartOAuthIdentityFields = { · 1 unmodified line | ||
| 71 | 73 | clientIdSecretId: Schema.String, |
| 72 | 74 | scopes: Schema.Array(Schema.String), |
| 73 | 75 | tokenScope: Schema.optional(ScopeId), |
| 74 | − accessTokenSecretId: Schema.String, | |
| 75 | 76 | } as const; |
| 76 | 77 | |
| 77 | 78 | const StartOAuthPayload = Schema.Union( |
| @@ -81,7 +82,6 @@ const StartOAuthPayload = Schema.Union( · 3 unmodified lines | ||
| 81 | 82 | authorizationUrl: Schema.String, |
| 82 | 83 | redirectUrl: Schema.String, |
| 83 | 84 | clientSecretSecretId: Schema.optional(Schema.NullOr(Schema.String)), |
| 84 | − refreshTokenSecretId: Schema.optional(Schema.NullOr(Schema.String)), | |
| 85 | 85 | }), |
| 86 | 86 | // RFC 6749 §4.4 — no user-interactive step, no session, no popup. The |
| 87 | 87 | // plugin exchanges tokens inline and returns a completed auth. The |
| @@ -91,6 +91,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 90 unmodified lines | ||
| 91 | 91 | name: payload.name, |
| 92 | 92 | baseUrl: payload.baseUrl, |
| 93 | 93 | headers: payload.headers as Record<string, HeaderValue> | undefined, |
| 94 | + oauth2: payload.oauth2, | |
| 94 | 95 | } as OpenApiUpdateSourceInput); |
| 95 | 96 | return { updated: true }; |
| 96 | 97 | })), |
| @@ -112,7 +113,6 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 15 unmodified lines | ||
| 112 | 113 | clientSecretSecretId: payload.clientSecretSecretId, |
| 113 | 114 | scopes: [...payload.scopes], |
| 114 | 115 | tokenScope, |
| 115 | − accessTokenSecretId: payload.accessTokenSecretId, | |
| 116 | 116 | }); |
| 117 | 117 | } |
| 118 | 118 | return yield* ext.startOAuth({ |
| @@ -126,8 +126,6 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 7 unmodified lines | ||
| 126 | 126 | clientSecretSecretId: payload.clientSecretSecretId ?? null, |
| 127 | 127 | scopes: [...payload.scopes], |
| 128 | 128 | tokenScope, |
| 129 | − accessTokenSecretId: payload.accessTokenSecretId, | |
| 130 | − refreshTokenSecretId: payload.refreshTokenSecretId ?? null, | |
| 131 | 129 | }); |
| 132 | 130 | })), |
| 133 | 131 | ) |