@openmarket/rooms-client 0.7.0 → 0.8.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/README.md CHANGED
@@ -22,4 +22,23 @@ Bun consumers resolve raw TypeScript via the `bun` export condition; Node and bu
22
22
 
23
23
  ## Versioning
24
24
 
25
- The npm version is managed by hand: bump `package.json` here, `npm publish`, then bump the consumers' pins. The monorepo's release script deliberately skips this package's `package.json`. `src/version.ts` is the exception: it carries the wire/runner version and is auto-bumped with every `om` release so the WebSocket handshake tracks the daemon.
25
+ The npm version is released separately from the OpenMarket application. The
26
+ monorepo release script deliberately skips this package's `package.json`;
27
+ `src/version.ts` is the exception because it carries the wire/runner version.
28
+
29
+ Prepare a version commit with the guarded package gates:
30
+
31
+ ```bash
32
+ bun run rooms-client:release prepare 0.8.0
33
+ ```
34
+
35
+ After that version commit is merged, publish only from a clean, current
36
+ `main` checkout:
37
+
38
+ ```bash
39
+ bun run rooms-client:release publish 0.8.0
40
+ ```
41
+
42
+ The publish command verifies npm identity and version availability, reruns
43
+ the package gates, publishes, and prints the update commands for
44
+ `openmarket-chat` and `openmarket-chat-cloud`. It never commits or pushes.
package/dist/client.d.ts CHANGED
@@ -212,6 +212,7 @@ export interface RoomDmSendRequest {
212
212
  * URL. Stored on the message's imageUrl; the client signs keys on render.
213
213
  * Additive — omitted for text-only sends. */
214
214
  imageUrl?: string;
215
+ attachments?: RoomAttachmentInput[];
215
216
  metadata?: RoomForwardMetadata;
216
217
  }
