···11+- creating a collection
22+ - in create collection drawer, include drop down for selecting the access type
33+- updating collection access type
44+ - creators can change the access type afterwards
55+ -
66+- viewing a collection
77+ - access type should be visible on the collection page and potentially even from the collection “card” view - means the `Collection` type in responses.ts needs to include the accessType
88+ - cards should indicate the author who added it
99+ - could be a small footnote “added by user A” or something else - UI needs to know if its an open collection
1010+ - maybe this should only show if the card author differs from the collection author, that way its the same logic for open and closed and handled the case where an open becomes closed
1111+- adding to a collection
1212+ - if creator - will show up as normal
1313+ - if contributor, will show up in a separate “open collections” tab
1414+ - consider showing recently added to open collections at the top of the list here
1515+ - not sure if the feed activity item link to collection needs to look different or not
1616+- removing from a collection
1717+ - creator can remove any of the cards in the collection
1818+ - if not added by them / not their card, then will need to be handled slightly differently (directly added to the collection record itself?)
1919+ - contributor can only remove the cards they’ve added (straightforward, delete the collection link as normal)
2020+- notifications
2121+ - creator is notified whenever someone adds to their collection
2222+ - user A added to your collection USER_ADDED_TO_YOUR_COLLECTION
2323+- Semble page
2424+ - same as current collections tab, perhaps with some visual indication of the changes
2525+- api client
2626+ - collection type should include access type
···11+# Open Collection Removal Logic Implementation
22+33+**Date:** January 22, 2026
44+55+## Overview
66+77+Implemented card removal logic for OPEN collections in the ATProto-based application, including the creation of a new `collectionLinkRemoval` lexicon record type to handle cases where collection owners need to remove cards added by other users.
88+99+## Problem Statement
1010+1111+In ATProto, users can only modify records in their own repositories. When a collection owner wants to remove a card that was added by another user from an OPEN collection, they cannot delete the `collectionLink` record (which exists in the other user's repository). A solution was needed to mark these cards as removed without requiring direct deletion of another user's records.
1212+1313+## Solution: CollectionLinkRemoval Record
1414+1515+Implemented Option 2 from the design phase: a separate `collectionLinkRemoval` record type that collection owners publish in their own repository to indicate a card has been removed.
1616+1717+### Lexicon Schema
1818+1919+Created `src/modules/atproto/infrastructure/lexicons/collectionLinkRemoval.json`:
2020+2121+```json
2222+{
2323+ "lexicon": 1,
2424+ "id": "network.cosmik.collectionLinkRemoval",
2525+ "description": "A record indicating that a card was removed from a collection by the collection owner.",
2626+ "defs": {
2727+ "main": {
2828+ "type": "record",
2929+ "description": "A record representing the removal of a collection link by a collection owner when they cannot delete the original link (which exists in another user's repository). The creator of this record (determined from the AT-URI) is the user who performed the removal.",
3030+ "key": "tid",
3131+ "record": {
3232+ "type": "object",
3333+ "required": ["collection", "removedLink", "removedAt"],
3434+ "properties": {
3535+ "collection": {
3636+ "type": "ref",
3737+ "description": "Strong reference to the collection record.",
3838+ "ref": "com.atproto.repo.strongRef"
3939+ },
4040+ "removedLink": {
4141+ "type": "ref",
4242+ "description": "Strong reference to the collectionLink record that is being removed.",
4343+ "ref": "com.atproto.repo.strongRef"
4444+ },
4545+ "removedAt": {
4646+ "type": "string",
4747+ "format": "datetime",
4848+ "description": "Timestamp when the link was removed from the collection."
4949+ }
5050+ }
5151+ }
5252+ }
5353+ }
5454+}
5555+```
5656+5757+**Design Notes:**
5858+5959+- No `removedBy` field - user identity is derived from the AT-URI of the removal record
6060+- No `reason` field - kept the schema minimal and focused
6161+- Uses StrongRef to ensure content-addressable references (URI + CID)
6262+6363+## Business Rules for OPEN Collections
6464+6565+### Removal Permissions
6666+6767+**For OPEN Collections:**
6868+6969+- ✅ Collection author can remove any card (including cards added by others)
7070+- ✅ Users can only remove cards they themselves added
7171+- ❌ Non-authors cannot remove cards added by others
7272+7373+### ATProto Publishing Logic
7474+7575+When removing a card from an OPEN collection:
7676+7777+1. **User removing their own card:**
7878+ - Unpublishes the `collectionLink` record (deletes from their repository)
7979+ - No removal record is created
8080+8181+2. **Collection author removing someone else's card:**
8282+ - Publishes a `collectionLinkRemoval` record in their own repository
8383+ - The original `collectionLink` remains in the other user's repository (cannot be deleted)
8484+8585+## Implementation Details
8686+8787+### 1. Domain Layer
8888+8989+**File:** `src/modules/cards/domain/Collection.ts`
9090+9191+Updated the `removeCard()` method (lines 260-309) to enforce permissions based on collection type:
9292+9393+```typescript
9494+public removeCard(
9595+ cardId: CardId,
9696+ userId: CuratorId,
9797+): Result<void, CollectionAccessError> {
9898+ // Find the card link to check who added it
9999+ const cardLink = this.props.cardLinks.find((link) =>
100100+ link.cardId.equals(cardId),
101101+ );
102102+103103+ // If card is not in collection, removal is a no-op (succeeds idempotently)
104104+ if (!cardLink) {
105105+ return ok(undefined);
106106+ }
107107+108108+ // Check removal permissions based on collection type and user role
109109+ const isAuthor = this.props.authorId.equals(userId);
110110+ const isUserRemovingOwnCard = cardLink.addedBy.equals(userId);
111111+112112+ if (this.isOpen) {
113113+ // For OPEN collections:
114114+ // - Author can remove any card
115115+ // - Users can only remove cards they added themselves
116116+ if (!isAuthor && !isUserRemovingOwnCard) {
117117+ return err(
118118+ new CollectionAccessError(
119119+ 'User does not have permission to remove cards from this collection',
120120+ ),
121121+ );
122122+ }
123123+ } else {
124124+ // For CLOSED collections:
125125+ // Use the standard canAddCard check (author + collaborators)
126126+ // Note: CLOSED collection removal scenarios with collaborators are deferred
127127+ if (!this.canAddCard(userId)) {
128128+ return err(
129129+ new CollectionAccessError(
130130+ 'User does not have permission to remove cards from this collection',
131131+ ),
132132+ );
133133+ }
134134+ }
135135+136136+ this.props.cardLinks = this.props.cardLinks.filter(
137137+ (link) => !link.cardId.equals(cardId),
138138+ );
139139+ this.props.cardCount = this.props.cardLinks.length;
140140+ this.props.updatedAt = new Date();
141141+142142+ return ok(undefined);
143143+}
144144+```
145145+146146+### 2. Service Layer
147147+148148+**File:** `src/modules/cards/domain/services/CardCollectionService.ts`
149149+150150+The `removeCardFromCollection` method (lines 213-279) handles the publishing logic:
151151+152152+```typescript
153153+// Check permissions FIRST before attempting any publishing operations
154154+const canRemoveResult = collection.removeCard(card.cardId, curatorId);
155155+if (canRemoveResult.isErr()) {
156156+ return err(new CardCollectionValidationError(...));
157157+}
158158+// Re-add the card since we only wanted to check permissions
159159+collection.addCard(card.cardId, cardLink.addedBy, cardLink.viaCardId);
160160+161161+// Handle unpublishing/removal based on options
162162+if (!options?.skipPublishing && cardLink.publishedRecordId) {
163163+ const isUserRemovingOwnCard = cardLink.addedBy.equals(curatorId);
164164+ const isCollectionAuthor = collection.authorId.equals(curatorId);
165165+166166+ if (isUserRemovingOwnCard) {
167167+ // Unpublish the CollectionLink (delete from their repo)
168168+ await collectionPublisher.unpublishCardAddedToCollection(...);
169169+ } else if (isCollectionAuthor) {
170170+ // Publish a CollectionLinkRemoval record (only author can do this)
171171+ await collectionPublisher.publishCollectionLinkRemoval(...);
172172+ } else {
173173+ // Should never happen due to permission check above
174174+ return err(new CardCollectionValidationError(
175175+ 'User does not have permission to remove this card from the collection'
176176+ ));
177177+ }
178178+}
179179+```
180180+181181+**Key Design Decision:** Permission check happens BEFORE any ATProto publishing operations to prevent unauthorized publishes.
182182+183183+### 3. Infrastructure Layer
184184+185185+**File:** `src/modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher.ts`
186186+187187+Added `publishCollectionLinkRemoval` method (lines 280-350):
188188+189189+```typescript
190190+async publishCollectionLinkRemoval(
191191+ card: Card,
192192+ collection: Collection,
193193+ curatorId: CuratorId,
194194+ removedLinkRef: PublishedRecordId,
195195+): Promise<Result<PublishedRecordId, UseCaseError>> {
196196+ // ... validation and auth ...
197197+ const removalRecordDTO = CollectionLinkRemovalMapper.toCreateRecordDTO(
198198+ collection.publishedRecordId.getValue(),
199199+ removedLinkRef.getValue(),
200200+ );
201201+ const createResult = await agent.com.atproto.repo.createRecord({
202202+ repo: curatorDid.value,
203203+ collection: this.collectionLinkRemovalCollection,
204204+ record: removalRecordDTO,
205205+ });
206206+ return ok(PublishedRecordId.create({...}));
207207+}
208208+```
209209+210210+### 4. Mapper
211211+212212+**File:** `src/modules/atproto/infrastructure/mappers/CollectionLinkRemovalMapper.ts` (CREATED)
213213+214214+Maps domain data to removal record DTOs:
215215+216216+```typescript
217217+static toCreateRecordDTO(
218218+ collectionPublishedRecordId: PublishedRecordIdProps,
219219+ removedLinkPublishedRecordId: PublishedRecordIdProps,
220220+): CollectionLinkRemovalRecordDTO {
221221+ const record: CollectionLinkRemovalRecordDTO = {
222222+ $type: this.collectionLinkRemovalType as any,
223223+ collection: {
224224+ uri: collectionPublishedRecordId.uri,
225225+ cid: collectionPublishedRecordId.cid,
226226+ },
227227+ removedLink: {
228228+ uri: removedLinkPublishedRecordId.uri,
229229+ cid: removedLinkPublishedRecordId.cid,
230230+ },
231231+ removedAt: new Date().toISOString(),
232232+ };
233233+ return record;
234234+}
235235+```
236236+237237+## Files Modified
238238+239239+### Created
240240+241241+1. `src/modules/atproto/infrastructure/lexicons/collectionLinkRemoval.json` - Lexicon schema
242242+2. `src/modules/atproto/infrastructure/mappers/CollectionLinkRemovalMapper.ts` - Mapper for removal records
243243+244244+### Modified
245245+246246+1. `src/modules/cards/domain/Collection.ts` - Updated `removeCard()` method with proper permissions
247247+2. `src/modules/cards/domain/services/CardCollectionService.ts` - Updated removal logic with publishing decisions
248248+3. `src/modules/cards/application/ports/ICollectionPublisher.ts` - Added `publishCollectionLinkRemoval` method signature
249249+4. `src/modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher.ts` - Implemented `publishCollectionLinkRemoval`
250250+5. `src/modules/cards/tests/utils/FakeCollectionPublisher.ts` - Added test implementation for removal tracking
251251+6. `src/shared/infrastructure/config/EnvironmentConfigService.ts` - Added `collectionLinkRemoval` config
252252+7. `src/shared/infrastructure/http/factories/ServiceFactory.ts` - Updated ATProtoCollectionPublisher instantiation
253253+8. `src/modules/cards/tests/application/RemoveCardFromCollectionUseCase.test.ts` - Updated/added tests
254254+255255+### Generated
256256+257257+- TypeScript types regenerated via `npm run lexgen`
258258+259259+## Test Coverage
260260+261261+### Test Results
262262+263263+- ✅ 20 tests passed in RemoveCardFromCollectionUseCase
264264+- ✅ 2 tests skipped (CLOSED collections with collaborators - out of scope)
265265+- ✅ 148 total collection-related tests passed
266266+267267+### Test Scenarios Added
268268+269269+1. **Collection owner removes contributor's card** → publishes removal record
270270+2. **Contributor removes their own card** → unpublishes link
271271+3. **Owner removes their own card** → unpublishes link
272272+4. **Non-author tries to remove another user's card** → fails with permission error
273273+274274+### Skipped Tests (Out of Scope)
275275+276276+The following tests were skipped as CLOSED collection scenarios with collaborators are deferred:
277277+278278+- `should allow collaborator to remove cards from closed collection`
279279+- `should handle mixed collection permissions when removing cards`
280280+281281+## Enforcement Layers
282282+283283+The removal rules are enforced at **two layers** for defense in depth:
284284+285285+1. **Domain Layer** (`Collection.removeCard()`) - First line of defense, encapsulates business rules
286286+2. **Service Layer** (`CardCollectionService.removeCardFromCollection()`) - Handles ATProto publishing logic
287287+288288+This ensures business rules are properly encapsulated in the domain model where they belong.
289289+290290+## Future Work
291291+292292+### CLOSED Collections with Collaborators
293293+294294+Deferred for future implementation:
295295+296296+- How collaborators can remove cards from CLOSED collections
297297+- Whether collaborators can remove any card or only their own
298298+- Permission rules for multi-user CLOSED collection scenarios
299299+300300+### Potential Enhancements
301301+302302+- Add a `reason` field to collectionLinkRemoval for moderation use cases
303303+- Implement bulk removal operations
304304+- Add notifications when cards are removed from collections
305305+- Query support for fetching removal records from the firehose
306306+307307+## Configuration
308308+309309+Added environment configuration for the collectionLinkRemoval collection:
310310+311311+```typescript
312312+collections: {
313313+ card: 'network.cosmik.card',
314314+ collection: 'network.cosmik.collection',
315315+ collectionLink: 'network.cosmik.collectionLink',
316316+ collectionLinkRemoval: 'network.cosmik.collectionLinkRemoval', // NEW
317317+}
318318+```
319319+320320+## References
321321+322322+- ATProto Documentation: https://atproto.com/
323323+- Lexicon Specification: https://atproto.com/specs/lexicon
324324+- StrongRef Definition: `com.atproto.repo.strongRef`
325325+326326+## Notes
327327+328328+- The implementation focuses exclusively on OPEN collections as specified by the user
329329+- Permission checks occur before any ATProto publishing to prevent unauthorized operations
330330+- The design follows the vertical slice architecture pattern used throughout the codebase
331331+- Error handling uses the Result/Either pattern for type-safe error propagation
···11- run `npm run build:check` to check type errors after making changes
22- whenever we make changes to sql schemas (denoted by .sql. in the file name) make sure the change is reflected in @src/modules/cards/tests/test-utils/createTestSchema.ts and generate the migration files by running `npm run db:generate`
33+- use npm run webapp:type-check to check type errors in frontend
···260260 },
261261 },
262262 },
263263+ NetworkCosmikCollectionLinkRemoval: {
264264+ lexicon: 1,
265265+ id: 'network.cosmik.collectionLinkRemoval',
266266+ description:
267267+ 'A record indicating that a card was removed from a collection by the collection owner.',
268268+ defs: {
269269+ main: {
270270+ type: 'record',
271271+ description:
272272+ "A record representing the removal of a collection link by a collection owner when they cannot delete the original link (which exists in another user's repository). The creator of this record (determined from the AT-URI) is the user who performed the removal.",
273273+ key: 'tid',
274274+ record: {
275275+ type: 'object',
276276+ required: ['collection', 'removedLink', 'removedAt'],
277277+ properties: {
278278+ collection: {
279279+ type: 'ref',
280280+ description: 'Strong reference to the collection record.',
281281+ ref: 'lex:com.atproto.repo.strongRef',
282282+ },
283283+ removedLink: {
284284+ type: 'ref',
285285+ description:
286286+ 'Strong reference to the collectionLink record that is being removed.',
287287+ ref: 'lex:com.atproto.repo.strongRef',
288288+ },
289289+ removedAt: {
290290+ type: 'string',
291291+ format: 'datetime',
292292+ description:
293293+ 'Timestamp when the link was removed from the collection.',
294294+ },
295295+ },
296296+ },
297297+ },
298298+ },
299299+ },
263300 NetworkCosmikDefs: {
264301 lexicon: 1,
265302 id: 'network.cosmik.defs',
···924961 NetworkCosmikCard: 'network.cosmik.card',
925962 NetworkCosmikCollection: 'network.cosmik.collection',
926963 NetworkCosmikCollectionLink: 'network.cosmik.collectionLink',
964964+ NetworkCosmikCollectionLinkRemoval: 'network.cosmik.collectionLinkRemoval',
927965 NetworkCosmikDefs: 'network.cosmik.defs',
928966 ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
929967 AtMarginAnnotation: 'at.margin.annotation',
···11+{
22+ "lexicon": 1,
33+ "id": "network.cosmik.collectionLinkRemoval",
44+ "description": "A record indicating that a card was removed from a collection by the collection owner.",
55+ "defs": {
66+ "main": {
77+ "type": "record",
88+ "description": "A record representing the removal of a collection link by a collection owner when they cannot delete the original link (which exists in another user's repository). The creator of this record (determined from the AT-URI) is the user who performed the removal.",
99+ "key": "tid",
1010+ "record": {
1111+ "type": "object",
1212+ "required": ["collection", "removedLink", "removedAt"],
1313+ "properties": {
1414+ "collection": {
1515+ "type": "ref",
1616+ "description": "Strong reference to the collection record.",
1717+ "ref": "com.atproto.repo.strongRef"
1818+ },
1919+ "removedLink": {
2020+ "type": "ref",
2121+ "description": "Strong reference to the collectionLink record that is being removed.",
2222+ "ref": "com.atproto.repo.strongRef"
2323+ },
2424+ "removedAt": {
2525+ "type": "string",
2626+ "format": "datetime",
2727+ "description": "Timestamp when the link was removed from the collection."
2828+ }
2929+ }
3030+ }
3131+ }
3232+ }
3333+}
···88import { CollectionDescription } from './value-objects/CollectionDescription';
99import { PublishedRecordId } from './value-objects/PublishedRecordId';
1010import { CardAddedToCollectionEvent } from './events/CardAddedToCollectionEvent';
1111+import { CardRemovedFromCollectionEvent } from './events/CardRemovedFromCollectionEvent';
1112import { CollectionCreatedEvent } from './events/CollectionCreatedEvent';
12131314export interface CardLink {
···199200 );
200201 }
201202203203+ public canRemoveCard(cardId: CardId, userId: CuratorId): boolean {
204204+ // Find the card link to check who added it
205205+ const cardLink = this.props.cardLinks.find((link) =>
206206+ link.cardId.equals(cardId),
207207+ );
208208+209209+ // If card is not in collection, removal is allowed (no-op)
210210+ if (!cardLink) {
211211+ return true;
212212+ }
213213+214214+ // Check removal permissions based on collection type and user role
215215+ const isAuthor = this.props.authorId.equals(userId);
216216+ const isUserRemovingOwnCard = cardLink.addedBy.equals(userId);
217217+218218+ if (this.isOpen) {
219219+ // For OPEN collections:
220220+ // - Author can remove any card
221221+ // - Users can only remove cards they added themselves
222222+ return isAuthor || isUserRemovingOwnCard;
223223+ } else {
224224+ // For CLOSED collections:
225225+ // Use the standard canAddCard check (author + collaborators)
226226+ // Note: CLOSED collection removal scenarios with collaborators are deferred
227227+ return this.canAddCard(userId);
228228+ }
229229+ }
230230+202231 public addCard(
203232 cardId: CardId,
204233 userId: CuratorId,
···264293 cardId: CardId,
265294 userId: CuratorId,
266295 ): Result<void, CollectionAccessError> {
267267- if (!this.canAddCard(userId)) {
268268- return err(
269269- new CollectionAccessError(
270270- 'User does not have permission to remove cards from this collection',
271271- ),
272272- );
296296+ // Find the card link to check who added it
297297+ const cardLink = this.props.cardLinks.find((link) =>
298298+ link.cardId.equals(cardId),
299299+ );
300300+301301+ // If card is not in collection, removal is a no-op (succeeds idempotently)
302302+ if (!cardLink) {
303303+ return ok(undefined);
304304+ }
305305+306306+ // Check removal permissions based on collection type and user role
307307+ const isAuthor = this.props.authorId.equals(userId);
308308+ const isUserRemovingOwnCard = cardLink.addedBy.equals(userId);
309309+310310+ if (this.isOpen) {
311311+ // For OPEN collections:
312312+ // - Author can remove any card
313313+ // - Users can only remove cards they added themselves
314314+ if (!isAuthor && !isUserRemovingOwnCard) {
315315+ return err(
316316+ new CollectionAccessError(
317317+ 'User does not have permission to remove cards from this collection',
318318+ ),
319319+ );
320320+ }
321321+ } else {
322322+ // For CLOSED collections:
323323+ // Use the standard canAddCard check (author + collaborators)
324324+ // Note: CLOSED collection removal scenarios with collaborators are deferred
325325+ if (!this.canAddCard(userId)) {
326326+ return err(
327327+ new CollectionAccessError(
328328+ 'User does not have permission to remove cards from this collection',
329329+ ),
330330+ );
331331+ }
273332 }
274333275334 this.props.cardLinks = this.props.cardLinks.filter(
···277336 );
278337 this.props.cardCount = this.props.cardLinks.length;
279338 this.props.updatedAt = new Date();
339339+340340+ // Raise domain event
341341+ this.addDomainEvent(
342342+ CardRemovedFromCollectionEvent.create(
343343+ cardId,
344344+ this.collectionId,
345345+ userId,
346346+ ).unwrap(),
347347+ );
280348281349 return ok(undefined);
282350 }
···212212 return ok(null);
213213 }
214214215215- // Handle unpublishing based on options
215215+ // Check permissions FIRST before attempting any publishing operations
216216+ if (!collection.canRemoveCard(card.cardId, curatorId)) {
217217+ return err(
218218+ new CardCollectionValidationError(
219219+ 'User does not have permission to remove cards from this collection',
220220+ ),
221221+ );
222222+ }
223223+224224+ // Handle unpublishing/removal based on options
216225 if (!options?.skipPublishing && cardLink.publishedRecordId) {
217217- const unpublishLinkResult =
218218- await this.collectionPublisher.unpublishCardAddedToCollection(
219219- cardLink.publishedRecordId,
220220- );
221221- if (unpublishLinkResult.isErr()) {
222222- // Propagate authentication errors
223223- if (unpublishLinkResult.error instanceof AuthenticationError) {
224224- return err(unpublishLinkResult.error);
226226+ // Determine if this is a user removing their own card or collection author removing someone else's card
227227+ const isUserRemovingOwnCard = cardLink.addedBy.equals(curatorId);
228228+ const isCollectionAuthor = collection.authorId.equals(curatorId);
229229+230230+ if (isUserRemovingOwnCard) {
231231+ // User is removing their own card - unpublish the CollectionLink (delete from their repo)
232232+ const unpublishLinkResult =
233233+ await this.collectionPublisher.unpublishCardAddedToCollection(
234234+ cardLink.publishedRecordId,
235235+ );
236236+ if (unpublishLinkResult.isErr()) {
237237+ // Propagate authentication errors
238238+ if (unpublishLinkResult.error instanceof AuthenticationError) {
239239+ return err(unpublishLinkResult.error);
240240+ }
241241+ return err(
242242+ new CardCollectionValidationError(
243243+ `Failed to unpublish collection link: ${unpublishLinkResult.error.message}`,
244244+ ),
245245+ );
246246+ }
247247+ } else if (isCollectionAuthor) {
248248+ // Collection author is removing someone else's card - publish a CollectionLinkRemoval record
249249+ // This is the ONLY case where removal records are published
250250+ const publishRemovalResult =
251251+ await this.collectionPublisher.publishCollectionLinkRemoval(
252252+ card,
253253+ collection,
254254+ curatorId,
255255+ cardLink.publishedRecordId,
256256+ );
257257+ if (publishRemovalResult.isErr()) {
258258+ // Propagate authentication errors
259259+ if (publishRemovalResult.error instanceof AuthenticationError) {
260260+ return err(publishRemovalResult.error);
261261+ }
262262+ return err(
263263+ new CardCollectionValidationError(
264264+ `Failed to publish collection link removal: ${publishRemovalResult.error.message}`,
265265+ ),
266266+ );
225267 }
268268+ } else {
269269+ // This should never happen because permissions are checked above
270270+ // If someone is neither the card adder nor the collection author, they shouldn't have permission
226271 return err(
227272 new CardCollectionValidationError(
228228- `Failed to unpublish collection link: ${unpublishLinkResult.error.message}`,
273273+ 'User does not have permission to remove this card from the collection',
229274 ),
230275 );
231276 }