diff --git a/docs/03-authentication-design.md b/docs/03-authentication-design.md index acf1a662..4c8ef11e 100644 --- a/docs/03-authentication-design.md +++ b/docs/03-authentication-design.md @@ -622,6 +622,7 @@ window.location.href = '/oauth2/authorization/github' | `POST /api/v1/skills/{ns}/{slug}/archive` | namespace ADMIN 以上 或 owner | `namespace_member.role` 或 `skill.owner_id` | | `POST .../versions/{ver}/rerelease` | namespace ADMIN 以上 或 owner;源版本必须 `PUBLISHED` | `namespace_member.role` 或 `skill.owner_id` + `skill_version.status` | | `DELETE .../versions/{ver}` | namespace ADMIN 以上 或 owner(仅 `DRAFT` / `REJECTED`) | `namespace_member.role` 或 `skill.owner_id` + `skill_version.status` | +| `POST .../versions/{ver}/yank` | namespace ADMIN 以上 或 owner;版本必须 `PUBLISHED`;API Token 需 `skill:publish` | `namespace_member.role` 或 `skill.owner_id` + `skill_version.status` | ### 10.3 CLI API diff --git a/docs/06-api-design.md b/docs/06-api-design.md index a0f580ae..1dbd906c 100644 --- a/docs/06-api-design.md +++ b/docs/06-api-design.md @@ -230,6 +230,7 @@ Public API 的可见性规则: | POST | `/api/v1/skills/{namespace}/{slug}/archive` | 归档技能(namespace ADMIN 或 owner) | | POST | `/api/v1/skills/{namespace}/{slug}/unarchive` | 恢复归档(namespace ADMIN 或 owner) | | DELETE | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 删除 DRAFT/REJECTED 版本 | +| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/yank` | 撤回已发布版本(namespace ADMIN 或 owner;可选 body `{"reason"}`;API Token 需 `skill:publish`) | | POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/rerelease` | 从已发布版本重新发出一个新版本(namespace ADMIN 或 owner) | 当前代码中的 skill 生命周期读模型不再依赖 `latestVersionStatus` / `viewingVersionStatus` 一类拼装字段,而统一使用以下 projection: diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java index b590fa22..87e3f8e9 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java @@ -24,7 +24,7 @@ import org.springframework.web.bind.annotation.RestController; /** * Endpoints that mutate skill lifecycle state, including archive, unarchive, - * withdraw-review, delete-version, and rerelease operations. + * withdraw-review, delete-version, yank-version, and rerelease operations. */ @RestController @RequestMapping({"/api/v1/skills", "/api/web/skills"}) @@ -87,6 +87,25 @@ public class SkillLifecycleController extends BaseApiController { AuditRequestContext.from(httpRequest))); } + @PostMapping("/{namespace}/{slug}/versions/{version}/yank") + public ApiResponse yankVersion(@PathVariable String namespace, + @PathVariable String slug, + @PathVariable String version, + @RequestBody(required = false) AdminSkillActionRequest request, + @RequestAttribute("userId") String userId, + @RequestAttribute(value = "userNsRoles", required = false) Map userNsRoles, + HttpServletRequest httpRequest) { + return ok("response.success.updated", + governanceWorkflowAppService.yankVersion( + namespace, + slug, + version, + request, + userId, + userNsRoles, + AuditRequestContext.from(httpRequest))); + } + @PostMapping("/{namespace}/{slug}/versions/{version}/withdraw-review") public ApiResponse withdrawReview(@PathVariable String namespace, @PathVariable String slug, diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/GovernanceWorkflowAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/GovernanceWorkflowAppService.java index c27f2a90..3a29d9f0 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/GovernanceWorkflowAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/GovernanceWorkflowAppService.java @@ -234,6 +234,24 @@ public class GovernanceWorkflowAppService { return skillLifecycleAppService.deleteVersion(namespace, slug, version, userId, userNsRoles, auditContext); } + public SkillLifecycleMutationResponse yankVersion(String namespace, + String slug, + String version, + AdminSkillActionRequest request, + String userId, + Map userNsRoles, + AuditRequestContext auditContext) { + return skillLifecycleAppService.yankVersion( + namespace, + slug, + version, + request, + userId, + userNsRoles, + auditContext + ); + } + public SkillLifecycleMutationResponse withdrawReviewVersion(String namespace, String slug, String version, diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLifecycleAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLifecycleAppService.java index 4aa57b8d..5e38644f 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLifecycleAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLifecycleAppService.java @@ -112,6 +112,28 @@ public class SkillLifecycleAppService { return new SkillLifecycleMutationResponse(skill.getId(), skillVersion.getId(), "DELETE_VERSION", version); } + @Transactional + public SkillLifecycleMutationResponse yankVersion(String namespace, + String slug, + String version, + AdminSkillActionRequest request, + String userId, + Map userNamespaceRoles, + AuditRequestContext auditContext) { + Skill skill = findSkill(namespace, slug, userId); + SkillVersion skillVersion = findVersion(skill.getId(), version); + SkillVersion yanked = skillGovernanceService.yankVersion( + skill, + skillVersion, + userId, + normalizeRoles(userNamespaceRoles), + auditContext.clientIp(), + auditContext.userAgent(), + request != null ? request.reason() : null + ); + return new SkillLifecycleMutationResponse(skill.getId(), skillVersion.getId(), "YANK", yanked.getStatus().name()); + } + @Transactional public SkillLifecycleMutationResponse withdrawReview(String namespace, String slug, diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java index bef8c274..44215f5d 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java @@ -2,6 +2,7 @@ package com.iflytek.skillhub.controller.portal; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -160,6 +161,76 @@ class SkillLifecycleControllerTest { .andExpect(jsonPath("$.data.status").value("1.0.0")); } + @Test + void yankVersion_returnsUnifiedEnvelope() throws Exception { + Namespace namespace = new Namespace("global", "Global", "owner"); + setNamespaceId(namespace, 1L); + Skill skill = new Skill(1L, "demo-skill", "owner", SkillVisibility.PUBLIC); + setSkillId(skill, 1L); + SkillVersion version = new SkillVersion(1L, "1.2.3", "owner"); + setSkillVersionId(version, 2L); + version.setStatus(SkillVersionStatus.PUBLISHED); + SkillVersion yanked = new SkillVersion(1L, "1.2.3", "owner"); + setSkillVersionId(yanked, 2L); + yanked.setStatus(SkillVersionStatus.YANKED); + + given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); + given(skillSlugResolutionService.resolve(1L, "demo-skill", "usr_1", SkillSlugResolutionService.Preference.CURRENT_USER)) + .willReturn(skill); + given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.2.3")).willReturn(java.util.Optional.of(version)); + given(skillGovernanceService.yankVersion( + eq(skill), eq(version), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class), eq("broken"))) + .willReturn(yanked); + + mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/yank") + .requestAttr("userId", "usr_1") + .requestAttr("userNsRoles", java.util.Map.of(1L, NamespaceRole.ADMIN)) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"reason\":\"broken\"}") + .with(user("usr_1")) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.skillId").value(1)) + .andExpect(jsonPath("$.data.versionId").value(2)) + .andExpect(jsonPath("$.data.action").value("YANK")) + .andExpect(jsonPath("$.data.status").value("YANKED")); + + verify(skillGovernanceService).yankVersion( + eq(skill), eq(version), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class), eq("broken")); + } + + @Test + void yankVersion_acceptsMissingBodyAndPassesNullReason() throws Exception { + Namespace namespace = new Namespace("global", "Global", "owner"); + setNamespaceId(namespace, 1L); + Skill skill = new Skill(1L, "demo-skill", "owner", SkillVisibility.PUBLIC); + setSkillId(skill, 1L); + SkillVersion version = new SkillVersion(1L, "1.2.3", "owner"); + setSkillVersionId(version, 2L); + version.setStatus(SkillVersionStatus.PUBLISHED); + SkillVersion yanked = new SkillVersion(1L, "1.2.3", "owner"); + setSkillVersionId(yanked, 2L); + yanked.setStatus(SkillVersionStatus.YANKED); + + given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); + given(skillSlugResolutionService.resolve(1L, "demo-skill", "usr_1", SkillSlugResolutionService.Preference.CURRENT_USER)) + .willReturn(skill); + given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.2.3")).willReturn(java.util.Optional.of(version)); + given(skillGovernanceService.yankVersion( + eq(skill), eq(version), eq("usr_1"), anyMap(), nullable(String.class), nullable(String.class), isNull())) + .willReturn(yanked); + + mockMvc.perform(post("/api/v1/skills/global/demo-skill/versions/1.2.3/yank") + .requestAttr("userId", "usr_1") + .with(user("usr_1")) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.action").value("YANK")) + .andExpect(jsonPath("$.data.status").value("YANKED")); + } + @Test void withdrawReview_returnsUnifiedEnvelope() throws Exception { Namespace namespace = new Namespace("global", "Global", "owner"); diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java index ee1c52bf..132ca515 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java @@ -258,6 +258,8 @@ public class RouteSecurityPolicyRegistry { ApiTokenPolicy.require(HttpMethod.POST, "/api/web/skills/*/*/submit-review", "skill:publish"), ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills/*/*/confirm-publish", "skill:publish"), ApiTokenPolicy.require(HttpMethod.POST, "/api/web/skills/*/*/confirm-publish", "skill:publish"), + ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills/*/*/versions/*/yank", "skill:publish"), + ApiTokenPolicy.require(HttpMethod.POST, "/api/web/skills/*/*/versions/*/yank", "skill:publish"), ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills", "skill:publish"), ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills/*/publish", "skill:publish"), ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills/*/versions/*/security-audit/retry", "skill:publish"), diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java index bf6ac622..33fa4ec4 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java @@ -166,6 +166,24 @@ class RouteSecurityPolicyRegistryTest { } } + @Test + void authorizeApiToken_requiresPublishScopeForOwnerYankEndpoint() { + for (String prefix : List.of("/api/v1", "/api/web")) { + String path = prefix + "/skills/global/demo-skill/versions/1.2.3/yank"; + var denied = registry.authorizeApiToken("POST", path, Set.of("skill:read", "skill:delete")); + var allowed = registry.authorizeApiToken("POST", path, Set.of("skill:publish")); + + assertFalse(denied.allowed(), path); + assertEquals("skill:publish", denied.requiredScope(), path); + assertTrue(allowed.allowed(), path); + } + } + + @Test + void authorizeApiToken_keepsAdminYankSessionOnly() { + assertFalse(registry.authorizeApiToken("POST", "/api/v1/admin/skills/versions/42/yank", ALL_SCOPES).allowed()); + } + @Test void authorizeApiToken_requiresPublishScopeForOwnerLifecycleEndpoints() { for (String prefix : List.of("/api/v1", "/api/web")) { diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java index e4bfc81c..5737e70e 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java @@ -260,10 +260,40 @@ public class SkillGovernanceService { return savedVersion; } + /** + * Yanks a published version on behalf of a platform admin. Callers are + * expected to have enforced the admin role before invoking this method. + */ @Transactional public SkillVersion yankVersion(Long versionId, String actorUserId, String clientIp, String userAgent, String reason) { SkillVersion version = skillVersionRepository.findById(versionId) .orElseThrow(() -> new DomainNotFoundException("error.skill.version.notFound", versionId)); + return yankVersionInternal(version, actorUserId, clientIp, userAgent, reason); + } + + /** + * Yanks a published version on behalf of the skill owner or a namespace + * ADMIN/OWNER, using the same lifecycle permission rule as archive and + * delete-version. + */ + @Transactional + public SkillVersion yankVersion(Skill skill, + SkillVersion version, + String actorUserId, + Map userNamespaceRoles, + String clientIp, + String userAgent, + String reason) { + assertCanManageLifecycle(skill, actorUserId, userNamespaceRoles); + return yankVersionInternal(version, actorUserId, clientIp, userAgent, reason); + } + + private SkillVersion yankVersionInternal(SkillVersion version, + String actorUserId, + String clientIp, + String userAgent, + String reason) { + Long versionId = version.getId(); if (version.getStatus() != SkillVersionStatus.PUBLISHED) { throw new DomainBadRequestException("error.skill.version.notPublished", version.getVersion()); } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java index 50aa3e4b..dd9e9a75 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java @@ -148,6 +148,7 @@ class SkillGovernanceServiceTest { @Test void yankVersion_setsYankedStatus() { SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); + setField(version, "id", 22L); version.setStatus(SkillVersionStatus.PUBLISHED); given(skillVersionRepository.findById(22L)).willReturn(Optional.of(version)); given(skillVersionRepository.save(version)).willReturn(version); @@ -161,6 +162,72 @@ class SkillGovernanceServiceTest { verify(auditLogService).record("admin", "YANK_SKILL_VERSION", "SKILL_VERSION", 22L, null, "127.0.0.1", "JUnit", "{\"reason\":\"broken\"}"); } + @Test + void yankVersion_allowsSkillOwnerWithoutAdminRole() { + Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); + setField(skill, "id", 2L); + SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); + setField(version, "id", 22L); + version.setStatus(SkillVersionStatus.PUBLISHED); + given(skillVersionRepository.save(version)).willReturn(version); + given(skillRepository.findById(2L)).willReturn(Optional.of(skill)); + + SkillVersion result = service.yankVersion(skill, version, "owner", Map.of(), "127.0.0.1", "JUnit", "broken"); + + assertThat(result.getStatus()).isEqualTo(SkillVersionStatus.YANKED); + assertThat(result.getYankedBy()).isEqualTo("owner"); + assertThat(result.isDownloadReady()).isFalse(); + verify(auditLogService).record("owner", "YANK_SKILL_VERSION", "SKILL_VERSION", 22L, null, "127.0.0.1", "JUnit", "{\"reason\":\"broken\"}"); + verify(eventPublisher).publishEvent(any(com.iflytek.skillhub.domain.event.SkillVersionYankedEvent.class)); + } + + @Test + void yankVersion_allowsNamespaceAdminWhoIsNotOwner() { + Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); + setField(skill, "id", 2L); + SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); + setField(version, "id", 22L); + version.setStatus(SkillVersionStatus.PUBLISHED); + given(skillVersionRepository.save(version)).willReturn(version); + given(skillRepository.findById(2L)).willReturn(Optional.of(skill)); + + SkillVersion result = service.yankVersion( + skill, version, "ns-admin", Map.of(1L, NamespaceRole.ADMIN), "127.0.0.1", "JUnit", null); + + assertThat(result.getStatus()).isEqualTo(SkillVersionStatus.YANKED); + assertThat(result.getYankedBy()).isEqualTo("ns-admin"); + } + + @Test + void yankVersion_rejectsMemberWhoIsNotOwner() { + Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); + setField(skill, "id", 2L); + SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); + setField(version, "id", 22L); + version.setStatus(SkillVersionStatus.PUBLISHED); + + assertThrows(DomainForbiddenException.class, + () -> service.yankVersion(skill, version, "other", Map.of(1L, NamespaceRole.MEMBER), "127.0.0.1", "JUnit", null)); + + assertThat(version.getStatus()).isEqualTo(SkillVersionStatus.PUBLISHED); + verify(skillVersionRepository, never()).save(any()); + verify(auditLogService, never()).record(any(), any(), any(), any(), any(), any(), any(), any()); + } + + @Test + void yankVersion_rejectsUnpublishedVersionForOwner() { + Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); + setField(skill, "id", 2L); + SkillVersion version = new SkillVersion(2L, "1.0.0", "owner"); + setField(version, "id", 22L); + version.setStatus(SkillVersionStatus.DRAFT); + + assertThrows(DomainBadRequestException.class, + () -> service.yankVersion(skill, version, "owner", Map.of(), "127.0.0.1", "JUnit", null)); + + verify(skillVersionRepository, never()).save(any()); + } + @Test void withdrawPendingVersion_demotesVersionToUploaded() { Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); diff --git a/web/src/api/generated/schema.d.ts b/web/src/api/generated/schema.d.ts index a57d765d..d756bff0 100644 --- a/web/src/api/generated/schema.d.ts +++ b/web/src/api/generated/schema.d.ts @@ -1100,6 +1100,38 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/skills/{namespace}/{slug}/versions/{version}/yank": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["yankVersion"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/web/skills/{namespace}/{slug}/versions/{version}/yank": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["yankVersion_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review": { parameters: { query?: never; @@ -2229,7 +2261,7 @@ export interface paths { }; get?: never; put?: never; - post: operations["yankVersion"]; + post: operations["yankVersion_2"]; delete?: never; options?: never; head?: never; @@ -5277,6 +5309,9 @@ export interface components { /** @enum {string} */ visibility?: "PUBLIC" | "NAMESPACE_ONLY" | "PRIVATE"; }; + AdminSkillActionRequest: { + reason?: string; + }; ApiResponseSkillLifecycleMutationResponse: { /** Format: int32 */ code?: number; @@ -5323,9 +5358,6 @@ export interface components { ConfirmPublishRequest: { version: string; }; - AdminSkillActionRequest: { - reason?: string; - }; ApiResponsePublishResponse: { /** Format: int32 */ code?: number; @@ -9583,6 +9615,62 @@ export interface operations { }; }; }; + yankVersion: { + parameters: { + query?: never; + header?: never; + path: { + namespace: string; + slug: string; + version: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["AdminSkillActionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseSkillLifecycleMutationResponse"]; + }; + }; + }; + }; + yankVersion_1: { + parameters: { + query?: never; + header?: never; + path: { + namespace: string; + slug: string; + version: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["AdminSkillActionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseSkillLifecycleMutationResponse"]; + }; + }; + }; + }; withdrawReview: { parameters: { query?: never; @@ -11585,7 +11673,7 @@ export interface operations { }; }; }; - yankVersion: { + yankVersion_2: { parameters: { query?: never; header?: never;