Fix three blockers and one contract drift issue surfaced in code review:
1. API token policy: add skill:publish scope policy and authentication
policy for /api/cli/v1/skills/*/publish/validate. Without these the
AntPathMatcher pattern /publish would not cover /publish/validate,
so Bearer-token requests would be rejected by the scope filter.
2. Warnings semantics: dry-run now treats warnings as making valid=false.
The CLI publish flow uses confirmWarnings=false, so the real publish
rejects any warnings; dry-run must mirror that to avoid false positives.
3. Visibility parameter: validate endpoint now accepts the same
visibility multipart field as publish. The CLI forwards --visibility
so invalid values are caught at dry-run time rather than at publish.
4. Schema drift: resolvedSlug and resolvedVersion are nullable in
practice (returned as null when validation fails before resolution).
Updated schema.d.ts to reflect string | null instead of optional string.
Tests added:
- RouteSecurityPolicyRegistryTest: validate endpoint scope check
- CliDryRunValidateTest: custom + invalid visibility cases
- publish-dry-run.test.ts: --visibility forwarded to server
- Exit non-zero (code 6) when --dry-run validation fails, enabling
CI/CD pipeline integration
- Add archived skill check: dry-run now detects when the publisher's
own skill is archived
- Add version-exists check: dry-run now detects when the resolved
version is already published
- Use StandardCharsets.UTF_8 for SKILL.md content parsing
Add a validate-only endpoint (POST /api/cli/v1/skills/{namespace}/publish/validate)
that runs the full pre-publish validation chain without persisting anything.
This allows developers to check their package locally before actual publishing.
The validation covers:
- SKILL.md existence and frontmatter parsing (name, description required)
- File extension whitelist and size limits
- Credential leak scanning with line-number precision
- Slug generation and name conflict detection
CLI usage: `skillhub publish <path> --dry-run`
Closes#429
When approving a promotion, the new SkillVersion was created without copying
bundleReady and downloadReady from the source version, causing the download
button to be permanently disabled for promoted skills.
Gemini review feedback: the previous != PUBLISHED condition was too broad
and could inadvertently overwrite terminal states like REJECTED or YANKED.
Now explicitly check == SCANNING before transitioning status.
Super admin auto-publish flow was skipping security scanning entirely.
Now triggerScan is called regardless of autoPublish flag, while preserving
the PUBLISHED status (scan runs as post-publish audit rather than blocking).
Closes#415
Introduce a dedicated `/space/$namespace/$slug/compare` page that compares
two published skill versions GitHub-style: left file list + right unified
diff. Backend exposes `GET /versions/compare` returning structured diff
(computed via java-diff-utils) with per-file hunks, binary placeholder,
and truncation flags. Frontend uses two version selectors scoped to
PUBLISHED versions, a file search box, active-file highlighting, and
whitespace-preserving unified view. E2E covers the publish + rerelease
+ approve round trip; controller/domain tests cover happy path and
same-version rejection.
* feat(publish): increase max file count from 100 to 500
Configurable via SKILLHUB_PUBLISH_MAX_FILE_COUNT env var.
* feat(publish): support SKILL.md in subdirectory with warning for ignored files
When SKILL.md is found in a single subdirectory (e.g. my-skill/SKILL.md),
promote that directory's contents to root and discard files outside it.
Discarded files are reported as warnings through the existing confirm flow.
* feat(publish): pass extraction warnings through confirm flow
When files are ignored during SKILL.md subdirectory promotion,
warnings are surfaced to the user via the existing precheck confirm dialog.
* fix(security): add invalidSessionStrategy to return 401 on expired session
Handles the case where Spring Security detects an invalid session cookie,
returning a clean 401 JSON response instead of triggering cascading exceptions.
Closes#360 (part 1/2)
* fix(security): handle session invalidation IllegalStateException as 401
Catches IllegalStateException with "Session was invalidated" message and
returns 401 instead of letting it fall through to the generic 500 handler.
Non-session IllegalStateExceptions are re-thrown to the catch-all handler.
Closes#360 (part 2/2)
* feat(publish): filter macOS metadata and add integration tests
Skip __MACOSX/, .DS_Store, and ._ resource fork entries during zip
extraction. Add integration tests for nested SKILL.md warning flow,
session invalidation 401 response, and macOS metadata filtering.
* test(publish): add real-world macOS zip and edge case integration tests
Covers: macOS zip with nested SKILL.md + __MACOSX + .DS_Store + stray files,
simple macOS single-folder case, and missing SKILL.md fallback behavior.
Include subscriptionCount in SkillDetailDTO and SkillDetailResponse
so the frontend SubscribeButton receives the updated count after
subscribe/unsubscribe mutations.
- Add platformRoles parameter to VisibilityChecker.canAccess() for platform-level access control
- SUPER_ADMIN can access all skills regardless of visibility or publication status
- Add archived namespace check to SkillQueryService.getSkillDetail()
- Extract platformRoles from AuthContext in SkillController
- Replace VisibilityChecker mock with real instance in SkillQueryServiceTest
- Add 5 new tests for SUPER_ADMIN access scenarios
- Add version-status-badge.tsx component for frontend status display
Tests: 347 domain tests + 16 app tests passing
- Remove duplicate useSubmitForReview/useConfirmPublish in hooks and client
- Restore resolveVersionStatusLabel (version-status-badge not in this branch)
- Fix missing Card closing tag in versions tab
- Backend: Add confirmWarnings parameter to rerelease DTO, domain service, and app service
- Frontend: Add warning dialog with retry logic when precheck warnings are detected
- i18n: Add rerelease warning dialog translations (en/zh)
Fixes the issue where rereleasing a published version with secret detection warnings
always fails with 400 error. Now follows the same confirm-and-retry pattern as initial publish.
* feat(skill): add UPLOADED status for PRIVATE skill lifecycle
## Summary
- Add UPLOADED status for PRIVATE skills after security scan passes
- PRIVATE skill owners can test before confirming publish or submitting for review
- Rerelease now follows visibility rules (PRIVATE→UPLOADED, PUBLIC→PENDING_REVIEW)
- Auto-withdraw changes status to UPLOADED (not DRAFT) to keep versions visible
## Changes
- SkillVersionStatus: Add UPLOADED enum value
- SkillPublishService: PRIVATE skills go to UPLOADED after scan
- SecurityScanService: Visibility-based status transition after scan
- SkillGovernanceService: Withdraw→UPLOADED, delete allows UPLOADED
- SkillQueryService: Include UPLOADED in version list filters
- SkillReviewSubmitService: New service for submit-review and confirm-publish
- SkillLifecycleController: Add submit-review and confirm-publish endpoints
- Frontend: Add buttons, dialogs, and hooks for new operations
## Workflow
- PRIVATE: Publish → SCANNING → UPLOADED → confirm-publish → PUBLISHED
- PUBLIC: Publish → SCANNING → PENDING_REVIEW → PUBLISHED
* feat(review): add backward compatibility for DRAFT status
Support both DRAFT (legacy) and UPLOADED (new flow) status in:
- SkillReviewSubmitService.submitForReview
- SkillReviewSubmitService.confirmPublish
- ReviewService.submitReview (both overloads)
This ensures existing data with DRAFT status continues to work
with the new visibility-based workflow introduced in OSS-02.
Support both DRAFT (legacy) and UPLOADED (new flow) status in:
- SkillReviewSubmitService.submitForReview
- SkillReviewSubmitService.confirmPublish
- ReviewService.submitReview (both overloads)
This ensures existing data with DRAFT status continues to work
with the new visibility-based workflow introduced in OSS-02.
## Summary
- Add UPLOADED status for PRIVATE skills after security scan passes
- PRIVATE skill owners can test before confirming publish or submitting for review
- Rerelease now follows visibility rules (PRIVATE→UPLOADED, PUBLIC→PENDING_REVIEW)
- Auto-withdraw changes status to UPLOADED (not DRAFT) to keep versions visible
## Changes
- SkillVersionStatus: Add UPLOADED enum value
- SkillPublishService: PRIVATE skills go to UPLOADED after scan
- SecurityScanService: Visibility-based status transition after scan
- SkillGovernanceService: Withdraw→UPLOADED, delete allows UPLOADED
- SkillQueryService: Include UPLOADED in version list filters
- SkillReviewSubmitService: New service for submit-review and confirm-publish
- SkillLifecycleController: Add submit-review and confirm-publish endpoints
- Frontend: Add buttons, dialogs, and hooks for new operations
## Workflow
- PRIVATE: Publish → SCANNING → UPLOADED → confirm-publish → PUBLISHED
- PUBLIC: Publish → SCANNING → PENDING_REVIEW → PUBLISHED
* feat(publish): allow warning-confirmed pre-publish checks\n\nFixes #287
* fix(i18n): add missing register validation translation keys
The registration form uses i18n keys like register.usernameInvalid,
register.passwordTooShort etc. but they were never defined in the
locale files, causing E2E tests to fail because the raw key strings
were displayed instead of human-readable messages.
* fix: increase nginx upload limit and filter Chrome DevTools logs
- Add client_max_body_size 100M to nginx config to allow large skill package uploads
- Silently handle Chrome DevTools .well-known requests to reduce log noise
Fixes#193
* feat(domain): add Unicode slug support with emoji validation
- Update SLUG_PATTERN to support Unicode letters (\p{L}), numbers (\p{N}), and symbols (\p{So})
- Add UPPERCASE_PATTERN to maintain lowercase-only validation
- Modify slugify() to preserve Unicode characters instead of replacing with hyphens
- Add 8 new test cases covering Chinese, Japanese, Korean, emoji, and mixed Unicode slugs
- Maintain backward compatibility with existing ASCII slug validation
- All 16 tests passing
Implements Phase 1 of unicode-slug-support-v1.0-prd.md
* feat(web): add URL encoding for Unicode slugs and improve monospace font support
- Encode skill slugs in all navigation URLs using encodeURIComponent
- Encode slugs in API client methods for proper HTTP request handling
- Update share button to generate properly encoded URLs
- Extend monospace font stack with system fonts for better Unicode support
- All frontend tests pass (506 tests)
Related to Unicode slug support PRD phase 2 and 3
* feat(web): regenerate API types for Unicode slug support
- Update OpenAPI schema types after backend slug validator changes
- Reflects new Unicode-aware slug pattern validation
* remove
* feat(upload): allow xsd, xsl, dtd file types in skill packages
Add XML schema-related file extensions (.xsd, .xsl, .dtd) to the
upload allowlist and text content validation. Users uploading skills
with XML Schema files (e.g., Anthropic's docx skill) were getting
rejected because .xsd was not in the allowed extensions list.
Closes#165
* feat(upload): expand office file allowlist
* test(app): verify publish extension env override
* docs(readme): document upload allowlist override
* fix(scanner): wire timeout config and create dedicated scanner HttpClient
- Create scanner-specific HttpClient bean with proper timeout configuration
- Wire SkillScannerProperties.connectTimeoutMs (5s) and readTimeoutMs (5min) into WebClient
- Add logging for timeout configuration verification
- Fix cloud environment 5-second timeout issue by ensuring responseTimeout is properly applied
The previous global WebClient.Builder configuration was not being used correctly,
causing scanner requests to timeout after 5 seconds in cloud environment.
This fix creates a dedicated HttpClient for scanner with explicit timeout settings.
* refactor(webclient): use WebClientCustomizer and add scanner connection pool
- Replace singleton WebClient.Builder bean with WebClientCustomizer
(follows Spring Boot best practice for prototype-scoped builder)
- Add connection pool config to scanner HttpClient (maxConn=10,
maxIdleTime=20s, evictInBackground=30s) to prevent stale connections
- Add connectTimeout to global WebClient config
* fix(skill): use system default timezone for auto-generated version numbers
- Change AUTO_VERSION_FORMATTER from UTC to ZoneId.systemDefault()
- Version format yyyyMMdd.HHmmss now uses server's local timezone
- Update test to validate format instead of exact value (timezone-independent)
This allows the service to adapt to deployment location:
- Deployed in China → uses Asia/Shanghai timezone
- Deployed in US → uses US timezone
- Follows server's system timezone configuration
* fix(skill): resolve duplicate result error when deleting skill with shared namespace+slug
V13 migration changed the unique constraint from (namespace_id, slug) to
(namespace_id, slug, owner_id), allowing multiple skills with the same
namespace+slug but different owners. The findByNamespaceSlugAndSlug query
returned Optional<Skill> which threw IncorrectResultSizeDataAccessException
when 2 rows matched.
Changed the query to return List<Skill> and added ownerId query param to
DELETE endpoints so the frontend can specify exactly which skill to delete.
* fix(review): keep failed scans reviewable and expose feedback
* fix(skill-delete): delete skills by id
* fix(skill): defer version storage deletion until commit
* fix(skill): resolve duplicate result error when deleting skill with shared namespace+slug
V13 migration changed the unique constraint from (namespace_id, slug) to
(namespace_id, slug, owner_id), allowing multiple skills with the same
namespace+slug but different owners. The findByNamespaceSlugAndSlug query
returned Optional<Skill> which threw IncorrectResultSizeDataAccessException
when 2 rows matched.
Changed the query to return List<Skill> and added ownerId query param to
DELETE endpoints so the frontend can specify exactly which skill to delete.
* fix(review): keep failed scans reviewable and expose feedback