From 40ff0fdcd5adcacbdfc865c3f290ac0e148389ba Mon Sep 17 00:00:00 2001 From: Ampersand Ops Date: Tue, 14 Jul 2026 00:38:04 +0000 Subject: [PATCH 1/7] [auto] Update openapi to 2e1dd6eef338426a1aa4a4f0fea94792cde50025 --- .../api/.openapi-generator/FILES | 6 + .../api/src/apis/ConnectionApi.ts | 73 +++++++++ .../src/models/CustomAuthConnectRequest.ts | 153 ++++++++++++++++++ .../src/models/CustomAuthConnectResponse.ts | 65 ++++++++ .../models/CustomAuthConnectResponseOneOf.ts | 73 +++++++++ .../models/CustomAuthConnectResponseOneOf1.ts | 73 +++++++++ .../api/src/models/CustomAuthInput.ts | 35 ++++ .../api/src/models/CustomAuthInputOption.ts | 75 +++++++++ .../api/src/models/CustomAuthOpts.ts | 16 ++ .../api/src/models/RedirectResponse.ts | 75 +++++++++ generated-sources/api/src/models/index.ts | 6 + 11 files changed, 650 insertions(+) create mode 100644 generated-sources/api/src/models/CustomAuthConnectRequest.ts create mode 100644 generated-sources/api/src/models/CustomAuthConnectResponse.ts create mode 100644 generated-sources/api/src/models/CustomAuthConnectResponseOneOf.ts create mode 100644 generated-sources/api/src/models/CustomAuthConnectResponseOneOf1.ts create mode 100644 generated-sources/api/src/models/CustomAuthInputOption.ts create mode 100644 generated-sources/api/src/models/RedirectResponse.ts diff --git a/generated-sources/api/.openapi-generator/FILES b/generated-sources/api/.openapi-generator/FILES index 883772189..0174c7108 100644 --- a/generated-sources/api/.openapi-generator/FILES +++ b/generated-sources/api/.openapi-generator/FILES @@ -93,8 +93,13 @@ src/models/CreateProviderAppRequest.ts src/models/CreateRevisionRequest.ts src/models/CreateTopicDestinationRouteRequest.ts src/models/CreateTopicRequest.ts +src/models/CustomAuthConnectRequest.ts +src/models/CustomAuthConnectResponse.ts +src/models/CustomAuthConnectResponseOneOf.ts +src/models/CustomAuthConnectResponseOneOf1.ts src/models/CustomAuthHeader.ts src/models/CustomAuthInput.ts +src/models/CustomAuthInputOption.ts src/models/CustomAuthOpts.ts src/models/CustomAuthQueryParam.ts src/models/DefaultPeriod.ts @@ -198,6 +203,7 @@ src/models/ReadConfig.ts src/models/ReadConfigAllOf.ts src/models/ReadConfigObject.ts src/models/ReadFilter.ts +src/models/RedirectResponse.ts src/models/Revision.ts src/models/SearchOperators.ts src/models/SearchSupport.ts diff --git a/generated-sources/api/src/apis/ConnectionApi.ts b/generated-sources/api/src/apis/ConnectionApi.ts index 44119163b..91c1e809d 100644 --- a/generated-sources/api/src/apis/ConnectionApi.ts +++ b/generated-sources/api/src/apis/ConnectionApi.ts @@ -17,6 +17,8 @@ import * as runtime from '../runtime'; import type { ApiProblem, Connection, + CustomAuthConnectRequest, + CustomAuthConnectResponse, GenerateConnectionRequest, InputValidationProblem, UpdateConnectionRequest, @@ -26,6 +28,10 @@ import { ApiProblemToJSON, ConnectionFromJSON, ConnectionToJSON, + CustomAuthConnectRequestFromJSON, + CustomAuthConnectRequestToJSON, + CustomAuthConnectResponseFromJSON, + CustomAuthConnectResponseToJSON, GenerateConnectionRequestFromJSON, GenerateConnectionRequestToJSON, InputValidationProblemFromJSON, @@ -34,6 +40,10 @@ import { UpdateConnectionRequestToJSON, } from '../models'; +export interface CustomAuthConnectOperationRequest { + customAuthConnectParams: CustomAuthConnectRequest; +} + export interface DeleteConnectionRequest { projectIdOrName: string; connectionId: string; @@ -72,6 +82,22 @@ export interface UpdateConnectionOperationRequest { * @interface ConnectionApiInterface */ export interface ConnectionApiInterface { + /** + * Drives a multi-step custom auth flow (browser redirects and/or server-side credential-exchange calls). Call it once with the flow inputs to start; if the response contains a redirect, open it, then call again with the returned sessionId and the provider\'s callback params to continue. Repeat until the response contains a connection. Used by the prebuilt UI components; only providers whose ProviderInfo has customOpts.multiStep present can use this endpoint. + * @summary Start or continue a multi-step custom auth flow + * @param {CustomAuthConnectRequest} customAuthConnectParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ConnectionApiInterface + */ + customAuthConnectRaw(requestParameters: CustomAuthConnectOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Drives a multi-step custom auth flow (browser redirects and/or server-side credential-exchange calls). Call it once with the flow inputs to start; if the response contains a redirect, open it, then call again with the returned sessionId and the provider\'s callback params to continue. Repeat until the response contains a connection. Used by the prebuilt UI components; only providers whose ProviderInfo has customOpts.multiStep present can use this endpoint. + * Start or continue a multi-step custom auth flow + */ + customAuthConnect(requestParameters: CustomAuthConnectOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + /** * * @summary Delete a connection @@ -167,6 +193,53 @@ export interface ConnectionApiInterface { */ export class ConnectionApi extends runtime.BaseAPI implements ConnectionApiInterface { + /** + * Drives a multi-step custom auth flow (browser redirects and/or server-side credential-exchange calls). Call it once with the flow inputs to start; if the response contains a redirect, open it, then call again with the returned sessionId and the provider\'s callback params to continue. Repeat until the response contains a connection. Used by the prebuilt UI components; only providers whose ProviderInfo has customOpts.multiStep present can use this endpoint. + * Start or continue a multi-step custom auth flow + */ + async customAuthConnectRaw(requestParameters: CustomAuthConnectOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.customAuthConnectParams === null || requestParameters.customAuthConnectParams === undefined) { + throw new runtime.RequiredError('customAuthConnectParams','Required parameter requestParameters.customAuthConnectParams was null or undefined when calling customAuthConnect.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["X-Api-Key"] = this.configuration.apiKey("X-Api-Key"); // APIKeyHeader authentication + } + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("Bearer", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + const response = await this.request({ + path: `/custom-auth/connect`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: CustomAuthConnectRequestToJSON(requestParameters.customAuthConnectParams), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => CustomAuthConnectResponseFromJSON(jsonValue)); + } + + /** + * Drives a multi-step custom auth flow (browser redirects and/or server-side credential-exchange calls). Call it once with the flow inputs to start; if the response contains a redirect, open it, then call again with the returned sessionId and the provider\'s callback params to continue. Repeat until the response contains a connection. Used by the prebuilt UI components; only providers whose ProviderInfo has customOpts.multiStep present can use this endpoint. + * Start or continue a multi-step custom auth flow + */ + async customAuthConnect(requestParameters: CustomAuthConnectOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.customAuthConnectRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * Delete a connection */ diff --git a/generated-sources/api/src/models/CustomAuthConnectRequest.ts b/generated-sources/api/src/models/CustomAuthConnectRequest.ts new file mode 100644 index 000000000..2f2c9dee0 --- /dev/null +++ b/generated-sources/api/src/models/CustomAuthConnectRequest.ts @@ -0,0 +1,153 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ampersand public API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { ProviderMetadataInfo } from './ProviderMetadataInfo'; +import { + ProviderMetadataInfoFromJSON, + ProviderMetadataInfoFromJSONTyped, + ProviderMetadataInfoToJSON, +} from './ProviderMetadataInfo'; + +/** + * Request body for the /custom-auth/connect endpoint. The first call supplies the flow inputs; subsequent calls supply sessionId and callbackParams to resume after a redirect. + * @export + * @interface CustomAuthConnectRequest + */ +export interface CustomAuthConnectRequest { + /** + * The Ampersand project ID or project name. Required on the first call. + * @type {string} + * @memberof CustomAuthConnectRequest + */ + projectIdOrName: string; + /** + * The provider that this app connects to. Required on the first call (when sessionId is not present); ignored on resume calls. Conditional requirement is enforced at the application layer. + * @type {string} + * @memberof CustomAuthConnectRequest + */ + provider?: string; + /** + * Your application's identifier for the organization or workspace that this connection belongs to. Supplied on the first call; ignored on resume calls (the parked flow's identity is used). + * @type {string} + * @memberof CustomAuthConnectRequest + */ + groupRef?: string; + /** + * The display name for the group. Defaults to groupRef if not provided. Supplied on the first call; ignored on resume calls. + * @type {string} + * @memberof CustomAuthConnectRequest + */ + groupName?: string; + /** + * The ID that your app uses to identify the user whose SaaS credential will be used. Supplied on the first call; ignored on resume calls (the parked flow's identity is used). + * @type {string} + * @memberof CustomAuthConnectRequest + */ + consumerRef?: string; + /** + * The display name for the consumer. Defaults to consumerRef if not provided. Supplied on the first call; ignored on resume calls. + * @type {string} + * @memberof CustomAuthConnectRequest + */ + consumerName?: string; + /** + * + * @type {{ [key: string]: ProviderMetadataInfo; }} + * @memberof CustomAuthConnectRequest + */ + providerMetadata?: { [key: string]: ProviderMetadataInfo; }; + /** + * ID of the provider app. If omitted, the default provider app set up on the Dashboard is assumed. + * @type {string} + * @memberof CustomAuthConnectRequest + */ + providerAppId?: string; + /** + * The consumer-supplied custom auth inputs (keyed by CustomAuthInput.name). Supplied on the first call (when sessionId is not present). + * @type {{ [key: string]: any; }} + * @memberof CustomAuthConnectRequest + */ + customAuth?: { [key: string]: any; }; + /** + * Identifies an in-progress flow to resume after a redirect. Returned in a prior redirect response. When present, provider and customAuth are not required. + * @type {string} + * @memberof CustomAuthConnectRequest + */ + sessionId?: string; + /** + * The query/body params the provider sent to the callback, forwarded to resume the flow. + * @type {{ [key: string]: string; }} + * @memberof CustomAuthConnectRequest + */ + callbackParams?: { [key: string]: string; }; +} + +/** + * Check if a given object implements the CustomAuthConnectRequest interface. + */ +export function instanceOfCustomAuthConnectRequest(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "projectIdOrName" in value; + + return isInstance; +} + +export function CustomAuthConnectRequestFromJSON(json: any): CustomAuthConnectRequest { + return CustomAuthConnectRequestFromJSONTyped(json, false); +} + +export function CustomAuthConnectRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): CustomAuthConnectRequest { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'projectIdOrName': json['projectIdOrName'], + 'provider': !exists(json, 'provider') ? undefined : json['provider'], + 'groupRef': !exists(json, 'groupRef') ? undefined : json['groupRef'], + 'groupName': !exists(json, 'groupName') ? undefined : json['groupName'], + 'consumerRef': !exists(json, 'consumerRef') ? undefined : json['consumerRef'], + 'consumerName': !exists(json, 'consumerName') ? undefined : json['consumerName'], + 'providerMetadata': !exists(json, 'providerMetadata') ? undefined : (mapValues(json['providerMetadata'], ProviderMetadataInfoFromJSON)), + 'providerAppId': !exists(json, 'providerAppId') ? undefined : json['providerAppId'], + 'customAuth': !exists(json, 'customAuth') ? undefined : json['customAuth'], + 'sessionId': !exists(json, 'sessionId') ? undefined : json['sessionId'], + 'callbackParams': !exists(json, 'callbackParams') ? undefined : json['callbackParams'], + }; +} + +export function CustomAuthConnectRequestToJSON(value?: CustomAuthConnectRequest | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'projectIdOrName': value.projectIdOrName, + 'provider': value.provider, + 'groupRef': value.groupRef, + 'groupName': value.groupName, + 'consumerRef': value.consumerRef, + 'consumerName': value.consumerName, + 'providerMetadata': value.providerMetadata === undefined ? undefined : (mapValues(value.providerMetadata, ProviderMetadataInfoToJSON)), + 'providerAppId': value.providerAppId, + 'customAuth': value.customAuth, + 'sessionId': value.sessionId, + 'callbackParams': value.callbackParams, + }; +} + diff --git a/generated-sources/api/src/models/CustomAuthConnectResponse.ts b/generated-sources/api/src/models/CustomAuthConnectResponse.ts new file mode 100644 index 000000000..80b871604 --- /dev/null +++ b/generated-sources/api/src/models/CustomAuthConnectResponse.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ampersand public API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { + CustomAuthConnectResponseOneOf, + instanceOfCustomAuthConnectResponseOneOf, + CustomAuthConnectResponseOneOfFromJSON, + CustomAuthConnectResponseOneOfFromJSONTyped, + CustomAuthConnectResponseOneOfToJSON, +} from './CustomAuthConnectResponseOneOf'; +import { + CustomAuthConnectResponseOneOf1, + instanceOfCustomAuthConnectResponseOneOf1, + CustomAuthConnectResponseOneOf1FromJSON, + CustomAuthConnectResponseOneOf1FromJSONTyped, + CustomAuthConnectResponseOneOf1ToJSON, +} from './CustomAuthConnectResponseOneOf1'; + +/** + * @type CustomAuthConnectResponse + * Response from /custom-auth/connect. Exactly one of redirect or connection is set. A redirect means the client should open the URL and call again with sessionId + callbackParams; a connection means the flow is complete. + * @export + */ +export type CustomAuthConnectResponse = CustomAuthConnectResponseOneOf | CustomAuthConnectResponseOneOf1; + +export function CustomAuthConnectResponseFromJSON(json: any): CustomAuthConnectResponse { + return CustomAuthConnectResponseFromJSONTyped(json, false); +} + +export function CustomAuthConnectResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): CustomAuthConnectResponse { + if ((json === undefined) || (json === null)) { + return json; + } + return { ...CustomAuthConnectResponseOneOfFromJSONTyped(json, true), ...CustomAuthConnectResponseOneOf1FromJSONTyped(json, true) }; +} + +export function CustomAuthConnectResponseToJSON(value?: CustomAuthConnectResponse | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + + if (instanceOfCustomAuthConnectResponseOneOf(value)) { + return CustomAuthConnectResponseOneOfToJSON(value as CustomAuthConnectResponseOneOf); + } + if (instanceOfCustomAuthConnectResponseOneOf1(value)) { + return CustomAuthConnectResponseOneOf1ToJSON(value as CustomAuthConnectResponseOneOf1); + } + + return {}; +} + diff --git a/generated-sources/api/src/models/CustomAuthConnectResponseOneOf.ts b/generated-sources/api/src/models/CustomAuthConnectResponseOneOf.ts new file mode 100644 index 000000000..4e1a5f274 --- /dev/null +++ b/generated-sources/api/src/models/CustomAuthConnectResponseOneOf.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ampersand public API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { RedirectResponse } from './RedirectResponse'; +import { + RedirectResponseFromJSON, + RedirectResponseFromJSONTyped, + RedirectResponseToJSON, +} from './RedirectResponse'; + +/** + * + * @export + * @interface CustomAuthConnectResponseOneOf + */ +export interface CustomAuthConnectResponseOneOf { + /** + * + * @type {RedirectResponse} + * @memberof CustomAuthConnectResponseOneOf + */ + redirect: RedirectResponse; +} + +/** + * Check if a given object implements the CustomAuthConnectResponseOneOf interface. + */ +export function instanceOfCustomAuthConnectResponseOneOf(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "redirect" in value; + + return isInstance; +} + +export function CustomAuthConnectResponseOneOfFromJSON(json: any): CustomAuthConnectResponseOneOf { + return CustomAuthConnectResponseOneOfFromJSONTyped(json, false); +} + +export function CustomAuthConnectResponseOneOfFromJSONTyped(json: any, ignoreDiscriminator: boolean): CustomAuthConnectResponseOneOf { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'redirect': RedirectResponseFromJSON(json['redirect']), + }; +} + +export function CustomAuthConnectResponseOneOfToJSON(value?: CustomAuthConnectResponseOneOf | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'redirect': RedirectResponseToJSON(value.redirect), + }; +} + diff --git a/generated-sources/api/src/models/CustomAuthConnectResponseOneOf1.ts b/generated-sources/api/src/models/CustomAuthConnectResponseOneOf1.ts new file mode 100644 index 000000000..f96a36b9e --- /dev/null +++ b/generated-sources/api/src/models/CustomAuthConnectResponseOneOf1.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ampersand public API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { Connection } from './Connection'; +import { + ConnectionFromJSON, + ConnectionFromJSONTyped, + ConnectionToJSON, +} from './Connection'; + +/** + * + * @export + * @interface CustomAuthConnectResponseOneOf1 + */ +export interface CustomAuthConnectResponseOneOf1 { + /** + * + * @type {Connection} + * @memberof CustomAuthConnectResponseOneOf1 + */ + connection: Connection; +} + +/** + * Check if a given object implements the CustomAuthConnectResponseOneOf1 interface. + */ +export function instanceOfCustomAuthConnectResponseOneOf1(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "connection" in value; + + return isInstance; +} + +export function CustomAuthConnectResponseOneOf1FromJSON(json: any): CustomAuthConnectResponseOneOf1 { + return CustomAuthConnectResponseOneOf1FromJSONTyped(json, false); +} + +export function CustomAuthConnectResponseOneOf1FromJSONTyped(json: any, ignoreDiscriminator: boolean): CustomAuthConnectResponseOneOf1 { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'connection': ConnectionFromJSON(json['connection']), + }; +} + +export function CustomAuthConnectResponseOneOf1ToJSON(value?: CustomAuthConnectResponseOneOf1 | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'connection': ConnectionToJSON(value.connection), + }; +} + diff --git a/generated-sources/api/src/models/CustomAuthInput.ts b/generated-sources/api/src/models/CustomAuthInput.ts index 10895417a..57c1d2984 100644 --- a/generated-sources/api/src/models/CustomAuthInput.ts +++ b/generated-sources/api/src/models/CustomAuthInput.ts @@ -13,6 +13,13 @@ */ import { exists, mapValues } from '../runtime'; +import type { CustomAuthInputOption } from './CustomAuthInputOption'; +import { + CustomAuthInputOptionFromJSON, + CustomAuthInputOptionFromJSONTyped, + CustomAuthInputOptionToJSON, +} from './CustomAuthInputOption'; + /** * A custom input field for authentication. This is used by the frontend to dynamically render input fields for custom auth. The backend will not interpret this. It will however receive the value of this field before making a request (in the connection secrets). * @export @@ -43,8 +50,32 @@ export interface CustomAuthInput { * @memberof CustomAuthInput */ docsURL?: string; + /** + * How the frontend should render this input. "fieldTypeText" is an unmasked field (not sensitive), "fieldTypePassword" is a masked field (sensitive), and "fieldTypeSelect" is a dropdown populated from options. Defaults to "fieldTypePassword" when omitted. + * @type {string} + * @memberof CustomAuthInput + */ + fieldType?: CustomAuthInputFieldTypeEnum; + /** + * The dropdown options, used only when fieldType is "select". + * @type {Array} + * @memberof CustomAuthInput + */ + options?: Array; } + +/** + * @export + */ +export const CustomAuthInputFieldTypeEnum = { + FieldTypeText: 'fieldTypeText', + FieldTypePassword: 'fieldTypePassword', + FieldTypeSelect: 'fieldTypeSelect' +} as const; +export type CustomAuthInputFieldTypeEnum = typeof CustomAuthInputFieldTypeEnum[keyof typeof CustomAuthInputFieldTypeEnum]; + + /** * Check if a given object implements the CustomAuthInput interface. */ @@ -70,6 +101,8 @@ export function CustomAuthInputFromJSONTyped(json: any, ignoreDiscriminator: boo 'displayName': json['displayName'], 'prompt': !exists(json, 'prompt') ? undefined : json['prompt'], 'docsURL': !exists(json, 'docsURL') ? undefined : json['docsURL'], + 'fieldType': !exists(json, 'fieldType') ? undefined : json['fieldType'], + 'options': !exists(json, 'options') ? undefined : ((json['options'] as Array).map(CustomAuthInputOptionFromJSON)), }; } @@ -86,6 +119,8 @@ export function CustomAuthInputToJSON(value?: CustomAuthInput | null): any { 'displayName': value.displayName, 'prompt': value.prompt, 'docsURL': value.docsURL, + 'fieldType': value.fieldType, + 'options': value.options === undefined ? undefined : ((value.options as Array).map(CustomAuthInputOptionToJSON)), }; } diff --git a/generated-sources/api/src/models/CustomAuthInputOption.ts b/generated-sources/api/src/models/CustomAuthInputOption.ts new file mode 100644 index 000000000..b4f09c73a --- /dev/null +++ b/generated-sources/api/src/models/CustomAuthInputOption.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ampersand public API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * A selectable option for a custom auth input whose fieldType is "select". + * @export + * @interface CustomAuthInputOption + */ +export interface CustomAuthInputOption { + /** + * The value stored when this option is selected. + * @type {string} + * @memberof CustomAuthInputOption + */ + value: string; + /** + * The human-readable label shown for this option. + * @type {string} + * @memberof CustomAuthInputOption + */ + label: string; +} + +/** + * Check if a given object implements the CustomAuthInputOption interface. + */ +export function instanceOfCustomAuthInputOption(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "value" in value; + isInstance = isInstance && "label" in value; + + return isInstance; +} + +export function CustomAuthInputOptionFromJSON(json: any): CustomAuthInputOption { + return CustomAuthInputOptionFromJSONTyped(json, false); +} + +export function CustomAuthInputOptionFromJSONTyped(json: any, ignoreDiscriminator: boolean): CustomAuthInputOption { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'value': json['value'], + 'label': json['label'], + }; +} + +export function CustomAuthInputOptionToJSON(value?: CustomAuthInputOption | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'value': value.value, + 'label': value.label, + }; +} + diff --git a/generated-sources/api/src/models/CustomAuthOpts.ts b/generated-sources/api/src/models/CustomAuthOpts.ts index f9333cca4..d1b3208f6 100644 --- a/generated-sources/api/src/models/CustomAuthOpts.ts +++ b/generated-sources/api/src/models/CustomAuthOpts.ts @@ -56,6 +56,18 @@ export interface CustomAuthOpts { * @memberof CustomAuthOpts */ inputs?: Array; + /** + * Input fields the builder configures on their provider app (e.g. client secrets, subscription keys) rather than the consumer. Routed to storage by fieldType. Optional. + * @type {Array} + * @memberof CustomAuthOpts + */ + providerInputs?: Array; + /** + * Whether this provider uses a multi-step custom auth flow (browser redirects and/or server-side credential-exchange calls) driven by the /custom-auth/connect endpoint, rather than static header/query-param injection. The step definitions and handlers live in the connectors library, not the catalog; this flag is the signal that lets clients tell "multi-step custom" apart from plain "custom" at a glance. + * @type {boolean} + * @memberof CustomAuthOpts + */ + multiStep?: boolean; } /** @@ -80,6 +92,8 @@ export function CustomAuthOptsFromJSONTyped(json: any, ignoreDiscriminator: bool 'headers': !exists(json, 'headers') ? undefined : ((json['headers'] as Array).map(CustomAuthHeaderFromJSON)), 'queryParams': !exists(json, 'queryParams') ? undefined : ((json['queryParams'] as Array).map(CustomAuthQueryParamFromJSON)), 'inputs': !exists(json, 'inputs') ? undefined : ((json['inputs'] as Array).map(CustomAuthInputFromJSON)), + 'providerInputs': !exists(json, 'providerInputs') ? undefined : ((json['providerInputs'] as Array).map(CustomAuthInputFromJSON)), + 'multiStep': !exists(json, 'multiStep') ? undefined : json['multiStep'], }; } @@ -95,6 +109,8 @@ export function CustomAuthOptsToJSON(value?: CustomAuthOpts | null): any { 'headers': value.headers === undefined ? undefined : ((value.headers as Array).map(CustomAuthHeaderToJSON)), 'queryParams': value.queryParams === undefined ? undefined : ((value.queryParams as Array).map(CustomAuthQueryParamToJSON)), 'inputs': value.inputs === undefined ? undefined : ((value.inputs as Array).map(CustomAuthInputToJSON)), + 'providerInputs': value.providerInputs === undefined ? undefined : ((value.providerInputs as Array).map(CustomAuthInputToJSON)), + 'multiStep': value.multiStep, }; } diff --git a/generated-sources/api/src/models/RedirectResponse.ts b/generated-sources/api/src/models/RedirectResponse.ts new file mode 100644 index 000000000..50f7fb3ab --- /dev/null +++ b/generated-sources/api/src/models/RedirectResponse.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ampersand public API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Instructs the client to open a URL (e.g. in a popup) to continue a custom auth flow, then resume by calling /custom-auth/connect with the sessionId. + * @export + * @interface RedirectResponse + */ +export interface RedirectResponse { + /** + * The URL the client should open to continue the flow. + * @type {string} + * @memberof RedirectResponse + */ + url: string; + /** + * The flow identifier to pass back to /custom-auth/connect once the provider redirects to the callback. + * @type {string} + * @memberof RedirectResponse + */ + sessionId: string; +} + +/** + * Check if a given object implements the RedirectResponse interface. + */ +export function instanceOfRedirectResponse(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "url" in value; + isInstance = isInstance && "sessionId" in value; + + return isInstance; +} + +export function RedirectResponseFromJSON(json: any): RedirectResponse { + return RedirectResponseFromJSONTyped(json, false); +} + +export function RedirectResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): RedirectResponse { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'url': json['url'], + 'sessionId': json['sessionId'], + }; +} + +export function RedirectResponseToJSON(value?: RedirectResponse | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'url': value.url, + 'sessionId': value.sessionId, + }; +} + diff --git a/generated-sources/api/src/models/index.ts b/generated-sources/api/src/models/index.ts index cf883acc0..185627f82 100644 --- a/generated-sources/api/src/models/index.ts +++ b/generated-sources/api/src/models/index.ts @@ -70,8 +70,13 @@ export * from './CreateProviderAppRequest'; export * from './CreateRevisionRequest'; export * from './CreateTopicDestinationRouteRequest'; export * from './CreateTopicRequest'; +export * from './CustomAuthConnectRequest'; +export * from './CustomAuthConnectResponse'; +export * from './CustomAuthConnectResponseOneOf'; +export * from './CustomAuthConnectResponseOneOf1'; export * from './CustomAuthHeader'; export * from './CustomAuthInput'; +export * from './CustomAuthInputOption'; export * from './CustomAuthOpts'; export * from './CustomAuthQueryParam'; export * from './DefaultPeriod'; @@ -175,6 +180,7 @@ export * from './ReadConfig'; export * from './ReadConfigAllOf'; export * from './ReadConfigObject'; export * from './ReadFilter'; +export * from './RedirectResponse'; export * from './Revision'; export * from './SearchOperators'; export * from './SearchSupport'; From 9faa3c33a30b9cccc03f5cf426c5f81a6926f5f8 Mon Sep 17 00:00:00 2001 From: RajatPawar <18614743+RajatPawar@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:51:28 -0700 Subject: [PATCH 2/7] feat(custom-auth): multi-step custom auth flow (redirect + resume) [ENG-4086] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the consumer-facing driver for providers whose ProviderInfo has customOpts.multiStep. It POSTs the form to /custom-auth/connect and, while the server returns a redirect, opens it in a popup and resumes with the params the callback forwards, until the server returns a connection. Add-only, no changes to the existing single-step custom auth: - MultiStep/MultiStepCustomAuthFlow.tsx: the popup + postMessage resume loop. - MultiStep/MultiStepCustomAuthContent.tsx: a fieldType-aware form (text/password/select) rendered from customOpts.inputs. - form/Select: a native select form primitive (reuses the shared input styles). - useCustomAuthConnectMutation: thin react-query wrapper over connectionApi. - ProtectedConnectionLayout: the only existing file touched — an additive sub-branch that routes custom+multiStep here; every current provider (none set multiStep) still hits the unchanged CustomAuthFlow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../layout/ProtectedConnectionLayout.tsx | 8 + .../MultiStep/MultiStepCustomAuthContent.tsx | 192 ++++++++++++++++++ .../MultiStep/MultiStepCustomAuthFlow.tsx | 151 ++++++++++++++ src/components/form/Select/index.tsx | 25 +++ .../mutation/useCustomAuthConnectMutation.ts | 14 ++ 5 files changed, 390 insertions(+) create mode 100644 src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthContent.tsx create mode 100644 src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthFlow.tsx create mode 100644 src/components/form/Select/index.tsx create mode 100644 src/hooks/mutation/useCustomAuthConnectMutation.ts diff --git a/src/components/Configure/layout/ProtectedConnectionLayout.tsx b/src/components/Configure/layout/ProtectedConnectionLayout.tsx index b150e1ee7..3e74c7434 100644 --- a/src/components/Configure/layout/ProtectedConnectionLayout.tsx +++ b/src/components/Configure/layout/ProtectedConnectionLayout.tsx @@ -10,6 +10,7 @@ import { handleServerError } from "src/utils/handleServerError"; import { ApiKeyAuthFlow } from "components/auth/ApiKeyAuth/ApiKeyAuthFlow"; import { BasicAuthFlow } from "components/auth/BasicAuth/BasicAuthFlow"; import { CustomAuthFlow } from "components/auth/CustomAuth/CustomAuthFlow"; +import { MultiStepCustomAuthFlow } from "components/auth/CustomAuth/MultiStep/MultiStepCustomAuthFlow"; import { NoAuthFlow } from "components/auth/NoAuth/NoAuthFlow"; import { OauthFlow } from "components/auth/Oauth/OauthFlow/OauthFlow"; import { @@ -202,6 +203,13 @@ export function ProtectedConnectionLayout({ } if (providerInfo.authType === "custom") { + if (providerInfo.customOpts?.multiStep) { + return ( + + {children} + + ); + } return {children}; } diff --git a/src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthContent.tsx b/src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthContent.tsx new file mode 100644 index 000000000..f745bb989 --- /dev/null +++ b/src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthContent.tsx @@ -0,0 +1,192 @@ +import { useState } from "react"; +import { + CustomAuthInput, + CustomAuthInputFieldTypeEnum, + MetadataItemInput, + ProviderInfo, +} from "@generated/api/src"; +import { AuthErrorAlert } from "src/components/auth/AuthErrorAlert/AuthErrorAlert"; +import { FormComponent } from "src/components/form"; +import { Select } from "src/components/form/Select"; +import { Button } from "src/components/ui-base/Button"; +import { + AuthCardLayout, + AuthTitle, +} from "src/layout/AuthCardLayout/AuthCardLayout"; + +import { MetadataInput } from "components/auth/MetadataInput"; +import { DocsHelperTextHeader } from "components/Docs/DocsHelperTextMinimal"; + +import { + getProviderMetadata, + isProviderMetadataValid, +} from "../../providerMetadata"; +import { CustomAuthFormData } from "../LandingContentProps"; + +type CustomInputChangeHandler = ( + event: React.FormEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, +) => void; + +// Renders a single custom-auth input by its fieldType. Defaults to a masked +// password field when fieldType is absent. +function CustomInputField({ + input, + value, + onChange, +}: { + input: CustomAuthInput; + value: string; + onChange: CustomInputChangeHandler; +}) { + switch (input.fieldType) { + case CustomAuthInputFieldTypeEnum.FieldTypeSelect: + return ( + + ); + case CustomAuthInputFieldTypeEnum.FieldTypeText: + return ( + + ); + default: + return ( + + ); + } +} + +type MultiStepCustomAuthFormProps = { + providerInfo: ProviderInfo; + handleSubmit: (form: CustomAuthFormData) => void; + isButtonDisabled?: boolean; + metadataInputs: MetadataItemInput[]; +}; + +function MultiStepCustomAuthForm({ + providerInfo, + handleSubmit, + isButtonDisabled, + metadataInputs, +}: MultiStepCustomAuthFormProps) { + const [formData, setFormData] = useState>({}); + + const handleChange: CustomInputChangeHandler = (event) => { + const { name, value } = event.currentTarget; + setFormData((prevData) => ({ ...prevData, [name]: value.trim() })); + }; + + const isMetadataValid = isProviderMetadataValid(metadataInputs, formData); + const isSubmitDisabled = isButtonDisabled || !isMetadataValid; + const customInputs = providerInfo.customOpts?.inputs || []; + + const onHandleSubmit = () => { + const metadata = getProviderMetadata(metadataInputs, formData); + const customAuthFields = Object.fromEntries( + customInputs.map((input) => [input.name, formData[input.name]]), + ); + + handleSubmit({ + customAuth: customAuthFields, + providerMetadata: metadata, + }); + }; + + return ( +
+ {customInputs.map((input) => ( +
+ + +
+ ))} + {metadataInputs.map((metadata: MetadataItemInput) => ( + + ))} + +
+ ); +} + +type MultiStepCustomAuthContentProps = { + providerInfo: ProviderInfo; + handleSubmit: (form: CustomAuthFormData) => void; + error: string | null; + isButtonDisabled?: boolean; + metadataInputs: MetadataItemInput[]; +}; + +export function MultiStepCustomAuthContent({ + providerInfo, + handleSubmit, + error, + isButtonDisabled, + metadataInputs, +}: MultiStepCustomAuthContentProps) { + return ( + + {`Set up ${providerInfo.displayName} integration`} + + + + ); +} diff --git a/src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthFlow.tsx b/src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthFlow.tsx new file mode 100644 index 000000000..eed4c2179 --- /dev/null +++ b/src/components/auth/CustomAuth/MultiStep/MultiStepCustomAuthFlow.tsx @@ -0,0 +1,151 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { + CustomAuthConnectResponse, + CustomAuthConnectResponseOneOf, + RedirectResponse, +} from "@generated/api/src"; +import { useQueryClient } from "@tanstack/react-query"; +import { useAmpersandProviderProps } from "src/context/AmpersandContextProvider"; +import { useCustomAuthConnectMutation } from "src/hooks/mutation/useCustomAuthConnectMutation"; +import { AMP_SERVER } from "src/services/api"; +import { handleServerError } from "src/utils/handleServerError"; + +import { CustomAuthFlowProps } from "../CustomAuthFlowProps"; +import { CustomAuthFormData } from "../LandingContentProps"; + +import { MultiStepCustomAuthContent } from "./MultiStepCustomAuthContent"; + +const POPUP_WIDTH = 600; // px +const POPUP_HEIGHT = 600; // px + +// The custom-auth callback (served by the API) posts this to window.opener. It +// is intentionally dumb: params are the raw query/body params the provider sent, +// which we forward to /custom-auth/connect to resume the flow. +type CustomAuthCallbackMessage = { + source: "ampersand-custom-auth"; + params: Record; +}; + +/** + * Drives a multi-step custom auth flow: submit the form to /custom-auth/connect, + * and while the server returns a redirect, open it in a popup and resume with the + * params the callback forwards, until the server returns a connection. + */ +export function MultiStepCustomAuthFlow({ + providerInfo, + consumerRef, + consumerName, + groupRef, + groupName, + children, + selectedConnection, + metadataInputs, + moduleError, +}: CustomAuthFlowProps) { + const { projectIdOrName } = useAmpersandProviderProps(); + const queryClient = useQueryClient(); + const { mutateAsync: customAuthConnectAsync, isPending } = + useCustomAuthConnectMutation(); + const popupRef = useRef(null); + const sessionIdRef = useRef(null); + const [error, setError] = useState(moduleError || null); + + const openPopup = (url: string) => { + const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2; + const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2.5; + const dimensions = `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top}`; + popupRef.current = window.open(url, "CustomAuthPopup", dimensions); + }; + + const handleResult = useCallback( + (res: CustomAuthConnectResponse) => { + const redirect: RedirectResponse | undefined = ( + res as CustomAuthConnectResponseOneOf + ).redirect; + + if (redirect) { + sessionIdRef.current = redirect.sessionId; + openPopup(redirect.url); + return; + } + + // A connection means the flow is complete; refresh the cached list. + sessionIdRef.current = null; + setError(null); + queryClient.invalidateQueries({ queryKey: ["amp", "connections"] }); + }, + [queryClient], + ); + + // One-time listener: the callback popup forwards the provider's params here. + useEffect(() => { + const onMessage = (ev: MessageEvent) => { + // Accept only messages from the API origin (where the callback is served). + if (ev.origin !== AMP_SERVER) return; + if (ev.data?.source !== "ampersand-custom-auth") return; + + const sessionId = sessionIdRef.current; + popupRef.current?.close(); + if (!sessionId) return; + + customAuthConnectAsync({ + customAuthConnectParams: { + projectIdOrName, + sessionId, + callbackParams: ev.data.params, + }, + }) + .then(handleResult) + .catch((err) => handleServerError(err, setError)); + }; + + window.addEventListener("message", onMessage); + return () => window.removeEventListener("message", onMessage); + }, [projectIdOrName, customAuthConnectAsync, handleResult]); + + const onNext = useCallback( + (form: CustomAuthFormData) => { + setError(null); + customAuthConnectAsync({ + customAuthConnectParams: { + projectIdOrName, + provider: providerInfo.name, + groupRef, + groupName, + consumerRef, + consumerName, + customAuth: form.customAuth, + ...(form.providerMetadata && { + providerMetadata: form.providerMetadata, + }), + }, + }) + .then(handleResult) + .catch((err) => handleServerError(err, setError)); + }, + [ + projectIdOrName, + providerInfo.name, + groupRef, + groupName, + consumerRef, + consumerName, + customAuthConnectAsync, + handleResult, + ], + ); + + if (selectedConnection === null) { + return ( + + ); + } + + return children; +} diff --git a/src/components/form/Select/index.tsx b/src/components/form/Select/index.tsx new file mode 100644 index 000000000..a2c9e5e2b --- /dev/null +++ b/src/components/form/Select/index.tsx @@ -0,0 +1,25 @@ +import { SelectHTMLAttributes } from "react"; +import classNames from "classnames"; + +import classes from "../Input/input.module.css"; + +export interface SelectProps extends SelectHTMLAttributes { + id: string; + className?: string; + isError?: boolean; +} + +export function Select({ + id, + className, + isError = false, + ...rest +}: SelectProps) { + const defaultClassName = isError + ? classNames(classes.inputError, classes.input) + : classes.input; + + return ( + - - {(input.options || []).map((option) => ( - - ))} - + ({ + id: option.value, + label: option.label, + value: option.value, + }))} + selectedValue={value || null} + onSelectedItemChange={(item) => + onValueChange(input.name, item?.value ?? "") + } + placeholder="Select an option" + /> ); case CustomAuthInputFieldTypeEnum.FieldTypeText: return ( @@ -96,15 +95,33 @@ function MultiStepCustomAuthForm({ }: MultiStepCustomAuthFormProps) { const [formData, setFormData] = useState>({}); + const setFieldValue = (name: string, value: string) => { + setFormData((prevData) => ({ ...prevData, [name]: value.trim() })); + }; + const handleChange: CustomInputChangeHandler = (event) => { const { name, value } = event.currentTarget; - setFormData((prevData) => ({ ...prevData, [name]: value.trim() })); + setFieldValue(name, value); }; - const isMetadataValid = isProviderMetadataValid(metadataInputs, formData); - const isSubmitDisabled = isButtonDisabled || !isMetadataValid; const customInputs = providerInfo.customOpts?.inputs || []; + // Every custom input must be filled before continuing; a select must resolve to + // one of its declared options, which also rejects the empty placeholder. + const areCustomInputsValid = customInputs.every((input) => { + const value = (formData[input.name] ?? "").trim(); + + if (input.fieldType === CustomAuthInputFieldTypeEnum.FieldTypeSelect) { + return (input.options ?? []).some((option) => option.value === value); + } + + return value !== ""; + }); + + const isMetadataValid = isProviderMetadataValid(metadataInputs, formData); + const isSubmitDisabled = + isButtonDisabled || !isMetadataValid || !areCustomInputsValid; + const onHandleSubmit = () => { const metadata = getProviderMetadata(metadataInputs, formData); const customAuthFields = Object.fromEntries( @@ -140,6 +157,7 @@ function MultiStepCustomAuthForm({ input={input} value={formData[input.name] || ""} onChange={handleChange} + onValueChange={setFieldValue} /> ))} diff --git a/src/components/form/Select/index.tsx b/src/components/form/Select/index.tsx deleted file mode 100644 index a2c9e5e2b..000000000 --- a/src/components/form/Select/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { SelectHTMLAttributes } from "react"; -import classNames from "classnames"; - -import classes from "../Input/input.module.css"; - -export interface SelectProps extends SelectHTMLAttributes { - id: string; - className?: string; - isError?: boolean; -} - -export function Select({ - id, - className, - isError = false, - ...rest -}: SelectProps) { - const defaultClassName = isError - ? classNames(classes.inputError, classes.input) - : classes.input; - - return ( -