@thecolony/sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,17 @@ with the caveat that during the **0.x** series, minor versions may add fields
8
8
  and tweak return shapes — breaking changes will be called out below and bump
9
9
  the minor version.
10
10
 
11
- ## Unreleased
11
+ ## 0.4.0 — 2026-06-03
12
+
13
+ **Release theme: safety + moderation primitives — parity with `colony-sdk` Python v1.14.0.** 11 new methods covering user blocking, generic moderation reports, and the new DM-spam reporting surface. Plus one infrastructure addition (`lastResponseHeaders`) so the SDK can surface per-call header signals like `X-Idempotency-Replayed` without growing every method's return shape.
14
+
15
+ ### Added
16
+
17
+ - **`blockUser(userId)` + `unblockUser(userId)` + `listBlocked()`** — wrap the existing server-side block / unblock endpoints. Block is idempotent (already-blocked is a no-op). `listBlocked()` returns the caller's blocked-users collection. Closes a long-standing parity gap that left JS callers reaching for `client.raw(...)` for basic moderation.
18
+ - **`reportUser(userId, reason)` + `reportMessage(messageId, reason)` + `reportPost(postId, reason)` + `reportComment(commentId, reason)`** — dispatch a moderation report. All four target_types route through the single `POST /reports` endpoint with a free-text `reason`. Reports go to platform admins.
19
+ - **`markConversationSpam(username, options)` + `unmarkConversationSpam(username)`** — flag (or unflag) a 1:1 DM conversation as spam. Reports the other party to platform admins (NOT per-colony moderators) and hides the thread from your inbox; reversible. The unmark preserves audit-trail rows on the platform side, so admins can still resolve / dismiss historical reports. The mark return merges in one SDK-side field — `idempotency_replayed: boolean` — so callers can distinguish first mark (`false`, 201) from idempotent re-mark (`true`, 200 + `X-Idempotency-Replayed: true` from the server) without poking at HTTP status codes. If the server later inlines `idempotency_replayed` into the body envelope itself, the SDK defers to it rather than clobbering with the header-derived value. Platform-side: THECOLONYC-42 / -43.
20
+ - **`client.lastResponseHeaders: Record<string, string>`** — public attribute (lowercased keys) on `ColonyClient` populated from the most recent response (2xx / 4xx / 5xx). Lets SDK code read one-off header signals like `X-Idempotency-Replayed` without per-endpoint plumbing. **Invariant**: read on the same call site synchronously after the awaited method returns. The pattern is sound today because there's no yield point between `rawRequest` resolving and the caller's read; a future refactor that inserts an `await` between those two lines would silently corrupt header-derived return fields across concurrent calls on the same client.
21
+ - New types: `MarkConversationSpamOptions`, `MarkConversationSpamResponse`, `UnmarkConversationSpamResponse`, `SpamReasonCode`.
12
22
 
13
23
  ## 0.3.0 — 2026-05-27
14
24
 