217
218
  export interface RoomDmReplyRequest extends RoomDmSendRequest {
package/dist/client.js CHANGED
@@ -489,6 +489,9 @@ export async function sendRoomDmOnClient(client, request, timeoutMs) {
489
489
  ...(request.username ? { username: request.username } : {}),
490
490
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
491
491
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
492
+ ...(request.attachments?.length
493
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
494
+ : {}),
492
495
  ...(request.metadata ? { metadata: request.metadata } : {}),
493
496
  },
494
497
  timestamp: Date.now(),
@@ -511,6 +514,9 @@ export async function replyRoomDmOnClient(client, request, timeoutMs) {
511
514
  ...(request.username ? { username: request.username } : {}),
512
515
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
513
516
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
517
+ ...(request.attachments?.length
518
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
519
+ : {}),
514
520
  ...(request.metadata ? { metadata: request.metadata } : {}),
515
521
  },
516
522
  timestamp: Date.now(),
@@ -98,6 +98,7 @@ export declare const RoomServerMessageType: {
98
98
  readonly DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED";
99
99
  readonly DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED";
100
100
  readonly DM_REACTION_UPDATED: "DM_REACTION_UPDATED";
101
+ readonly DM_ATTACHMENT_UPDATED: "DM_ATTACHMENT_UPDATED";
101
102
  readonly DM_PIN_UPDATED: "DM_PIN_UPDATED";
102
103
  readonly DM_PINS: "DM_PINS";
103
104
  readonly TOPIC: "TOPIC";
@@ -278,10 +279,11 @@ export interface RoomWebhookMetadata {
278
279
  }
279
280
  export interface RoomAttachment {
280
281
  /**
281
- * Object key under room-attachments/; clients sign it at render time.
282
- * Absent while scanStatus is "pending" on entries served to members: the
283
- * store withholds the signable key until the scan clears, so render the
284
- * metadata as a placeholder and do not attempt signing without a key.
282
+ * Object key under room-attachments/ or dm-attachments/; clients sign it
283
+ * at render time. Absent while scanStatus is "pending" on entries served
284
+ * to recipients: the store withholds the signable key until the scan
285
+ * clears, so render the metadata as a placeholder and do not attempt
286
+ * signing without a key.
285
287
  */
286
288
  key?: string | undefined;
287
289
  name: string;
@@ -292,7 +294,7 @@ export interface RoomAttachment {
292
294
  /**
293
295
  * A postable attachment reference. Outbound frames must always carry the
294
296
  * stored key (senders reference their own uploads, which include it); only
295
- * read views of pending entries may be keyless, and those must never be
297
+ * recipient views of pending entries may be keyless, and those must never be
296
298
  * forwarded back into a post.
297
299
  */
298
300
  export type RoomAttachmentInput = RoomAttachment & {
@@ -619,6 +621,7 @@ export interface RoomDmSendPayload {
619
621
  /** DM-attachment key or legacy public image URL; stored on the message's
620
622
  * imageUrl. Additive — text-only sends omit it. */
621
623
  imageUrl?: string;
624
+ attachments?: RoomAttachmentInput[];
622
625
  metadata?: RoomForwardMetadata | undefined;
623
626
  }
624
627
  /** Client → server: send a DM whose parent is another message in the same conversation. */
@@ -677,6 +680,12 @@ export interface RoomDmReactionUpdatedPayload {
677
680
  }>;
678
681
  participant: RoomDmParticipant;
679
682
  }
683
+ export interface RoomDmAttachmentUpdatedPayload {
684
+ conversationId: string;
685
+ messageId: string;
686
+ scanStatus: "clean" | "held";
687
+ attachments: RoomAttachment[];
688
+ }
680
689
  export interface RoomDmPinPayload {
681
690
  requestId?: string;
682
691
  conversationId?: string;
@@ -1471,6 +1480,10 @@ export type RoomServerMessage = {
1471
1480
  type: typeof RoomServerMessageType.DM_REACTION_UPDATED;
1472
1481
  payload: RoomDmReactionUpdatedPayload;
1473
1482
  timestamp: number;
1483
+ } | {
1484
+ type: typeof RoomServerMessageType.DM_ATTACHMENT_UPDATED;
1485
+ payload: RoomDmAttachmentUpdatedPayload;
1486
+ timestamp: number;
1474
1487
  } | {
1475
1488
  type: typeof RoomServerMessageType.DM_PIN_UPDATED;
1476
1489
  payload: RoomDmPinUpdatedPayload;
@@ -97,6 +97,7 @@ export const RoomServerMessageType = {
97
97
  DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
98
98
  DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
99
99
  DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
100
+ DM_ATTACHMENT_UPDATED: "DM_ATTACHMENT_UPDATED",
100
101
  DM_PIN_UPDATED: "DM_PIN_UPDATED",
101
102
  DM_PINS: "DM_PINS",
102
103
  TOPIC: "TOPIC",
@@ -450,13 +450,27 @@ export declare function markRoomUnread(config: RoomSocialConfig, room: string, s
450
450
  * one. The pinned ROOM_SET_ATTENTION frame (setRoomAttentionOnClient)
451
451
  * predates `untilMs` and stays the live-session path for permanent
452
452
  * postures; servers older than mute-with-duration strip the field and land
453
- * the mute permanent, so a requested `untilMs` the echo omits fail-closes:
454
- * the attention is reset and the call throws rather than leave an
455
- * unbounded mute the caller believes timed. Echoes the persisted
456
- * attention; `muteUntil` is the ISO expiry, null when none stands.
453
+ * the mute permanent, so a requested `untilMs` the echo omits is a DROPPED
454
+ * DEADLINE. By default that fail-closes here: the attention is reset and
455
+ * the call throws rather than leave an unbounded mute the caller believes
456
+ * timed. Callers with a richer fail-closed seam of their own opt out via
457
+ * `onDroppedDeadline: "resolve"` and own the handling (see the option).
458
+ * Echoes the persisted attention; `muteUntil` is the ISO expiry, null when
459
+ * none stands (which, under "resolve", is how a dropped deadline reads).
457
460
  */
458
461
  export declare function setRoomAttentionRest(config: RoomSocialConfig, room: string, attention: RoomTopicAttention, options?: {
459
462
  untilMs?: number | undefined;
463
+ /** What to do when a requested `untilMs` comes back without an echoed
464
+ * `muteUntil` (the server predates timed mutes and landed the mute
465
+ * PERMANENT). Default "undo-and-throw": reset attention to 'default'
466
+ * and throw RoomSocialError 502, so no unbounded mute stands. Pass
467
+ * "resolve" ONLY when the caller implements its own dropped-deadline
468
+ * handling: the call then performs no undo write and no throw, and
469
+ * resolves with `muteUntil: null` for the caller to act on. Canonical
470
+ * example: the GUI session seam, which undoes to the last
471
+ * WS-confirmed attention, richer than this library's
472
+ * undo-to-default. */
473
+ onDroppedDeadline?: "undo-and-throw" | "resolve" | undefined;
460
474
  }): Promise<{
461
475
  attention: RoomTopicAttention;
462
476
  muteUntil: string | null;
@@ -590,8 +604,10 @@ export declare function socialRequest<T>(config: RoomSocialConfig, path: string,
590
604
  formData?: FormData;
591
605
  headers?: Record<string, string>;
592
606
  /** Fetch cache mode; token-bearing reads pass "no-store" so posting
593
- * credentials never persist in a browser HTTP cache. */
594
- cache?: RequestCache;
607
+ * credentials never persist in a browser HTTP cache. Spelled as the
608
+ * literal union rather than DOM's RequestCache: this package
609
+ * typechecks without the DOM lib. */
610
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
595
611
  }): Promise<T>;
596
612
  export declare function cleanUsername(username: string): string;
597
613
  export declare function cleanRoomId(room: string): string;
@@ -724,10 +724,13 @@ export async function markRoomUnread(config, room, seq) {
724
724
  * one. The pinned ROOM_SET_ATTENTION frame (setRoomAttentionOnClient)
725
725
  * predates `untilMs` and stays the live-session path for permanent
726
726
  * postures; servers older than mute-with-duration strip the field and land
727
- * the mute permanent, so a requested `untilMs` the echo omits fail-closes:
728
- * the attention is reset and the call throws rather than leave an
729
- * unbounded mute the caller believes timed. Echoes the persisted
730
- * attention; `muteUntil` is the ISO expiry, null when none stands.
727
+ * the mute permanent, so a requested `untilMs` the echo omits is a DROPPED
728
+ * DEADLINE. By default that fail-closes here: the attention is reset and
729
+ * the call throws rather than leave an unbounded mute the caller believes
730
+ * timed. Callers with a richer fail-closed seam of their own opt out via
731
+ * `onDroppedDeadline: "resolve"` and own the handling (see the option).
732
+ * Echoes the persisted attention; `muteUntil` is the ISO expiry, null when
733
+ * none stands (which, under "resolve", is how a dropped deadline reads).
731
734
  */
732
735
  export async function setRoomAttentionRest(config, room, attention, options = {}) {
733
736
  const body = { attention };
@@ -738,7 +741,9 @@ export async function setRoomAttentionRest(config, room, attention, options = {}
738
741
  body,
739
742
  });
740
743
  const muteUntil = typeof result.muteUntil === "string" ? result.muteUntil : null;
741
- if (options.untilMs !== undefined && muteUntil === null) {
744
+ if (options.untilMs !== undefined &&
745
+ muteUntil === null &&
746
+ options.onDroppedDeadline !== "resolve") {
742
747
  // The undo cannot know the caller's prior attention, so it deliberately
743
748
  // lands 'default' (the same neutral a lapsed timed mute reads as, see
744
749
  // resolveTopicAttention) rather than leave the unbounded mute standing.
@@ -125,6 +125,8 @@ export interface RoomDmMessage {
125
125
  /** DM image: a private dm-attachments/ key (signed on render) or a legacy
126
126
  * public URL. Additive — old messages carry none. */
127
127
  imageUrl?: string | null;
128
+ /** Private scanned attachments. Keys are absent until the bytes are clean. */
129
+ attachments?: RoomAttachment[];
128
130
  /** Aggregated reactions (per-emoji userId lists). Additive. */
129
131
  reactions?: Array<{
130
132
  emoji: string;
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPollOption, RoomPollSummary, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUnfurl, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
1
+ export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmAttachmentUpdatedPayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPollOption, RoomPollSummary, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUnfurl, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
2
2
  export { applyRoomPresenceDelta, isSystemRoomLedgerEntryType, ROOM_PRESENCE_SAMPLE_LIMIT, RoomClientMessageType, RoomLedgerEntryType, RoomServerMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION, } from "./shared/rooms-protocol.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmarket/rooms-client",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -170,9 +170,7 @@ export function ownSeqsOf(entries: ReadonlyArray<RoomLedgerEntry>, ownUserId: st
170
170
  * autonomous turn, and webhook chatter cannot satisfy the
171
171
  * human-activity brake. */
172
172
  function isWebhookEntry(entry: RoomLedgerEntry): boolean {
173
- return Boolean(
174
- (entry.metadata as { webhook?: unknown } | undefined)?.webhook,
175
- );
173
+ return Boolean((entry.metadata as { webhook?: unknown } | undefined)?.webhook);
176
174
  }
177
175
 
178
176
  export function isTriggeringEntry(
package/src/client.ts CHANGED
@@ -297,6 +297,7 @@ export interface RoomDmSendRequest {
297
297
  * URL. Stored on the message's imageUrl; the client signs keys on render.
298
298
  * Additive — omitted for text-only sends. */
299
299
  imageUrl?: string;
300
+ attachments?: RoomAttachmentInput[];
300
301
  metadata?: RoomForwardMetadata;
301
302
  }
302
303
 
@@ -994,6 +995,9 @@ export async function sendRoomDmOnClient(
994
995
  ...(request.username ? { username: request.username } : {}),
995
996
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
996
997
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
998
+ ...(request.attachments?.length
999
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
1000
+ : {}),
997
1001
  ...(request.metadata ? { metadata: request.metadata } : {}),
998
1002
  },
999
1003
  timestamp: Date.now(),
@@ -1027,6 +1031,9 @@ export async function replyRoomDmOnClient(
1027
1031
  ...(request.username ? { username: request.username } : {}),
1028
1032
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
1029
1033
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
1034
+ ...(request.attachments?.length
1035
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
1036
+ : {}),
1030
1037
  ...(request.metadata ? { metadata: request.metadata } : {}),
1031
1038
  },
1032
1039
  timestamp: Date.now(),
@@ -101,6 +101,7 @@ export const RoomServerMessageType = {
101
101
  DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
102
102
  DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
103
103
  DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
104
+ DM_ATTACHMENT_UPDATED: "DM_ATTACHMENT_UPDATED",
104
105
  DM_PIN_UPDATED: "DM_PIN_UPDATED",
105
106
  DM_PINS: "DM_PINS",
106
107
  TOPIC: "TOPIC",
@@ -278,7 +279,9 @@ export interface RoomLedgerEntry {
278
279
  * (and servers older than the unfurl worker) omit it. */
279
280
  unfurls?: RoomUnfurl[] | undefined;
280
281
  roomRole?: RoomRole | null | undefined;
281
- metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
282
+ metadata?:
283
+ | (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>)
284
+ | undefined;
282
285
  }
283
286
 
284
287
  export type RoomForwardedFrom =
@@ -312,10 +315,11 @@ export interface RoomWebhookMetadata {
312
315
 
313
316
  export interface RoomAttachment {
314
317
  /**
315
- * Object key under room-attachments/; clients sign it at render time.
316
- * Absent while scanStatus is "pending" on entries served to members: the
317
- * store withholds the signable key until the scan clears, so render the
318
- * metadata as a placeholder and do not attempt signing without a key.
318
+ * Object key under room-attachments/ or dm-attachments/; clients sign it
319
+ * at render time. Absent while scanStatus is "pending" on entries served
320
+ * to recipients: the store withholds the signable key until the scan
321
+ * clears, so render the metadata as a placeholder and do not attempt
322
+ * signing without a key.
319
323
  */
320
324
  key?: string | undefined;
321
325
  name: string;
@@ -327,7 +331,7 @@ export interface RoomAttachment {
327
331
  /**
328
332
  * A postable attachment reference. Outbound frames must always carry the
329
333
  * stored key (senders reference their own uploads, which include it); only
330
- * read views of pending entries may be keyless, and those must never be
334
+ * recipient views of pending entries may be keyless, and those must never be
331
335
  * forwarded back into a post.
332
336
  */
333
337
  export type RoomAttachmentInput = RoomAttachment & { key: string };
@@ -683,7 +687,9 @@ export interface RoomDmMessage {
683
687
  createdAt?: string | number | undefined;
684
688
  /** Sealed DM: content is '' and the opaque envelope rides here. */
685
689
  secret?: RoomLedgerSecretPayload | null | undefined;
686
- metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
690
+ metadata?:
691
+ | (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>)
692
+ | undefined;
687
693
  /** Legacy public chat-image URL (renders as-is, no signing). */
688
694
  imageUrl?: string | null | undefined;
689
695
  /** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
@@ -718,6 +724,7 @@ export interface RoomDmSendPayload {
718
724
  /** DM-attachment key or legacy public image URL; stored on the message's
719
725
  * imageUrl. Additive — text-only sends omit it. */
720
726
  imageUrl?: string;
727
+ attachments?: RoomAttachmentInput[];
721
728
  metadata?: RoomForwardMetadata | undefined;
722
729
  }
723
730
 
@@ -781,6 +788,13 @@ export interface RoomDmReactionUpdatedPayload {
781
788
  participant: RoomDmParticipant;
782
789
  }
783
790
 
791
+ export interface RoomDmAttachmentUpdatedPayload {
792
+ conversationId: string;
793
+ messageId: string;
794
+ scanStatus: "clean" | "held";
795
+ attachments: RoomAttachment[];
796
+ }
797
+
784
798
  export interface RoomDmPinPayload {
785
799
  requestId?: string;
786
800
  conversationId?: string;
@@ -1653,6 +1667,11 @@ export type RoomServerMessage =
1653
1667
  payload: RoomDmReactionUpdatedPayload;
1654
1668
  timestamp: number;
1655
1669
  }
1670
+ | {
1671
+ type: typeof RoomServerMessageType.DM_ATTACHMENT_UPDATED;
1672
+ payload: RoomDmAttachmentUpdatedPayload;
1673
+ timestamp: number;
1674
+ }
1656
1675
  | {
1657
1676
  type: typeof RoomServerMessageType.DM_PIN_UPDATED;
1658
1677
  payload: RoomDmPinUpdatedPayload;
@@ -1456,16 +1456,32 @@ export async function markRoomUnread(
1456
1456
  * one. The pinned ROOM_SET_ATTENTION frame (setRoomAttentionOnClient)
1457
1457
  * predates `untilMs` and stays the live-session path for permanent
1458
1458
  * postures; servers older than mute-with-duration strip the field and land
1459
- * the mute permanent, so a requested `untilMs` the echo omits fail-closes:
1460
- * the attention is reset and the call throws rather than leave an
1461
- * unbounded mute the caller believes timed. Echoes the persisted
1462
- * attention; `muteUntil` is the ISO expiry, null when none stands.
1459
+ * the mute permanent, so a requested `untilMs` the echo omits is a DROPPED
1460
+ * DEADLINE. By default that fail-closes here: the attention is reset and
1461
+ * the call throws rather than leave an unbounded mute the caller believes
1462
+ * timed. Callers with a richer fail-closed seam of their own opt out via
1463
+ * `onDroppedDeadline: "resolve"` and own the handling (see the option).
1464
+ * Echoes the persisted attention; `muteUntil` is the ISO expiry, null when
1465
+ * none stands (which, under "resolve", is how a dropped deadline reads).
1463
1466
  */
1464
1467
  export async function setRoomAttentionRest(
1465
1468
  config: RoomSocialConfig,
1466
1469
  room: string,
1467
1470
  attention: RoomTopicAttention,
1468
- options: { untilMs?: number | undefined } = {},
1471
+ options: {
1472
+ untilMs?: number | undefined;
1473
+ /** What to do when a requested `untilMs` comes back without an echoed
1474
+ * `muteUntil` (the server predates timed mutes and landed the mute
1475
+ * PERMANENT). Default "undo-and-throw": reset attention to 'default'
1476
+ * and throw RoomSocialError 502, so no unbounded mute stands. Pass
1477
+ * "resolve" ONLY when the caller implements its own dropped-deadline
1478
+ * handling: the call then performs no undo write and no throw, and
1479
+ * resolves with `muteUntil: null` for the caller to act on. Canonical
1480
+ * example: the GUI session seam, which undoes to the last
1481
+ * WS-confirmed attention, richer than this library's
1482
+ * undo-to-default. */
1483
+ onDroppedDeadline?: "undo-and-throw" | "resolve" | undefined;
1484
+ } = {},
1469
1485
  ): Promise<{ attention: RoomTopicAttention; muteUntil: string | null }> {
1470
1486
  const body: { attention: RoomTopicAttention; untilMs?: number } = { attention };
1471
1487
  if (options.untilMs !== undefined) body.untilMs = options.untilMs;
@@ -1479,7 +1495,11 @@ export async function setRoomAttentionRest(
1479
1495
  body,
1480
1496
  });
1481
1497
  const muteUntil = typeof result.muteUntil === "string" ? result.muteUntil : null;
1482
- if (options.untilMs !== undefined && muteUntil === null) {
1498
+ if (
1499
+ options.untilMs !== undefined &&
1500
+ muteUntil === null &&
1501
+ options.onDroppedDeadline !== "resolve"
1502
+ ) {
1483
1503
  // The undo cannot know the caller's prior attention, so it deliberately
1484
1504
  // lands 'default' (the same neutral a lapsed timed mute reads as, see
1485
1505
  // resolveTopicAttention) rather than leave the unbounded mute standing.
@@ -1914,8 +1934,10 @@ export async function socialRequest<T>(
1914
1934
  formData?: FormData;
1915
1935
  headers?: Record<string, string>;
1916
1936
  /** Fetch cache mode; token-bearing reads pass "no-store" so posting
1917
- * credentials never persist in a browser HTTP cache. */
1918
- cache?: RequestCache;
1937
+ * credentials never persist in a browser HTTP cache. Spelled as the
1938
+ * literal union rather than DOM's RequestCache: this package
1939
+ * typechecks without the DOM lib. */
1940
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
1919
1941
  } = {},
1920
1942
  ): Promise<T> {
1921
1943
  const url = `${trimTrailingSlash(config.apiUrl)}${path}`;
@@ -147,6 +147,8 @@ export interface RoomDmMessage {
147
147
  /** DM image: a private dm-attachments/ key (signed on render) or a legacy
148
148
  * public URL. Additive — old messages carry none. */
149
149
  imageUrl?: string | null;
150
+ /** Private scanned attachments. Keys are absent until the bytes are clean. */
151
+ attachments?: RoomAttachment[];
150
152
  /** Aggregated reactions (per-emoji userId lists). Additive. */
151
153
  reactions?: Array<{ emoji: string; users: string[] }>;
152
154
  /** Shared pin state. Additive; absent means unpinned. */
package/src/types.ts CHANGED
@@ -11,6 +11,7 @@ export type {
11
11
  RoomClientPlatform,
12
12
  RoomCreatedPayload,
13
13
  RoomCreatePayload,
14
+ RoomDmAttachmentUpdatedPayload,
14
15
  RoomDmHistoryPayload,
15
16
  RoomDmHistoryResultPayload,
16
17
  RoomDmInboxUpdatedPayload,