History

Record originIntegration on manual OAuth clients

A manual OAuth app registered from within an integration's connect dialog now stamps that integration on origin_integration (kind stays manual), mirroring how DCR clients already record the integration that requested them. Thread it through the createClient wire contract (HTTP API + core tool), persist it for both manual and DCR origins, and have parseOAuthClientOrigin surface it on the manual summary so the picker can match a bring-your-own app to its integration by recorded intent rather than guessing by root domain.

Rhys Sullivan committed 4 months ago

Commit 7b4a85b1 parent 8155ab27 files · +37 −8Browse files

Unified · Split

7 files
  1. packages/core/api/src/handlers/oauth.ts +1 −0
  2. packages/core/api/src/oauth/api.ts +3 −0
  3. packages/core/sdk/src/core-tools.ts +10 −0
  4. packages/core/sdk/src/oauth-client.ts +9 −1
  5. packages/core/sdk/src/oauth-list-clients.test.ts +3 −1
  6. packages/core/sdk/src/oauth-service.ts +10 −6
  7. packages/core/sdk/src/shared.ts +1 −0
packages/core/api/src/handlers/oauth.ts+1−0
@@ -103,6 +103,7 @@ export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handler · 102 unmodified lines
103103 clientId: payload.clientId,
104104 clientSecret: payload.clientSecret,
105105 resource: payload.resource ?? null,
106+ origin: { kind: "manual", integration: payload.originIntegration ?? null },
106107 });
107108 return { client };
108109 }),
packages/core/api/src/oauth/api.ts+3−0
@@ -65,6 +65,9 @@ const CreateClientPayload = Schema.Struct({ · 64 unmodified lines
6565 clientId: Schema.String,
6666 clientSecret: Schema.String,
6767 resource: Schema.optional(Schema.NullOr(Schema.String)),
68+ /** Integration whose connect dialog registered this manual app. Recorded so
69+ * the picker matches it to this integration by intent, not root domain. */
70+ originIntegration: Schema.optional(Schema.NullOr(IntegrationSlug)),
6871 });
6972
7073 const CreateClientResponse = Schema.Struct({
packages/core/sdk/src/core-tools.ts+10−0
@@ -243,6 +243,9 @@ const OAuthCreateClientInput = Schema.Struct({ · 242 unmodified lines
243243 grant: OAuthGrantSchema,
244244 clientId: Schema.String,
245245 resource: Schema.optional(Schema.NullOr(Schema.String)),
246+ /** Integration whose connect dialog registered this manual app. Recorded so
247+ * the picker can match it by intent (exact) instead of by root domain. */
248+ originIntegration: Schema.optional(Schema.NullOr(Schema.String)),
246249 });
247250 // Browser-handoff for a CONFIDENTIAL OAuth app: carries only the NON-secret
248251 // fields the form pre-fills. The client secret is typed by the human in the web
@@ -713,6 +716,13 @@ export const coreToolsPlugin = definePlugin((options: CoreToolsPluginOptions = { · 464 unmodified lines
713716 // `oauth.clients.createHandoff`.
714717 clientSecret: "",
715718 resource: input.resource ?? null,
719+ origin: {
720+ kind: "manual",
721+ integration:
722+ input.originIntegration == null
723+ ? null
724+ : IntegrationSlug.make(input.originIntegration),
725+ },
716726 }),
717727 (client) => ({ client: String(client) }),
718728 ),
packages/core/sdk/src/oauth-client.ts+9−1
@@ -64,7 +64,15 @@ export interface OAuthClient { · 63 unmodified lines
6464 }
6565
6666 export type OAuthClientOrigin =
67− | { readonly kind: "manual" }
67+ | {
68+ readonly kind: "manual";
69+ /** Integration whose connect dialog registered this manual app, when the
70+ * registration happened from within one. Lets the picker match a BYO app
71+ * to its integration by recorded intent (exact) instead of guessing by
72+ * root domain. Null for apps registered outside any integration context
73+ * (and for legacy rows predating the stamp). */
74+ readonly integration?: IntegrationSlug | null;
75+ }
6876 | {
6977 readonly kind: "dynamic_client_registration";
7078 readonly integration?: IntegrationSlug | null;
packages/core/sdk/src/oauth-list-clients.test.ts+3−1
@@ -91,7 +91,9 @@ describe("oauth.listClients", () => { · 90 unmodified lines
9191 tokenUrl: "https://acme.test/token",
9292 resource: null,
9393 clientId: "org-client-id",
94− origin: { kind: "manual" },
94+ // Manual apps carry a nullable recorded-intent integration; a client
95+ // created outside any integration dialog stamps null.
96+ origin: { kind: "manual", integration: null },
9597 });
9698 expect(user!.owner).toBe("user");
9799 expect(user!.grant).toBe("client_credentials");
packages/core/sdk/src/oauth-service.ts+10−6
@@ -367,7 +367,11 @@ const parseOAuthClientOrigin = (row: { · 366 unmodified lines
367367 ) {
368368 return { kind: "dynamic_client_registration", integration: null };
369369 }
370− return { kind: "manual" };
370+ return {
371+ kind: "manual",
372+ integration:
373+ row.origin_integration == null ? null : IntegrationSlug.make(String(row.origin_integration)),
374+ };
371375 };
372376
373377 interface LoadedOAuthClient {
@@ -614,12 +618,12 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { · 240 unmodified lines
614618 client_secret_item_id: clientSecretItemIdValue,
615619 resource: input.resource ?? null,
616620 origin_kind: input.origin?.kind ?? "manual",
621+ // Recorded intent, kept for BOTH origins: a manual app registered from
622+ // an integration's dialog stamps its integration so the picker can
623+ // match it exactly, the same way a DCR client records the integration
624+ // that requested it.
617625 origin_integration:
618− input.origin?.kind === "dynamic_client_registration"
619− ? input.origin.integration == null
620− ? null
621− : String(input.origin.integration)
622− : null,
626+ input.origin?.integration == null ? null : String(input.origin.integration),
623627 origin_issuer:
624628 input.origin?.kind === "dynamic_client_registration"
625629 ? (canonicalIssuerUrl(input.originIssuer) ?? null)
packages/core/sdk/src/shared.ts+1−0
@@ -110,6 +110,7 @@ export { · 109 unmodified lines
110110 type OAuthGrant,
111111 type OAuthAuthentication,
112112 type OAuthClient,
113+ type OAuthClientOrigin,
113114 type OAuthClientSummary,
114115 type CreateOAuthClientInput,
115116 type RegisterDynamicClientInput,