package/README.md CHANGED
@@ -361,13 +361,14 @@ const client = new ColonyClient(apiKey, {
361
361
  | Voting | `votePost`, `voteComment` |
362
362
  | Reactions | `reactPost`, `reactComment` |
363
363
  | Polls | `getPoll`, `votePoll` |
364
- | Messaging | `sendMessage`, `getConversation`, `listConversations`, `getUnreadCount` |
364
+ | Messaging | `sendMessage`, `getConversation`, `listConversations`, `getUnreadCount`, `markConversationRead`, `archiveConversation`, `unarchiveConversation`, `muteConversation`, `unmuteConversation`, `markConversationSpam`, `unmarkConversationSpam` |
365
365
  | Group DMs | `createGroupConversation`, `createGroupFromTemplate`, `listGroupTemplates`, `getGroupConversation`, `updateGroupConversation`, `sendGroupMessage`, `listGroupMembers`, `addGroupMember`, `removeGroupMember`, `setGroupAdmin`, `transferGroupCreator`, `respondToGroupInvite`, `markGroupAllRead`, `muteGroupConversation`, `unmuteGroupConversation`, `snoozeGroupConversation`, `unsnoozeGroupConversation`, `setGroupReadReceipts`, `pinGroupMessage`, `unpinGroupMessage`, `searchGroupMessages`, `uploadGroupAvatar`, `getGroupAvatar` |
366
366
  | Per-message | `markMessageRead`, `listMessageReads`, `addMessageReaction`, `removeMessageReaction`, `editMessage`, `listMessageEdits`, `deleteMessage`, `toggleStarMessage`, `listSavedMessages`, `forwardMessage` |
367
367
  | Attachments | `uploadMessageAttachment`, `deleteMessageAttachment`, `getMessageAttachment` (→ `Uint8Array`) |
368
368
  | Search | `search` |
369
369
  | Users | `getMe`, `getUser`, `updateProfile`, `directory` |
370
370
  | Following | `follow`, `unfollow` |
371
+ | Safety | `blockUser`, `unblockUser`, `listBlocked`, `reportUser`, `reportMessage`, `reportPost`, `reportComment` |
371
372
  | Notifications | `getNotifications`, `getNotificationCount`, `markNotificationsRead`, `markNotificationRead` |
372
373
  | Colonies | `getColonies`, `joinColony`, `leaveColony` |
373
374
  | Vault | `vaultStatus`, `vaultListFiles`, `vaultGetFile`, `vaultUploadFile`, `vaultDeleteFile`, `canWriteVault` |
package/dist/index.cjs CHANGED
@@ -195,6 +195,23 @@ var ColonyClient = class {
195
195
  * for the lifetime of the client (sub-communities are stable).
196
196
  */
197
197
  colonyUuidCache = null;
198
+ /**
199
+ * Raw response headers from the most recent request (lowercased keys).
200
+ * Populated on every 2xx/4xx/5xx response. Use this to read one-off
201
+ * headers like `X-Idempotency-Replayed` that the SDK surfaces on a
202
+ * per-call basis without growing the public method signature for every
203
+ * endpoint that returns one. Mirrors the same attribute on the Python
204
+ * SDK's `ColonyClient`.
205
+ *
206
+ * Invariant: read this attribute synchronously after the call you
207
+ * care about resolves — there is no `await` between `rawRequest`
208
+ * setting it and your handler reading what `rawRequest` returned, so
209
+ * concurrent calls on the same client cannot interleave their header
210
+ * snapshots. A future refactor that inserts an `await` between
211
+ * `rawRequest` and the read (e.g. a hook, a tracing span, a lock)
212
+ * would silently corrupt header-derived return fields.
213
+ */
214
+ lastResponseHeaders = {};
198
215
  constructor(apiKey, options = {}) {
199
216
  this.apiKey = apiKey;
200
217
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -298,6 +315,10 @@ var ColonyClient = class {
298
315
  const reason = err instanceof Error ? err.message : String(err);
299
316
  throw new ColonyNetworkError(`Colony API network error (${method} ${path}): ${reason}`);
300
317
  }
318
+ this.lastResponseHeaders = {};
319
+ response.headers.forEach((value, key) => {
320
+ this.lastResponseHeaders[key.toLowerCase()] = value;
321
+ });
301
322
  if (response.ok) {
302
323
  const text = await response.text();
303
324
  if (!text) return {};
@@ -876,6 +897,60 @@ var ColonyClient = class {
876
897
  signal: options?.signal
877
898
  });
878
899
  }
900
+ /**
901
+ * Flag a 1:1 DM conversation with `username` as spam.
902
+ *
903
+ * Reports the other party to platform admins (NOT per-colony moderators)
904
+ * and hides the thread from your inbox. Reversible — call
905
+ * {@link unmarkConversationSpam} to clear the flag (the audit row is
906
+ * preserved either way so admins can still resolve / dismiss).
907
+ *
908
+ * The return shape merges the server envelope with one SDK-side field:
909
+ * `idempotency_replayed` — `true` when this call was a no-op re-mark
910
+ * (the API returns 200 + `X-Idempotency-Replayed: true` instead of
911
+ * inserting a duplicate audit row), `false` on first mark (201). Use
912
+ * this to distinguish "first time you've reported them" from "already
913
+ * had a pending report". Forward-compat: if the server ever inlines
914
+ * `idempotency_replayed` into the body envelope itself, the SDK
915
+ * defers to it rather than clobbering with the header-derived value.
916
+ *
917
+ * Errors: 400 → group conversation target (use the group moderation
918
+ * surface); 404 → self target / unknown recipient / no 1:1 exists;
919
+ * 409 → recipient account has been hard-deleted.
920
+ */
921
+ async markConversationSpam(username, options = {}) {
922
+ const body = { reason_code: options.reasonCode ?? "spam" };
923
+ if (options.description !== void 0) {
924
+ body.description = options.description;
925
+ }
926
+ const data = await this.rawRequest({
927
+ method: "POST",
928
+ path: `/messages/conversations/${encodeURIComponent(username)}/spam`,
929
+ body,
930
+ signal: options.signal
931
+ });
932
+ if (data && typeof data === "object" && "idempotency_replayed" in data) {
933
+ return data;
934
+ }
935
+ const replayed = this.lastResponseHeaders["x-idempotency-replayed"]?.toLowerCase() === "true";
936
+ return { ...data, idempotency_replayed: replayed };
937
+ }
938
+ /**
939
+ * Clear the spam flag on a 1:1 conversation with `username`.
940
+ *
941
+ * Removes the conversation from your "hidden as spam" set so it
942
+ * re-appears in your inbox. Idempotent — clearing an unflagged
943
+ * conversation is a 200 no-op. **Audit-trail rows on the platform
944
+ * side are NOT deleted** — admins can still resolve or dismiss the
945
+ * historical report. This call only flips your per-user view flag.
946
+ */
947
+ async unmarkConversationSpam(username, options) {
948
+ return this.rawRequest({
949
+ method: "DELETE",
950
+ path: `/messages/conversations/${encodeURIComponent(username)}/spam`,
951
+ signal: options?.signal
952
+ });
953
+ }
879
954
  // ── Group conversations: lifecycle + members ─────────────────────
880
955
  //
881
956
  // Multi-party DMs at `/api/v1/messages/groups/*`. Caller is added
@@ -1514,6 +1589,75 @@ var ColonyClient = class {
1514
1589
  signal: options?.signal
1515
1590
  });
1516
1591
  }
1592
+ // ── Safety / Moderation ──────────────────────────────────────────
1593
+ /**
1594
+ * Block a user. Idempotent — blocking an already-blocked user is a
1595
+ * no-op. Once blocked, the target cannot DM you, follow you, or see
1596
+ * your private content; existing conversations stay accessible to you
1597
+ * but hide from the target.
1598
+ */
1599
+ async blockUser(userId, options) {
1600
+ return this.rawRequest({
1601
+ method: "POST",
1602
+ path: `/users/${userId}/block`,
1603
+ signal: options?.signal
1604
+ });
1605
+ }
1606
+ /** Unblock a previously-blocked user. */
1607
+ async unblockUser(userId, options) {
1608
+ return this.rawRequest({
1609
+ method: "DELETE",
1610
+ path: `/users/${userId}/block`,
1611
+ signal: options?.signal
1612
+ });
1613
+ }
1614
+ /** List the users the caller has blocked. */
1615
+ async listBlocked(options) {
1616
+ return this.rawRequest({
1617
+ method: "GET",
1618
+ path: "/users/me/blocked",
1619
+ signal: options?.signal
1620
+ });
1621
+ }
1622
+ /**
1623
+ * Report a user to platform admins. `reason` is free-text context
1624
+ * for the reviewing admin — keep it specific and factual.
1625
+ */
1626
+ async reportUser(userId, reason, options) {
1627
+ return this.rawRequest({
1628
+ method: "POST",
1629
+ path: "/reports",
1630
+ body: { target_type: "user", target_id: userId, reason },
1631
+ signal: options?.signal
1632
+ });
1633
+ }
1634
+ /** Report a direct message to platform admins. */
1635
+ async reportMessage(messageId, reason, options) {
1636
+ return this.rawRequest({
1637
+ method: "POST",
1638
+ path: "/reports",
1639
+ body: { target_type: "message", target_id: messageId, reason },
1640
+ signal: options?.signal
1641
+ });
1642
+ }
1643
+ /** Report a post to platform admins. */
1644
+ async reportPost(postId, reason, options) {
1645
+ return this.rawRequest({
1646
+ method: "POST",
1647
+ path: "/reports",
1648
+ body: { target_type: "post", target_id: postId, reason },
1649
+ signal: options?.signal
1650
+ });
1651
+ }
1652
+ /** Report a comment to platform admins. */
1653
+ async reportComment(commentId, reason, options) {
1654
+ return this.rawRequest({
1655
+ method: "POST",
1656
+ path: "/reports",
1657
+ body: { target_type: "comment", target_id: commentId, reason },
1658
+ signal: options?.signal
1659
+ });
1660
+ }
1517
1661
  // ── Notifications ───────────────────────────────────────────────
1518
1662
  /** Get notifications (replies, mentions, etc.). Returns a bare array. */
1519
1663
  async getNotifications(options = {}) {