feat(skills): let skill owners yank a published version (#866)
Some checks are pending
Deploy Docs / build (push) Waiting to run
Deploy Docs / Deploy (push) Blocked by required conditions
Security / Dependency Review (push) Waiting to run
Security / CodeQL (java-kotlin) (push) Waiting to run
Security / CodeQL (python) (push) Waiting to run
Security / CodeQL (javascript-typescript) (push) Waiting to run

* feat(auth): expose skill lifecycle routes to API tokens

With an API token, v0.2.19 can remove a whole skill (DELETE
/api/v1/skills/{ns}/{slug} with skill:delete) but cannot archive or
unarchive a skill, nor delete a single draft/rejected version. Those
three routes are opened by AUTHORIZATION_POLICIES (authenticated
fallback) yet have no entry in API_TOKEN_POLICIES, so a bearer request
falls through to "unsupported" and is rejected with 403.

That contradicts the contract written above SESSION_ONLY_ROUTES in
RouteSecurityPolicyRegistry: bearer tokens are rejected on exactly the
listed session-only routes and nowhere else, and anything else the
authorization list opens must be reachable with a token holding the
required scope.

Add API-token policies for both the /api/v1 and /api/web prefixes that
SkillLifecycleController serves:

- POST .../skills/{ns}/{slug}/archive and .../unarchive require
  skill:publish. They are owner-level operations, gated by the same
  assertCanManageLifecycle check as publishing, so they sit at the same
  scope tier.
- DELETE .../skills/{ns}/{slug}/versions/{version} requires
  skill:delete, matching the existing whole-skill delete.

Whole-skill DELETE on /api/web stays session-only as documented; the
new version-delete pattern does not overlap it. No scope allow-list
exists outside the registry (TokenController and ApiTokenScopeService
accept any scope string), so no other change is needed for tokens to
carry these scopes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdnYX4jTS3JwMMP9JCGxzU

* feat(skills): let skill owners yank a published version

Yanking a published version is only available through
POST /api/v1/admin/skills/versions/{versionId}/yank, which is
session-only (SESSION_ONLY_ROUTES covers /api/v1/admin/**) and requires
SKILL_ADMIN or SUPER_ADMIN. A skill owner therefore cannot pull a
broken release themselves, neither from the web surface nor from a
script holding an API token.

In package registries yank is an act of the publisher: `cargo yank`
and PyPI's "yank release" are performed by the package owner, not by a
registry admin, because the goal is to stop new installs of a bad
release while keeping the artifact available for lock files. SkillHub
already lets owners archive, unarchive, rerelease and delete draft
versions through SkillLifecycleController under the
assertCanManageLifecycle rule (owner, or namespace ADMIN/OWNER); yank
belongs on the same surface with the same rule.

Changes:

- SkillGovernanceService: add an owner-checked yankVersion(skill,
  version, actor, roles, ip, ua, reason) that runs
  assertCanManageLifecycle and then the same yank logic as the admin
  variant, now shared in yankVersionInternal. The admin entry point is
  unchanged for AdminSkillController.
- SkillLifecycleAppService / GovernanceWorkflowAppService: resolve
  skill and version by namespace/slug/version, delegate to the new
  domain method, and return SkillLifecycleMutationResponse with action
  YANK and the resulting version status. The YANK_SKILL_VERSION audit
  record and SkillVersionYankedEvent are emitted by the domain service
  exactly as for the admin path.
- SkillLifecycleController: POST /{namespace}/{slug}/versions/{version}/yank
  on both /api/v1/skills and /api/web/skills, optional body
  AdminSkillActionRequest (reason).
- RouteSecurityPolicyRegistry: require skill:yank for the new route on
  both prefixes, so tokens can reach it as the SESSION_ONLY_ROUTES
  comment promises for every route the authorization list opens. The
  admin yank stays session-only. No allow-list of scopes exists outside
  the registry; the docs' scope enumeration is updated to include
  skill:yank.
- Tests: RouteSecurityPolicyRegistryTest (scope required on both
  prefixes, admin route still closed), SkillGovernanceServiceTest
  (owner and namespace ADMIN allowed, MEMBER forbidden, unpublished
  rejected), SkillLifecycleControllerTest (envelope with and without
  body).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdnYX4jTS3JwMMP9JCGxzU

* fix(auth): complete API token lifecycle access

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(skills): align owner lifecycle token access

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: Thiago Nascimento Nogueira <thiago.nascimento.nogueira@emeal.nttdata.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
thiagonogueira 2026-09-17 07:48:05 -03:00 committed by GitHub
parent 95d3d6a1df
commit 217e7f4042
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 343 additions and 6 deletions

View file

@ -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

View file

@ -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

View file

@ -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<SkillLifecycleMutationResponse> 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<Long, NamespaceRole> 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<SkillLifecycleMutationResponse> withdrawReview(@PathVariable String namespace,
@PathVariable String slug,

View file

@ -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<Long, NamespaceRole> userNsRoles,
AuditRequestContext auditContext) {
return skillLifecycleAppService.yankVersion(
namespace,
slug,
version,
request,
userId,
userNsRoles,
auditContext
);
}
public SkillLifecycleMutationResponse withdrawReviewVersion(String namespace,
String slug,
String version,

View file

@ -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<Long, NamespaceRole> 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,

View file

@ -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");

View file

@ -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"),

View file

@ -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")) {

View file

@ -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<Long, NamespaceRole> 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());
}

View file

@ -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);

View file

@ -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;