History

connections: extract sign-in state into a first-class SDK primitive

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.

Rhys Sullivan committed 6 months ago

Commit bd7faba1 parent 7909ef473 files · +6087 −2239Browse files

Showing the changes to packages/plugins/openapi/src/api (2 of 73 files). Show all

Unified · Split

2 files
  1. packages/plugins/openapi/src/api/group.ts +6 −6
  2. packages/plugins/openapi/src/api/handlers.ts +1 −3
packages/plugins/openapi/src/api/group.ts+6−6
@@ -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
45const UpdateSourceResponse = Schema.Struct({48const 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 which65// 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 shadow68// callers never pick them.
66// org-level fallbacks via secret fall-through on read.
67const StartOAuthIdentityFields = {69const 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
77const StartOAuthPayload = Schema.Union(78const 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. The86 // RFC 6749 §4.4 — no user-interactive step, no session, no popup. The
87 // plugin exchanges tokens inline and returns a completed auth. The87 // plugin exchanges tokens inline and returns a completed auth. The
packages/plugins/openapi/src/api/handlers.ts+1−3
@@ -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 )

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t