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 | name: Schema.optional(Schema.String), | 40 | name: Schema.optional(Schema.String), |
| 41 | baseUrl: Schema.optional(Schema.String), | 41 | baseUrl: Schema.optional(Schema.String), |
| 42 | headers: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), | 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 | const UpdateSourceResponse = Schema.Struct({ | 48 | const UpdateSourceResponse = Schema.Struct({ |
| @@ -60,10 +63,9 @@ const AddSpecResponse = Schema.Struct({ · 14 unmodified lines | |||
| 60 | // --------------------------------------------------------------------------- | 63 | // --------------------------------------------------------------------------- |
| 61 | 64 | ||
| 62 | // Shared identity fields for both OAuth2 flows. `tokenScope` names which | 65 | // Shared identity fields for both OAuth2 flows. `tokenScope` names which |
| 63 | // executor scope will own the minted tokens (typically the per-user scope). | 66 | // executor scope will own the resulting Connection (typically the per-user |
| 64 | // The token secret ids are pre-decided so the source's stored `OAuth2Auth` | 67 | // scope). Token secret ids are minted by the SDK from the Connection id — |
| 65 | // can reference the same ids across every user — per-user values shadow | 68 | // callers never pick them. |
| 66 | // org-level fallbacks via secret fall-through on read. | ||
| 67 | const StartOAuthIdentityFields = { | 69 | const StartOAuthIdentityFields = { |
| 68 | displayName: Schema.String, | 70 | displayName: Schema.String, |
| 69 | securitySchemeName: Schema.String, | 71 | securitySchemeName: Schema.String, |
| @@ -71,7 +73,6 @@ const StartOAuthIdentityFields = { · 1 unmodified line | |||
| 71 | clientIdSecretId: Schema.String, | 73 | clientIdSecretId: Schema.String, |
| 72 | scopes: Schema.Array(Schema.String), | 74 | scopes: Schema.Array(Schema.String), |
| 73 | tokenScope: Schema.optional(ScopeId), | 75 | tokenScope: Schema.optional(ScopeId), |
| 74 | accessTokenSecretId: Schema.String, | ||
| 75 | } as const; | 76 | } as const; |
| 76 | 77 | ||
| 77 | const StartOAuthPayload = Schema.Union( | 78 | const StartOAuthPayload = Schema.Union( |
| @@ -81,7 +82,6 @@ const StartOAuthPayload = Schema.Union( · 3 unmodified lines | |||
| 81 | authorizationUrl: Schema.String, | 82 | authorizationUrl: Schema.String, |
| 82 | redirectUrl: Schema.String, | 83 | redirectUrl: Schema.String, |
| 83 | clientSecretSecretId: Schema.optional(Schema.NullOr(Schema.String)), | 84 | clientSecretSecretId: Schema.optional(Schema.NullOr(Schema.String)), |
| 84 | refreshTokenSecretId: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 85 | }), | 85 | }), |
| 86 | // RFC 6749 §4.4 — no user-interactive step, no session, no popup. The | 86 | // RFC 6749 §4.4 — no user-interactive step, no session, no popup. The |
| 87 | // plugin exchanges tokens inline and returns a completed auth. The | 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 | name: payload.name, | 91 | name: payload.name, |
| 92 | baseUrl: payload.baseUrl, | 92 | baseUrl: payload.baseUrl, |
| 93 | headers: payload.headers as Record<string, HeaderValue> | undefined, | 93 | headers: payload.headers as Record<string, HeaderValue> | undefined, |
| 94 | oauth2: payload.oauth2, | ||
| 94 | } as OpenApiUpdateSourceInput); | 95 | } as OpenApiUpdateSourceInput); |
| 95 | return { updated: true }; | 96 | return { updated: true }; |
| 96 | })), | 97 | })), |
| @@ -112,7 +113,6 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 15 unmodified lines | |||
| 112 | clientSecretSecretId: payload.clientSecretSecretId, | 113 | clientSecretSecretId: payload.clientSecretSecretId, |
| 113 | scopes: [...payload.scopes], | 114 | scopes: [...payload.scopes], |
| 114 | tokenScope, | 115 | tokenScope, |
| 115 | accessTokenSecretId: payload.accessTokenSecretId, | ||
| 116 | }); | 116 | }); |
| 117 | } | 117 | } |
| 118 | return yield* ext.startOAuth({ | 118 | return yield* ext.startOAuth({ |
| @@ -126,8 +126,6 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 7 unmodified lines | |||
| 126 | clientSecretSecretId: payload.clientSecretSecretId ?? null, | 126 | clientSecretSecretId: payload.clientSecretSecretId ?? null, |
| 127 | scopes: [...payload.scopes], | 127 | scopes: [...payload.scopes], |
| 128 | tokenScope, | 128 | tokenScope, |
| 129 | accessTokenSecretId: payload.accessTokenSecretId, | ||
| 130 | refreshTokenSecretId: payload.refreshTokenSecretId ?? null, | ||
| 131 | }); | 129 | }); |
| 132 | })), | 130 | })), |
| 133 | ) | 131 | ) |