···
// NewCommunityService creates a new community service
func NewCommunityService(repo Repository, pdsURL, instanceDID, instanceDomain string, provisioner *PDSAccountProvisioner) Service {
31
+
// SECURITY: Basic validation that did:web domain matches configured instanceDomain
32
+
// This catches honest configuration mistakes but NOT malicious code modifications
33
+
// Full verification (Phase 2) requires fetching DID document from domain
34
+
// See: docs/PRD_BACKLOG.md - "did:web Domain Verification"
35
+
if strings.HasPrefix(instanceDID, "did:web:") {
36
+
didDomain := strings.TrimPrefix(instanceDID, "did:web:")
37
+
if didDomain != instanceDomain {
38
+
log.Printf("⚠️ SECURITY WARNING: Instance DID domain (%s) doesn't match configured domain (%s)",
39
+
didDomain, instanceDomain)
40
+
log.Printf(" This could indicate a configuration error or potential domain spoofing attempt")
41
+
log.Printf(" Communities will be hosted by: %s", instanceDID)
return &communityService{
···
if req.Visibility == "" {
req.Visibility = "public"
78
+
// SECURITY: Auto-populate hostedByDID from instance configuration
79
+
// Clients MUST NOT provide this field - it's derived from the instance receiving the request
80
+
// This prevents malicious instances from claiming to host communities for domains they don't own
81
+
req.HostedByDID = s.instanceDID
if err := s.validateCreateRequest(req); err != nil {
···
// SubscribeToCommunity creates a subscription via write-forward to PDS
356
-
func (s *communityService) SubscribeToCommunity(ctx context.Context, userDID, communityIdentifier string) (*Subscription, error) {
375
+
func (s *communityService) SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*Subscription, error) {
return nil, NewValidationError("userDid", "required")
379
+
if userAccessToken == "" {
380
+
return nil, NewValidationError("userAccessToken", "required")
// Resolve community identifier to DID
···
"community": communityDID,
384
-
// Write-forward: create subscription record in user's repo
385
-
recordURI, recordCID, err := s.createRecordOnPDS(ctx, userDID, "social.coves.community.subscribe", "", subRecord)
406
+
// Write-forward: create subscription record in user's repo using their access token
407
+
recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.subscribe", "", subRecord, userAccessToken)
return nil, fmt.Errorf("failed to create subscription on PDS: %w", err)
···
// UnsubscribeFromCommunity removes a subscription via PDS delete
403
-
func (s *communityService) UnsubscribeFromCommunity(ctx context.Context, userDID, communityIdentifier string) error {
425
+
func (s *communityService) UnsubscribeFromCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error {
return NewValidationError("userDid", "required")
429
+
if userAccessToken == "" {
430
+
return NewValidationError("userAccessToken", "required")
// Resolve community identifier
···
return fmt.Errorf("invalid subscription record URI")
426
-
// Write-forward: delete record from PDS
427
-
if err := s.deleteRecordOnPDS(ctx, userDID, "social.coves.community.subscribe", rkey); err != nil {
451
+
// Write-forward: delete record from PDS using user's access token
452
+
if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.subscribe", rkey, userAccessToken); err != nil {
return fmt.Errorf("failed to delete subscription on PDS: %w", err)
···
return NewValidationError("createdByDid", "required")
551
-
if req.HostedByDID == "" {
552
-
return NewValidationError("hostedByDid", "required")
576
+
// hostedByDID is auto-populated by the service layer, no validation needed
577
+
// The handler ensures clients cannot provide this field
···
_, _, err := s.callPDS(ctx, "POST", endpoint, payload)
644
+
// deleteRecordOnPDSAs deletes a record with a specific access token (for user-scoped deletions)
645
+
func (s *communityService) deleteRecordOnPDSAs(ctx context.Context, repoDID, collection, rkey string, accessToken string) error {
646
+
endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.deleteRecord", strings.TrimSuffix(s.pdsURL, "/"))
648
+
payload := map[string]interface{}{
650
+
"collection": collection,
654
+
_, _, err := s.callPDSWithAuth(ctx, "POST", endpoint, payload, accessToken)