diff --git a/.changelog-pending/2026-07-02T14-59-12-e350eb0b1521b954c25625b4858b2fd14a506cd2.md b/.changelog-pending/2026-07-02T14-59-12-e350eb0b1521b954c25625b4858b2fd14a506cd2.md new file mode 100644 index 00000000..73302038 --- /dev/null +++ b/.changelog-pending/2026-07-02T14-59-12-e350eb0b1521b954c25625b4858b2fd14a506cd2.md @@ -0,0 +1,27 @@ +* [#413](https://github.com/workos/workos-php/pull/413) feat(generated): regenerate from spec (1 change) + + **Features** + * **[pipes](https://workos.com/docs/reference/pipes)**: + * Added model `DataIntegrationCredentialsDto` + * Added model `CustomProviderDefinition` + * Added model `CreateDataIntegration` + * Added model `UpdateCustomProviderDefinition` + * Added model `UpdateDataIntegration` + * Added model `DataIntegration` + * Added model `DataIntegrationList` + * Added model `DataIntegrationListListMetadata` + * Added model `DataIntegrationCredential` + * Added model `DataIntegrationCustomProvider` + * Added enum `DataIntegrationCredentialsType` + * Added enum `CustomProviderDefinitionAuthenticateVia` + * Added enum `UpdateCustomProviderDefinitionAuthenticateVia` + * Added enum `DataIntegrationState` + * Added enum `DataIntegrationCredentialType` + * Added enum `DataIntegrationCustomProviderAuthenticateVia` + * Added endpoint `GET /data-integrations` + * Added endpoint `POST /data-integrations` + * Added endpoint `GET /data-integrations/{slug}` + * Added endpoint `PUT /data-integrations/{slug}` + * Added endpoint `DELETE /data-integrations/{slug}` + * Added endpoint `POST /user_management/users/{user_id}/connected_accounts/{slug}` + * Added endpoint `PUT /user_management/users/{user_id}/connected_accounts/{slug}` diff --git a/.last-synced-sha b/.last-synced-sha index cdc071b3..639e2e87 100644 --- a/.last-synced-sha +++ b/.last-synced-sha @@ -1 +1 @@ -dc04d30b352063b5c97e45de03be5c83629f5412 +4b4e0618779460dbebc1cf5e0f02197c21796d1f diff --git a/.oagen-manifest.json b/.oagen-manifest.json index d999f1e4..45889b3e 100644 --- a/.oagen-manifest.json +++ b/.oagen-manifest.json @@ -1,7 +1,7 @@ { "version": 2, "language": "php", - "generatedAt": "2026-07-01T18:19:15.811Z", + "generatedAt": "2026-07-02T14:58:50.844Z", "files": [ "lib/Resource/ActionAuthenticationDenied.php", "lib/Resource/ActionAuthenticationDeniedData.php", @@ -130,6 +130,7 @@ "lib/Resource/ConnectApplicationRedirectUri.php", "lib/Resource/ConnectedAccount.php", "lib/Resource/ConnectedAccountAuthMethod.php", + "lib/Resource/ConnectedAccountDto.php", "lib/Resource/ConnectedAccountState.php", "lib/Resource/Connection.php", "lib/Resource/ConnectionActivated.php", @@ -161,6 +162,7 @@ "lib/Resource/CreateAuthorizationPermission.php", "lib/Resource/CreateAuthorizationResource.php", "lib/Resource/CreateCORSOrigin.php", + "lib/Resource/CreateDataIntegration.php", "lib/Resource/CreateDataKeyRequest.php", "lib/Resource/CreateDataKeyResponse.php", "lib/Resource/CreateGroup.php", @@ -185,21 +187,30 @@ "lib/Resource/CreateUserPasswordHashType.php", "lib/Resource/CreateWebhookEndpoint.php", "lib/Resource/CreateWebhookEndpointEvents.php", + "lib/Resource/CustomProviderDefinition.php", + "lib/Resource/CustomProviderDefinitionAuthenticateVia.php", + "lib/Resource/DataIntegration.php", "lib/Resource/DataIntegrationAccessTokenResponse.php", "lib/Resource/DataIntegrationAccessTokenResponseAccessToken.php", "lib/Resource/DataIntegrationAccessTokenResponseError.php", "lib/Resource/DataIntegrationAuthorizeUrlResponse.php", "lib/Resource/DataIntegrationConfigurationListResponse.php", "lib/Resource/DataIntegrationConfigurationResponse.php", + "lib/Resource/DataIntegrationCredential.php", + "lib/Resource/DataIntegrationCredentialType.php", "lib/Resource/DataIntegrationCredentials.php", "lib/Resource/DataIntegrationCredentialsCredentialsType.php", + "lib/Resource/DataIntegrationCredentialsDto.php", "lib/Resource/DataIntegrationCredentialsResponse.php", "lib/Resource/DataIntegrationCredentialsResponseCredential.php", + "lib/Resource/DataIntegrationCustomProvider.php", + "lib/Resource/DataIntegrationState.php", "lib/Resource/DataIntegrationsGetDataIntegrationAuthorizeUrlRequest.php", "lib/Resource/DataIntegrationsGetUserTokenRequest.php", "lib/Resource/DataIntegrationsListResponse.php", "lib/Resource/DataIntegrationsListResponseData.php", "lib/Resource/DataIntegrationsListResponseDataConnectedAccount.php", + "lib/Resource/DataIntegrationsListResponseDataConnectedAccountState.php", "lib/Resource/DataIntegrationsListResponseDataOwnership.php", "lib/Resource/DataIntegrationsUpsertApiKeyRequest.php", "lib/Resource/DataIntegrationsVendCredentialsRequest.php", @@ -458,6 +469,8 @@ "lib/Resource/UpdateAuditLogsRetention.php", "lib/Resource/UpdateAuthorizationPermission.php", "lib/Resource/UpdateAuthorizationResource.php", + "lib/Resource/UpdateCustomProviderDefinition.php", + "lib/Resource/UpdateDataIntegration.php", "lib/Resource/UpdateGroup.php", "lib/Resource/UpdateJWTTemplate.php", "lib/Resource/UpdateOAuthApplication.php", @@ -688,6 +701,7 @@ "tests/Fixtures/connect_application_oauth.json", "tests/Fixtures/connect_application_oauth_redirect_uris.json", "tests/Fixtures/connected_account.json", + "tests/Fixtures/connected_account_dto.json", "tests/Fixtures/connection.json", "tests/Fixtures/connection_activated.json", "tests/Fixtures/connection_activated_data.json", @@ -712,6 +726,7 @@ "tests/Fixtures/create_authorization_permission.json", "tests/Fixtures/create_authorization_resource.json", "tests/Fixtures/create_cors_origin.json", + "tests/Fixtures/create_data_integration.json", "tests/Fixtures/create_data_key_request.json", "tests/Fixtures/create_data_key_response.json", "tests/Fixtures/create_group.json", @@ -733,14 +748,19 @@ "tests/Fixtures/create_user_invite_options.json", "tests/Fixtures/create_user_organization_membership.json", "tests/Fixtures/create_webhook_endpoint.json", + "tests/Fixtures/custom_provider_definition.json", + "tests/Fixtures/data_integration.json", "tests/Fixtures/data_integration_access_token_response.json", "tests/Fixtures/data_integration_access_token_response_access_token.json", "tests/Fixtures/data_integration_authorize_url_response.json", "tests/Fixtures/data_integration_configuration_list_response.json", "tests/Fixtures/data_integration_configuration_response.json", + "tests/Fixtures/data_integration_credential.json", "tests/Fixtures/data_integration_credentials.json", + "tests/Fixtures/data_integration_credentials_dto.json", "tests/Fixtures/data_integration_credentials_response.json", "tests/Fixtures/data_integration_credentials_response_credential.json", + "tests/Fixtures/data_integration_custom_provider.json", "tests/Fixtures/data_integrations_get_data_integration_authorize_url_request.json", "tests/Fixtures/data_integrations_get_user_token_request.json", "tests/Fixtures/data_integrations_list_response.json", @@ -869,6 +889,7 @@ "tests/Fixtures/list_connect_application.json", "tests/Fixtures/list_connection.json", "tests/Fixtures/list_cors_origin_response.json", + "tests/Fixtures/list_data_integration.json", "tests/Fixtures/list_directory.json", "tests/Fixtures/list_directory_group.json", "tests/Fixtures/list_directory_user_with_groups.json", @@ -1011,6 +1032,8 @@ "tests/Fixtures/update_audit_logs_retention.json", "tests/Fixtures/update_authorization_permission.json", "tests/Fixtures/update_authorization_resource.json", + "tests/Fixtures/update_custom_provider_definition.json", + "tests/Fixtures/update_data_integration.json", "tests/Fixtures/update_group.json", "tests/Fixtures/update_jwt_template.json", "tests/Fixtures/update_oauth_application.json", @@ -1890,6 +1913,38 @@ "GET /user_management/redirect_uris": { "sdkMethod": "listRedirectUris", "service": "userManagement" + }, + "GET /data-integrations": { + "sdkMethod": "listDataIntegrations", + "service": "pipes" + }, + "POST /data-integrations": { + "sdkMethod": "createDataIntegration", + "service": "pipes" + }, + "GET /data-integrations/{slug}": { + "sdkMethod": "getDataIntegration", + "service": "pipes" + }, + "PUT /data-integrations/{slug}": { + "sdkMethod": "updateDataIntegration", + "service": "pipes" + }, + "DELETE /data-integrations/{slug}": { + "sdkMethod": "deleteDataIntegration", + "service": "pipes" + }, + "POST /user_management/radar_challenges": { + "sdkMethod": "createRadarChallenge", + "service": "userManagement" + }, + "POST /user_management/users/{user_id}/connected_accounts/{slug}": { + "sdkMethod": "createUserConnectedAccount", + "service": "pipes" + }, + "PUT /user_management/users/{user_id}/connected_accounts/{slug}": { + "sdkMethod": "updateUserConnectedAccount", + "service": "pipes" } } } diff --git a/lib/Resource/ConnectedAccountDto.php b/lib/Resource/ConnectedAccountDto.php new file mode 100644 index 00000000..8e388ba7 --- /dev/null +++ b/lib/Resource/ConnectedAccountDto.php @@ -0,0 +1,51 @@ +|null + */ + public ?array $scopes = null, + /** Explicitly set the state of the connected account. When omitted, the state is derived from the token combination provided. */ + public ?ConnectedAccountState $state = null, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + accessToken: $data['access_token'] ?? null, + refreshToken: $data['refresh_token'] ?? null, + expiresAt: isset($data['expires_at']) ? new \DateTimeImmutable($data['expires_at']) : null, + scopes: $data['scopes'] ?? null, + state: isset($data['state']) ? ConnectedAccountState::from($data['state']) : null, + ); + } + + public function toArray(): array + { + return [ + 'access_token' => $this->accessToken, + 'refresh_token' => $this->refreshToken, + 'expires_at' => $this->expiresAt?->format(\DateTimeInterface::RFC3339_EXTENDED), + 'scopes' => $this->scopes, + 'state' => $this->state?->value, + ]; + } +} diff --git a/lib/Resource/ConnectedAccountState.php b/lib/Resource/ConnectedAccountState.php index 2c18c9d4..98853712 100644 --- a/lib/Resource/ConnectedAccountState.php +++ b/lib/Resource/ConnectedAccountState.php @@ -10,5 +10,4 @@ enum ConnectedAccountState: string { case Connected = 'connected'; case NeedsReauthorization = 'needs_reauthorization'; - case Disconnected = 'disconnected'; } diff --git a/lib/Resource/CreateDataIntegration.php b/lib/Resource/CreateDataIntegration.php new file mode 100644 index 00000000..16c05eee --- /dev/null +++ b/lib/Resource/CreateDataIntegration.php @@ -0,0 +1,55 @@ +|null + */ + public ?array $scopes = null, + /** The credentials to configure for the Data Integration. Required for both built-in and custom providers. */ + public ?DataIntegrationCredentialsDto $credentials = null, + /** The OAuth definition for a custom provider. Supply this to define a custom provider; omit it to create an integration for a built-in provider. */ + public ?CustomProviderDefinition $customProvider = null, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + provider: $data['provider'], + description: $data['description'] ?? null, + enabled: $data['enabled'] ?? null, + scopes: $data['scopes'] ?? null, + credentials: isset($data['credentials']) ? DataIntegrationCredentialsDto::fromArray($data['credentials']) : null, + customProvider: isset($data['custom_provider']) ? CustomProviderDefinition::fromArray($data['custom_provider']) : null, + ); + } + + public function toArray(): array + { + return [ + 'provider' => $this->provider, + 'description' => $this->description, + 'enabled' => $this->enabled, + 'scopes' => $this->scopes, + 'credentials' => $this->credentials?->toArray(), + 'custom_provider' => $this->customProvider?->toArray(), + ]; + } +} diff --git a/lib/Resource/CustomProviderDefinition.php b/lib/Resource/CustomProviderDefinition.php new file mode 100644 index 00000000..6b97d21d --- /dev/null +++ b/lib/Resource/CustomProviderDefinition.php @@ -0,0 +1,75 @@ +|null + */ + public ?array $additionalAuthorizationParameters = null, + /** The Content-Type used when exchanging the token request. */ + public ?string $tokenBodyContentType = null, + /** How client credentials are sent when exchanging authorization codes and refreshing tokens. */ + public ?CustomProviderDefinitionAuthenticateVia $authenticateVia = null, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + name: $data['name'], + authorizationUrl: $data['authorization_url'], + tokenUrl: $data['token_url'], + refreshTokenUrl: $data['refresh_token_url'] ?? null, + pkceEnabled: $data['pkce_enabled'] ?? null, + requestScopeSeparator: $data['request_scope_separator'] ?? null, + scopesRequired: $data['scopes_required'] ?? null, + clientSecretRequired: $data['client_secret_required'] ?? null, + additionalAuthorizationParameters: $data['additional_authorization_parameters'] ?? null, + tokenBodyContentType: $data['token_body_content_type'] ?? null, + authenticateVia: isset($data['authenticate_via']) ? CustomProviderDefinitionAuthenticateVia::from($data['authenticate_via']) : null, + ); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'authorization_url' => $this->authorizationUrl, + 'token_url' => $this->tokenUrl, + 'refresh_token_url' => $this->refreshTokenUrl, + 'pkce_enabled' => $this->pkceEnabled, + 'request_scope_separator' => $this->requestScopeSeparator, + 'scopes_required' => $this->scopesRequired, + 'client_secret_required' => $this->clientSecretRequired, + 'additional_authorization_parameters' => $this->additionalAuthorizationParameters, + 'token_body_content_type' => $this->tokenBodyContentType, + 'authenticate_via' => $this->authenticateVia?->value, + ]; + } +} diff --git a/lib/Resource/CustomProviderDefinitionAuthenticateVia.php b/lib/Resource/CustomProviderDefinitionAuthenticateVia.php new file mode 100644 index 00000000..852420ea --- /dev/null +++ b/lib/Resource/CustomProviderDefinitionAuthenticateVia.php @@ -0,0 +1,13 @@ +|null + */ + public ?array $scopes, + /** The OAuth redirect URI to register with the provider when configuring the custom application. */ + public string $redirectUri, + /** The credentials configured for the Data Integration. */ + public DataIntegrationCredential $credentials, + /** The OAuth definition when this is a custom provider; `null` for built-in providers. */ + public ?DataIntegrationCustomProvider $customProvider, + /** An ISO 8601 timestamp. */ + public \DateTimeImmutable $createdAt, + /** An ISO 8601 timestamp. */ + public \DateTimeImmutable $updatedAt, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + object: $data['object'] ?? 'data_integration', + id: $data['id'], + slug: $data['slug'], + integrationType: $data['integration_type'], + description: $data['description'] ?? null, + enabled: $data['enabled'], + state: DataIntegrationState::from($data['state']), + scopes: $data['scopes'] ?? null, + redirectUri: $data['redirect_uri'], + credentials: DataIntegrationCredential::fromArray($data['credentials']), + customProvider: isset($data['custom_provider']) ? DataIntegrationCustomProvider::fromArray($data['custom_provider']) : null, + createdAt: new \DateTimeImmutable($data['created_at']), + updatedAt: new \DateTimeImmutable($data['updated_at']), + ); + } + + public function toArray(): array + { + return [ + 'object' => $this->object, + 'id' => $this->id, + 'slug' => $this->slug, + 'integration_type' => $this->integrationType, + 'description' => $this->description, + 'enabled' => $this->enabled, + 'state' => $this->state->value, + 'scopes' => $this->scopes, + 'redirect_uri' => $this->redirectUri, + 'credentials' => $this->credentials->toArray(), + 'custom_provider' => $this->customProvider?->toArray(), + 'created_at' => $this->createdAt->format(\DateTimeInterface::RFC3339_EXTENDED), + 'updated_at' => $this->updatedAt->format(\DateTimeInterface::RFC3339_EXTENDED), + ]; + } +} diff --git a/lib/Resource/DataIntegrationCredential.php b/lib/Resource/DataIntegrationCredential.php new file mode 100644 index 00000000..dd0b67ee --- /dev/null +++ b/lib/Resource/DataIntegrationCredential.php @@ -0,0 +1,41 @@ + $this->type->value, + 'client_id' => $this->clientId, + 'redacted_client_secret' => $this->redactedClientSecret, + ]; + } +} diff --git a/lib/Resource/DataIntegrationCredentialType.php b/lib/Resource/DataIntegrationCredentialType.php new file mode 100644 index 00000000..956c7e94 --- /dev/null +++ b/lib/Resource/DataIntegrationCredentialType.php @@ -0,0 +1,13 @@ + $this->type->value, + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } +} diff --git a/lib/Resource/DataIntegrationCustomProvider.php b/lib/Resource/DataIntegrationCustomProvider.php new file mode 100644 index 00000000..aac40e90 --- /dev/null +++ b/lib/Resource/DataIntegrationCustomProvider.php @@ -0,0 +1,75 @@ + + */ + public array $additionalAuthorizationParameters, + /** The Content-Type used when exchanging the token request. */ + public string $tokenBodyContentType, + /** How client credentials are sent when exchanging authorization codes and refreshing tokens. */ + public CustomProviderDefinitionAuthenticateVia $authenticateVia, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + name: $data['name'], + authorizationUrl: $data['authorization_url'] ?? null, + tokenUrl: $data['token_url'] ?? null, + refreshTokenUrl: $data['refresh_token_url'] ?? null, + pkceEnabled: $data['pkce_enabled'], + requestScopeSeparator: $data['request_scope_separator'], + scopesRequired: $data['scopes_required'], + clientSecretRequired: $data['client_secret_required'], + additionalAuthorizationParameters: $data['additional_authorization_parameters'], + tokenBodyContentType: $data['token_body_content_type'], + authenticateVia: CustomProviderDefinitionAuthenticateVia::from($data['authenticate_via']), + ); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'authorization_url' => $this->authorizationUrl, + 'token_url' => $this->tokenUrl, + 'refresh_token_url' => $this->refreshTokenUrl, + 'pkce_enabled' => $this->pkceEnabled, + 'request_scope_separator' => $this->requestScopeSeparator, + 'scopes_required' => $this->scopesRequired, + 'client_secret_required' => $this->clientSecretRequired, + 'additional_authorization_parameters' => $this->additionalAuthorizationParameters, + 'token_body_content_type' => $this->tokenBodyContentType, + 'authenticate_via' => $this->authenticateVia->value, + ]; + } +} diff --git a/lib/Resource/DataIntegrationState.php b/lib/Resource/DataIntegrationState.php new file mode 100644 index 00000000..89a35925 --- /dev/null +++ b/lib/Resource/DataIntegrationState.php @@ -0,0 +1,14 @@ +|null + */ + public ?array $additionalAuthorizationParameters = null, + /** The Content-Type used when exchanging the token request. */ + public ?string $tokenBodyContentType = null, + /** How client credentials are sent when exchanging authorization codes and refreshing tokens. */ + public ?CustomProviderDefinitionAuthenticateVia $authenticateVia = null, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + name: $data['name'] ?? null, + authorizationUrl: $data['authorization_url'] ?? null, + tokenUrl: $data['token_url'] ?? null, + refreshTokenUrl: $data['refresh_token_url'] ?? null, + pkceEnabled: $data['pkce_enabled'] ?? null, + requestScopeSeparator: $data['request_scope_separator'] ?? null, + scopesRequired: $data['scopes_required'] ?? null, + clientSecretRequired: $data['client_secret_required'] ?? null, + additionalAuthorizationParameters: $data['additional_authorization_parameters'] ?? null, + tokenBodyContentType: $data['token_body_content_type'] ?? null, + authenticateVia: isset($data['authenticate_via']) ? CustomProviderDefinitionAuthenticateVia::from($data['authenticate_via']) : null, + ); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'authorization_url' => $this->authorizationUrl, + 'token_url' => $this->tokenUrl, + 'refresh_token_url' => $this->refreshTokenUrl, + 'pkce_enabled' => $this->pkceEnabled, + 'request_scope_separator' => $this->requestScopeSeparator, + 'scopes_required' => $this->scopesRequired, + 'client_secret_required' => $this->clientSecretRequired, + 'additional_authorization_parameters' => $this->additionalAuthorizationParameters, + 'token_body_content_type' => $this->tokenBodyContentType, + 'authenticate_via' => $this->authenticateVia?->value, + ]; + } +} diff --git a/lib/Resource/UpdateDataIntegration.php b/lib/Resource/UpdateDataIntegration.php new file mode 100644 index 00000000..8216d996 --- /dev/null +++ b/lib/Resource/UpdateDataIntegration.php @@ -0,0 +1,51 @@ +|null + */ + public ?array $scopes = null, + /** New credentials for the Data Integration. When provided, rotates the stored client secret. */ + public ?DataIntegrationCredentialsDto $credentials = null, + /** Updates to a custom provider's OAuth definition. Only valid for custom-provider integrations. */ + public ?UpdateCustomProviderDefinition $customProvider = null, + ) { + } + + public static function fromArray(array $data): self + { + return new self( + description: $data['description'] ?? null, + enabled: $data['enabled'] ?? null, + scopes: $data['scopes'] ?? null, + credentials: isset($data['credentials']) ? DataIntegrationCredentialsDto::fromArray($data['credentials']) : null, + customProvider: isset($data['custom_provider']) ? UpdateCustomProviderDefinition::fromArray($data['custom_provider']) : null, + ); + } + + public function toArray(): array + { + return [ + 'description' => $this->description, + 'enabled' => $this->enabled, + 'scopes' => $this->scopes, + 'credentials' => $this->credentials?->toArray(), + 'custom_provider' => $this->customProvider?->toArray(), + ]; + } +} diff --git a/lib/Service/Pipes.php b/lib/Service/Pipes.php index 77ec3d85..0bbf941d 100644 --- a/lib/Service/Pipes.php +++ b/lib/Service/Pipes.php @@ -7,6 +7,7 @@ namespace WorkOS\Service; use WorkOS\Resource\ConnectedAccount; +use WorkOS\Resource\DataIntegration; use WorkOS\Resource\DataIntegrationAccessTokenResponse; use WorkOS\Resource\DataIntegrationAuthorizeUrlResponse; use WorkOS\Resource\DataIntegrationCredentialsResponse; @@ -19,6 +20,155 @@ public function __construct( ) { } + /** + * List data integrations + * + * Lists the environment's data integrations configured with `custom` or `organization` credentials, including custom providers. + * @param string|null $before An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. + * @param string|null $after An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. + * @param int|null $limit Upper limit on the number of objects to return, between `1` and `100`. Defaults to 10. + * @param \WorkOS\Resource\PaginationOrder $order Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to "desc". + * @return \WorkOS\PaginatedResponse<\WorkOS\Resource\DataIntegration> + * @throws \WorkOS\Exception\WorkOSException + */ + public function listDataIntegrations( + ?string $before = null, + ?string $after = null, + ?int $limit = null, + \WorkOS\Resource\PaginationOrder $order = \WorkOS\Resource\PaginationOrder::Desc, + ?\WorkOS\RequestOptions $options = null, + ): \WorkOS\PaginatedResponse { + $query = array_filter([ + 'before' => $before, + 'after' => $after, + 'limit' => $limit, + 'order' => $order->value, + ], fn ($v) => $v !== null); + return $this->client->requestPage( + method: 'GET', + path: 'data-integrations', + query: $query, + modelClass: DataIntegration::class, + options: $options, + ); + } + + /** + * Create a data integration + * + * Creates a data integration for a provider. Set `credentials.type` to `custom` to use your own OAuth app credentials, or `organization` to have each organization supply its own. For a built-in provider, pass its slug as `provider`. For a custom provider, pass a new slug plus a `custom_provider` definition. + * @param string $provider The provider to create a Data Integration for. For a built-in provider use its slug (e.g. `github`, `slack`). For a custom provider, this is the new provider slug and `custom_provider` must be supplied. A custom provider slug cannot shadow an existing global provider slug. + * @param string|null $description An optional description of the Data Integration. + * @param bool|null $enabled Whether the Data Integration is enabled. Defaults to `false`. + * @param array|null $scopes The OAuth scopes to request for the Data Integration. Defaults to the provider's configured scopes when omitted. + * @param \WorkOS\Resource\DataIntegrationCredentialsDto|null $credentials The credentials to configure for the Data Integration. Required for both built-in and custom providers. + * @param \WorkOS\Resource\CustomProviderDefinition|null $customProvider The OAuth definition for a custom provider. Supply this to define a custom provider; omit it to create an integration for a built-in provider. + * @return \WorkOS\Resource\DataIntegration + * @throws \WorkOS\Exception\WorkOSException + */ + public function createDataIntegration( + string $provider, + ?string $description = null, + ?bool $enabled = null, + ?array $scopes = null, + ?\WorkOS\Resource\DataIntegrationCredentialsDto $credentials = null, + ?\WorkOS\Resource\CustomProviderDefinition $customProvider = null, + ?\WorkOS\RequestOptions $options = null, + ): \WorkOS\Resource\DataIntegration { + $body = array_filter([ + 'provider' => $provider, + 'description' => $description, + 'enabled' => $enabled, + 'scopes' => $scopes, + 'credentials' => $credentials, + 'custom_provider' => $customProvider, + ], fn ($v) => $v !== null); + $response = $this->client->request( + method: 'POST', + path: 'data-integrations', + body: $body, + options: $options, + ); + return DataIntegration::fromArray($response); + } + + /** + * Get a data integration + * + * Retrieves a data integration by its slug. + * @param string $slug The slug identifier of the data integration. + * @return \WorkOS\Resource\DataIntegration + * @throws \WorkOS\Exception\WorkOSException + */ + public function getDataIntegration( + string $slug, + ?\WorkOS\RequestOptions $options = null, + ): \WorkOS\Resource\DataIntegration { + $response = $this->client->request( + method: 'GET', + path: 'data-integrations/' . rawurlencode($slug), + options: $options, + ); + return DataIntegration::fromArray($response); + } + + /** + * Update a data integration + * + * Updates the description, enabled state, or custom credentials of a data integration. For custom providers, `custom_provider` updates the OAuth definition. + * @param string $slug The slug identifier of the data integration. + * @param string|null $description An optional description of the Data Integration. + * @param bool|null $enabled Whether the Data Integration is enabled. + * @param array|null $scopes The OAuth scopes to request for the Data Integration. Pass `null` to reset to the provider's configured scopes. + * @param \WorkOS\Resource\DataIntegrationCredentialsDto|null $credentials New credentials for the Data Integration. When provided, rotates the stored client secret. + * @param \WorkOS\Resource\UpdateCustomProviderDefinition|null $customProvider Updates to a custom provider's OAuth definition. Only valid for custom-provider integrations. + * @return \WorkOS\Resource\DataIntegration + * @throws \WorkOS\Exception\WorkOSException + */ + public function updateDataIntegration( + string $slug, + ?string $description = null, + ?bool $enabled = null, + ?array $scopes = null, + ?\WorkOS\Resource\DataIntegrationCredentialsDto $credentials = null, + ?\WorkOS\Resource\UpdateCustomProviderDefinition $customProvider = null, + ?\WorkOS\RequestOptions $options = null, + ): \WorkOS\Resource\DataIntegration { + $body = array_filter([ + 'description' => $description, + 'enabled' => $enabled, + 'scopes' => $scopes, + 'credentials' => $credentials, + 'custom_provider' => $customProvider, + ], fn ($v) => $v !== null); + $response = $this->client->request( + method: 'PUT', + path: 'data-integrations/' . rawurlencode($slug), + body: $body, + options: $options, + ); + return DataIntegration::fromArray($response); + } + + /** + * Delete a data integration + * + * Deletes a data integration and all of its connected installations. For a custom provider, also deletes the custom provider definition. + * @param string $slug The slug identifier of the data integration. + * @return void + * @throws \WorkOS\Exception\WorkOSException + */ + public function deleteDataIntegration( + string $slug, + ?\WorkOS\RequestOptions $options = null, + ): void { + $this->client->request( + method: 'DELETE', + path: 'data-integrations/' . rawurlencode($slug), + options: $options, + ); + } + /** * Upsert an API key for a connected account * @@ -169,6 +319,90 @@ public function getUserConnectedAccount( return ConnectedAccount::fromArray($response); } + /** + * Import a connected account + * + * Imports a [connected account](https://workos.com/docs/reference/pipes/connected-account) for a user by providing OAuth tokens directly. Use this to migrate existing connections or set up connections without going through the OAuth flow. + * @param string $userId A [User](https://workos.com/docs/reference/authkit/user) identifier. + * @param string $slug The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + * @param string|null $accessToken The OAuth access token for the connected account. + * @param string|null $refreshToken The OAuth refresh token for the connected account. + * @param \DateTimeImmutable|null $expiresAt The ISO-8601 timestamp when the access token expires. Required when `access_token` is provided for tokens that expire. + * @param array|null $scopes The OAuth scopes granted for this connection. + * @param \WorkOS\Resource\ConnectedAccountState|null $state Explicitly set the state of the connected account. When omitted, the state is derived from the token combination provided. + * @param string|null $organizationId An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + * @return \WorkOS\Resource\ConnectedAccount + * @throws \WorkOS\Exception\WorkOSException + */ + public function createUserConnectedAccount( + string $userId, + string $slug, + ?string $accessToken = null, + ?string $refreshToken = null, + ?\DateTimeImmutable $expiresAt = null, + ?array $scopes = null, + ?\WorkOS\Resource\ConnectedAccountState $state = null, + ?string $organizationId = null, + ?\WorkOS\RequestOptions $options = null, + ): \WorkOS\Resource\ConnectedAccount { + $body = array_filter([ + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'expires_at' => $expiresAt?->format(\DateTimeInterface::RFC3339_EXTENDED), + 'scopes' => $scopes, + 'state' => $state?->value, + ], fn ($v) => $v !== null); + $response = $this->client->request( + method: 'POST', + path: 'user_management/users/' . rawurlencode($userId) . '/connected_accounts/' . rawurlencode($slug), + body: $body, + options: $options, + ); + return ConnectedAccount::fromArray($response); + } + + /** + * Update a connected account + * + * Updates a user's [connected account](https://workos.com/docs/reference/pipes/connected-account) tokens, scopes, or state for a specific provider. + * @param string $userId A [User](https://workos.com/docs/reference/authkit/user) identifier. + * @param string $slug The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + * @param string|null $accessToken The OAuth access token for the connected account. + * @param string|null $refreshToken The OAuth refresh token for the connected account. + * @param \DateTimeImmutable|null $expiresAt The ISO-8601 timestamp when the access token expires. Required when `access_token` is provided for tokens that expire. + * @param array|null $scopes The OAuth scopes granted for this connection. + * @param \WorkOS\Resource\ConnectedAccountState|null $state Explicitly set the state of the connected account. When omitted, the state is derived from the token combination provided. + * @param string|null $organizationId An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + * @return \WorkOS\Resource\ConnectedAccount + * @throws \WorkOS\Exception\WorkOSException + */ + public function updateUserConnectedAccount( + string $userId, + string $slug, + ?string $accessToken = null, + ?string $refreshToken = null, + ?\DateTimeImmutable $expiresAt = null, + ?array $scopes = null, + ?\WorkOS\Resource\ConnectedAccountState $state = null, + ?string $organizationId = null, + ?\WorkOS\RequestOptions $options = null, + ): \WorkOS\Resource\ConnectedAccount { + $body = array_filter([ + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'expires_at' => $expiresAt?->format(\DateTimeInterface::RFC3339_EXTENDED), + 'scopes' => $scopes, + 'state' => $state?->value, + ], fn ($v) => $v !== null); + $response = $this->client->request( + method: 'PUT', + path: 'user_management/users/' . rawurlencode($userId) . '/connected_accounts/' . rawurlencode($slug), + body: $body, + options: $options, + ); + return ConnectedAccount::fromArray($response); + } + /** * Delete a connected account * diff --git a/tests/Fixtures/connected_account_dto.json b/tests/Fixtures/connected_account_dto.json new file mode 100644 index 00000000..d04b7e83 --- /dev/null +++ b/tests/Fixtures/connected_account_dto.json @@ -0,0 +1,10 @@ +{ + "access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a", + "refresh_token": "ghr_xxxxxxxxxxxxxxxxxxxx", + "expires_at": "2025-12-31T23:59:59.000Z", + "scopes": [ + "repo", + "user:email" + ], + "state": "connected" +} diff --git a/tests/Fixtures/create_data_integration.json b/tests/Fixtures/create_data_integration.json new file mode 100644 index 00000000..8051c136 --- /dev/null +++ b/tests/Fixtures/create_data_integration.json @@ -0,0 +1,29 @@ +{ + "provider": "github", + "description": "Production GitHub app", + "enabled": true, + "scopes": [ + "repo", + "read:org" + ], + "credentials": { + "type": "custom", + "client_id": "Iv1.abc123", + "client_secret": "secret_…" + }, + "custom_provider": { + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" + } +} diff --git a/tests/Fixtures/custom_provider_definition.json b/tests/Fixtures/custom_provider_definition.json new file mode 100644 index 00000000..9909168b --- /dev/null +++ b/tests/Fixtures/custom_provider_definition.json @@ -0,0 +1,15 @@ +{ + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" +} diff --git a/tests/Fixtures/data_integration.json b/tests/Fixtures/data_integration.json new file mode 100644 index 00000000..997363ae --- /dev/null +++ b/tests/Fixtures/data_integration.json @@ -0,0 +1,36 @@ +{ + "object": "data_integration", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "github", + "integration_type": "github", + "description": "Production GitHub app", + "enabled": true, + "state": "valid", + "scopes": [ + "repo", + "read:org" + ], + "redirect_uri": "https://api.workos.com/data-integrations/github/dik_01EHZNVPK3SFK441A1RGBFSHRT/callback", + "credentials": { + "type": "custom", + "client_id": "Iv1.abc123", + "redacted_client_secret": "6789" + }, + "custom_provider": { + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/Fixtures/data_integration_credential.json b/tests/Fixtures/data_integration_credential.json new file mode 100644 index 00000000..60dfe77a --- /dev/null +++ b/tests/Fixtures/data_integration_credential.json @@ -0,0 +1,5 @@ +{ + "type": "custom", + "client_id": "Iv1.abc123", + "redacted_client_secret": "6789" +} diff --git a/tests/Fixtures/data_integration_credentials_dto.json b/tests/Fixtures/data_integration_credentials_dto.json new file mode 100644 index 00000000..e5098765 --- /dev/null +++ b/tests/Fixtures/data_integration_credentials_dto.json @@ -0,0 +1,5 @@ +{ + "type": "custom", + "client_id": "Iv1.abc123", + "client_secret": "secret_…" +} diff --git a/tests/Fixtures/data_integration_custom_provider.json b/tests/Fixtures/data_integration_custom_provider.json new file mode 100644 index 00000000..9909168b --- /dev/null +++ b/tests/Fixtures/data_integration_custom_provider.json @@ -0,0 +1,15 @@ +{ + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" +} diff --git a/tests/Fixtures/list_data_integration.json b/tests/Fixtures/list_data_integration.json new file mode 100644 index 00000000..13639f77 --- /dev/null +++ b/tests/Fixtures/list_data_integration.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "object": "data_integration", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "github", + "integration_type": "github", + "description": "Production GitHub app", + "enabled": true, + "state": "valid", + "scopes": [ + "repo", + "read:org" + ], + "redirect_uri": "https://api.workos.com/data-integrations/github/dik_01EHZNVPK3SFK441A1RGBFSHRT/callback", + "credentials": { + "type": "custom", + "client_id": "Iv1.abc123", + "redacted_client_secret": "6789" + }, + "custom_provider": { + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/Fixtures/update_custom_provider_definition.json b/tests/Fixtures/update_custom_provider_definition.json new file mode 100644 index 00000000..9909168b --- /dev/null +++ b/tests/Fixtures/update_custom_provider_definition.json @@ -0,0 +1,15 @@ +{ + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" +} diff --git a/tests/Fixtures/update_data_integration.json b/tests/Fixtures/update_data_integration.json new file mode 100644 index 00000000..8996a29c --- /dev/null +++ b/tests/Fixtures/update_data_integration.json @@ -0,0 +1,28 @@ +{ + "description": "Production GitHub app", + "enabled": true, + "scopes": [ + "repo", + "read:org" + ], + "credentials": { + "type": "custom", + "client_id": "Iv1.abc123", + "client_secret": "secret_…" + }, + "custom_provider": { + "name": "My OAuth App", + "authorization_url": "https://provider.example.com/oauth/authorize", + "token_url": "https://provider.example.com/oauth/token", + "refresh_token_url": "https://provider.example.com/oauth/token", + "pkce_enabled": true, + "request_scope_separator": " ", + "scopes_required": false, + "client_secret_required": true, + "additional_authorization_parameters": { + "prompt": "consent" + }, + "token_body_content_type": "application/x-www-form-urlencoded", + "authenticate_via": "request_body" + } +} diff --git a/tests/Service/PipesTest.php b/tests/Service/PipesTest.php index e8a7d2d2..4a43d636 100644 --- a/tests/Service/PipesTest.php +++ b/tests/Service/PipesTest.php @@ -13,6 +13,75 @@ class PipesTest extends TestCase { use TestHelper; + public function testListDataIntegrations(): void + { + $fixture = $this->loadFixture('list_data_integration'); + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->listDataIntegrations(before: 'test_value', after: 'test_value', limit: 1, order: \WorkOS\Resource\PaginationOrder::Normal); + $this->assertInstanceOf(\WorkOS\PaginatedResponse::class, $result); + $request = $this->getLastRequest(); + $this->assertSame('GET', $request->getMethod()); + $this->assertStringEndsWith('data-integrations', $request->getUri()->getPath()); + parse_str($request->getUri()->getQuery(), $query); + $this->assertSame('test_value', $query['before']); + $this->assertSame('test_value', $query['after']); + $this->assertArrayHasKey('limit', $query); + $this->assertSame('normal', $query['order']); + } + + public function testCreateDataIntegration(): void + { + $fixture = $this->loadFixture('data_integration'); + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->createDataIntegration(provider: 'test_value'); + $this->assertInstanceOf(\WorkOS\Resource\DataIntegration::class, $result); + $this->assertSame($fixture['id'], $result->id); + $this->assertSame($fixture['slug'], $result->slug); + $this->assertIsArray($result->toArray()); + $request = $this->getLastRequest(); + $this->assertSame('POST', $request->getMethod()); + $this->assertStringEndsWith('data-integrations', $request->getUri()->getPath()); + $body = json_decode((string) $request->getBody(), true); + $this->assertSame('test_value', $body['provider']); + } + + public function testGetDataIntegration(): void + { + $fixture = $this->loadFixture('data_integration'); + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->getDataIntegration('test_slug'); + $this->assertInstanceOf(\WorkOS\Resource\DataIntegration::class, $result); + $this->assertSame($fixture['id'], $result->id); + $this->assertSame($fixture['slug'], $result->slug); + $this->assertIsArray($result->toArray()); + $request = $this->getLastRequest(); + $this->assertSame('GET', $request->getMethod()); + $this->assertStringEndsWith('data-integrations/test_slug', $request->getUri()->getPath()); + } + + public function testUpdateDataIntegration(): void + { + $fixture = $this->loadFixture('data_integration'); + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->updateDataIntegration('test_slug'); + $this->assertInstanceOf(\WorkOS\Resource\DataIntegration::class, $result); + $this->assertSame($fixture['id'], $result->id); + $this->assertSame($fixture['slug'], $result->slug); + $this->assertIsArray($result->toArray()); + $request = $this->getLastRequest(); + $this->assertSame('PUT', $request->getMethod()); + $this->assertStringEndsWith('data-integrations/test_slug', $request->getUri()->getPath()); + } + + public function testDeleteDataIntegration(): void + { + $client = $this->createMockClient([['status' => 204]]); + $client->pipes()->deleteDataIntegration('test_slug'); + $request = $this->getLastRequest(); + $this->assertSame('DELETE', $request->getMethod()); + $this->assertStringEndsWith('data-integrations/test_slug', $request->getUri()->getPath()); + } + public function testUpdateDataIntegrationApiKey(): void { $fixture = $this->loadFixture('connected_account'); @@ -87,6 +156,34 @@ public function testGetUserConnectedAccount(): void $this->assertStringEndsWith('user_management/users/test_user_id/connected_accounts/test_slug', $request->getUri()->getPath()); } + public function testCreateUserConnectedAccount(): void + { + $fixture = $this->loadFixture('connected_account'); + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->createUserConnectedAccount('test_user_id', 'test_slug'); + $this->assertInstanceOf(\WorkOS\Resource\ConnectedAccount::class, $result); + $this->assertSame($fixture['id'], $result->id); + $this->assertSame($fixture['created_at'], $result->createdAt); + $this->assertIsArray($result->toArray()); + $request = $this->getLastRequest(); + $this->assertSame('POST', $request->getMethod()); + $this->assertStringEndsWith('user_management/users/test_user_id/connected_accounts/test_slug', $request->getUri()->getPath()); + } + + public function testUpdateUserConnectedAccount(): void + { + $fixture = $this->loadFixture('connected_account'); + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->updateUserConnectedAccount('test_user_id', 'test_slug'); + $this->assertInstanceOf(\WorkOS\Resource\ConnectedAccount::class, $result); + $this->assertSame($fixture['id'], $result->id); + $this->assertSame($fixture['created_at'], $result->createdAt); + $this->assertIsArray($result->toArray()); + $request = $this->getLastRequest(); + $this->assertSame('PUT', $request->getMethod()); + $this->assertStringEndsWith('user_management/users/test_user_id/connected_accounts/test_slug', $request->getUri()->getPath()); + } + public function testDeleteUserConnectedAccount(): void { $client = $this->createMockClient([['status' => 204]]); @@ -107,4 +204,23 @@ public function testListUserDataProviders(): void $this->assertSame('GET', $request->getMethod()); $this->assertStringEndsWith('user_management/users/test_user_id/data_providers', $request->getUri()->getPath()); } + + public function testPaginationBoundary(): void + { + $fixture = $this->loadFixture('list_data_integration'); + // Ensure cursors are null (first/last page boundary) + $fixture['list_metadata']['before'] = null; + $fixture['list_metadata']['after'] = null; + $client = $this->createMockClient([['status' => 200, 'body' => $fixture]]); + $result = $client->pipes()->listDataIntegrations(); + $this->assertInstanceOf(\WorkOS\PaginatedResponse::class, $result); + // Verify cursors are null on boundary page + $this->assertNull($result->listMetadata['before']); + $this->assertNull($result->listMetadata['after']); + // Iterating should not throw on null cursors + foreach ($result as $item) { + $this->assertNotNull($item); + break; + } + } }