fix: allow reserved collective members to push extensions#642
Conversation
Previously, `swamp extension push` unconditionally rejected @swamp/ and @si/ scoped extensions at manifest parse time, before checking the user's actual collective membership. This moves the reserved collective check from schema validation (structural) to the push command (authorization), so legitimate collective members can push. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Review Summary
This PR correctly moves reserved collective validation from the domain layer (manifest schema) to the application layer (push command authorization). This is architecturally sound.
Domain-Driven Design Analysis ✅
The change properly separates concerns:
- Manifest schema (Value Object): Validates structure - "Is this a valid extension manifest format?"
- Push command (Application layer): Validates authorization - "Is this user allowed to publish to this collective?"
Authorization requires external service calls (SwampClub API) and should not be in the domain's parsing logic. This refactoring follows DDD principles correctly.
Code Quality ✅
- TypeScript strict mode: No
anytypes - Named exports: Used correctly
- Tests updated: Rejection tests converted to acceptance tests appropriately
- License headers: Present
- Error handling: Uses
UserErrorwith clear, actionable messages - Security: Fails closed when server is unreachable for reserved collectives
CI Status ✅
- Lint, Test, and Format Check: pass
- Dependency Audit: pass
- All 2733 tests pass
No Blocking Issues
The implementation is correct:
ModelType.isReservedCollective()is well-tested inmodel_type_test.ts- Error message is actionable: tells user to check network and try again
- Non-members are still rejected via the existing
isAllowedcheck - Network failures are handled safely for reserved collectives
🤖 Reviewed with Claude Code
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
Misleading error message for non-network failures (
src/cli/commands/extension_push.ts:141-144)The error message claims "Could not verify membership — please check your network connection" but fires for multiple failure modes beyond network issues:
Breaking scenario:
- User has expired/invalid API key stored in credentials
whoami()returns{ authenticated: false }(HTTP 401, no exception thrown)getCollectives({ authenticated: false })returnsundefined(noorganizationsfield)- Error fires: "please check your network connection"
- User checks network, finds nothing wrong, is confused
Another scenario:
- Server API version mismatch where
/api/whoamisucceeds but doesn't include theorganizationsfield - Same misleading error
The security boundary is preserved (push is correctly rejected), but users may waste time debugging the wrong thing.
Suggested fix: Check
whoami.authenticatedexplicitly and provide differentiated error messages:const whoamiResult = await swampClubClient.whoami(credentials.apiKey); if (!whoamiResult.authenticated) { throw new UserError("API key is invalid or expired. Run 'swamp auth login' to re-authenticate."); } collectives = getCollectives(whoamiResult);
Low
-
No unit/integration tests for the new authorization logic (
src/cli/commands/extension_push.ts:128-145)The PR adds 17 lines of authorization logic but the only test changes verify manifest parsing accepts reserved collectives. Missing test coverage for:
- Reserved collective with successful server membership verification → push proceeds
- Reserved collective with server unreachable → push rejected with appropriate error
- Reserved collective where user is not a member → push rejected
- Reserved collective with invalid API key (401) → current behavior (misleading error)
The integration test file
integration/extension_push_test.tshas no reserved collective test cases. -
whoami.authenticatedis never checked (src/cli/commands/extension_push.ts:130-137)The code extracts collectives from the whoami response but never verifies
whoami.authenticated === true. For non-reserved collectives with an invalid API key, the code falls through to username matching and attempts the push anyway (which the server would reject, but the CLI could catch this earlier).This is existing behavior, not introduced by this PR, but the new reserved collective check makes the gap more visible.
Verdict
PASS — The security model is sound and correctly implemented. Reserved collectives require server verification with fail-closed semantics when verification is impossible. The misleading error message (Medium #1) should be addressed but does not compromise security. The code does what it claims in the PR description.
fix: allow reserved collective members to push extensions
Summary
@swamp,@si) check from manifest schema validation to the push command's authorization flowswamporsicollectives (verified viaswamp auth whoami) can now push extensions scoped to those collectivesProblem
Running
swamp extension push manifest.yamlwith a@swamp/or@si/scoped extension was unconditionally rejected during Zod schema parsing — before the user's collective membership was ever checked. This made it impossible for actual collective members to publish official extensions.What changed
Manifest validation (
extension_manifest.ts).refine()that rejected reserved collectives at parse timeModelTypeimportPush command (
extension_push.ts)ModelType.isReservedCollective()check in the existing collective membership validationTests (
extension_manifest_test.ts)@swamp/and@si/names are valid at the manifest levelSecurity boundaries preserved
isAllowedcheck validates collective membership via the APITest plan
deno checkpassesdeno lintpassesdeno fmtpassesdeno run test)swamp extension pushwith@swamp/extension as a collective member succeedsswamp extension pushwith@swamp/extension as a non-member is rejected🤖 Generated with Claude Code