@openmarket/rooms-client 0.2.1 → 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/dist/client.d.ts +25 -5
- package/dist/client.js +41 -8
- package/dist/shared/rooms-protocol.d.ts +46 -3
- package/dist/shared/rooms-protocol.js +1 -0
- package/dist/social-client.d.ts +8 -1
- package/dist/social-client.js +84 -3
- package/dist/social-types.d.ts +16 -3
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/client.ts +57 -9
- package/src/shared/rooms-protocol.ts +51 -4
- package/src/social-client.ts +81 -2
- package/src/social-types.ts +20 -4
- package/src/types.ts +3 -0
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RoomAccessMode, type
|
|
1
|
+
import { type RoomAccessMode, type RoomAttachmentInput, type RoomAuditLogPayload, type RoomAuthSuccessPayload, type RoomBackfillPayload, type RoomClientPlatform, type RoomDmHistoryResultPayload, type RoomDmMessagePayload, type RoomDmOpenedPayload, type RoomFriendPresence, type RoomInvitedPayload, type RoomLedgerEntry, type RoomMemberRemovedPayload, type RoomMemberRoleChangedPayload, type RoomMeta, type RoomPinsPayload, type RoomPresencePayload, type RoomReactorsPayload, type RoomReadStatePayload, type RoomReportedPayload, type RoomRole, type RoomSearchResultsPayload, type RoomSearchScope, type RoomSpace, type RoomThreadPayload, type RoomTopic, type RoomTopicAttention, type RoomTopicResultPayload, type RoomTopicsResultPayload, type RoomUserStatus, type RoomWhoPayload } from "./types.js";
|
|
2
2
|
import { type RoomWebSocketCtor, RoomWsClient } from "./ws-client.js";
|
|
3
3
|
export interface RoomClientConfig {
|
|
4
4
|
url: string;
|
|
@@ -39,7 +39,7 @@ export interface RoomJoinOptions {
|
|
|
39
39
|
}
|
|
40
40
|
export interface RoomSayOptions extends RoomJoinOptions {
|
|
41
41
|
text?: string;
|
|
42
|
-
attachments?:
|
|
42
|
+
attachments?: RoomAttachmentInput[];
|
|
43
43
|
inReplyTo?: number;
|
|
44
44
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
45
45
|
topicId?: string;
|
|
@@ -283,9 +283,18 @@ export declare function joinRoom(config: RoomClientConfig, options: RoomJoinOpti
|
|
|
283
283
|
/** Re-join a room on an already-connected client (used after a reconnect). */
|
|
284
284
|
export declare function rejoinRoomOnClient(client: RoomWsClient, options: RoomJoinOptions): Promise<RoomBackfillPayload>;
|
|
285
285
|
export declare function attachRejoinOnReconnect(client: RoomWsClient, options: RoomJoinOptions, latestSeq: () => number | undefined, onRejoined?: (backfill: RoomBackfillPayload) => void, onError?: (err: unknown) => void): () => void;
|
|
286
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Post into a joined room and return the frame's clientMsgId (what MESSAGE
|
|
288
|
+
* echoes carry back for optimistic-send reconciliation). `clientMsgId` is
|
|
289
|
+
* caller-suppliable so an offline send queue can retry one logical message
|
|
290
|
+
* under one stable id (same contract as sayRoom / sendRoomDmOnClient);
|
|
291
|
+
* omitted, it mints a UUID exactly as before. The relay validates the id
|
|
292
|
+
* (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
|
|
293
|
+
* work and NOT implied here.
|
|
294
|
+
*/
|
|
295
|
+
export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string, clientMsgId?: string): string;
|
|
287
296
|
export declare function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void;
|
|
288
|
-
export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null): void;
|
|
297
|
+
export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): void;
|
|
289
298
|
/**
|
|
290
299
|
* Fetch a joined room's meta (title, access, postingRoles, viewerRole) over
|
|
291
300
|
* the live socket via a scoped SEARCH. Returns null on timeout/no match so
|
|
@@ -310,7 +319,18 @@ export declare function requestRoomReactors(client: RoomWsClient, request: {
|
|
|
310
319
|
* persists it durably; a one-shot socket has no joined rooms, so live
|
|
311
320
|
* rosters pick the change up from the store (the TUI/GUI send STATUS on
|
|
312
321
|
* their live connection instead, which also broadcasts PRESENCE_DELTA). */
|
|
313
|
-
export declare function setRoomStatusOnce(config: RoomClientConfig, status: RoomUserStatus, statusText?: string | null): Promise<void>;
|
|
322
|
+
export declare function setRoomStatusOnce(config: RoomClientConfig, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): Promise<void>;
|
|
323
|
+
/**
|
|
324
|
+
* Set the caller's room-level attention (notification posture) on a live
|
|
325
|
+
* client. The relay validates room participation, persists through the
|
|
326
|
+
* store, and acks with a READ_STATE frame scoped to this room whose
|
|
327
|
+
* attention map carries the persisted value (readState stays empty: cursors
|
|
328
|
+
* are untouched). Private per-user state; nothing broadcasts.
|
|
329
|
+
*/
|
|
330
|
+
export declare function setRoomAttentionOnClient(client: RoomWsClient, request: {
|
|
331
|
+
room: string;
|
|
332
|
+
attention: RoomTopicAttention;
|
|
333
|
+
}): Promise<RoomReadStatePayload>;
|
|
314
334
|
/** Add/remove one reaction (one-shot). Returns the updated entry. */
|
|
315
335
|
export declare function reactRoom(config: RoomClientConfig, options: {
|
|
316
336
|
room: string;
|
package/dist/client.js
CHANGED
|
@@ -618,21 +618,30 @@ export function attachRejoinOnReconnect(client, options, latestSeq, onRejoined,
|
|
|
618
618
|
.catch((err) => onError?.(err));
|
|
619
619
|
});
|
|
620
620
|
}
|
|
621
|
-
|
|
622
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Post into a joined room and return the frame's clientMsgId (what MESSAGE
|
|
623
|
+
* echoes carry back for optimistic-send reconciliation). `clientMsgId` is
|
|
624
|
+
* caller-suppliable so an offline send queue can retry one logical message
|
|
625
|
+
* under one stable id (same contract as sayRoom / sendRoomDmOnClient);
|
|
626
|
+
* omitted, it mints a UUID exactly as before. The relay validates the id
|
|
627
|
+
* (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
|
|
628
|
+
* work and NOT implied here.
|
|
629
|
+
*/
|
|
630
|
+
export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId, clientMsgId) {
|
|
631
|
+
const msgId = clientMsgId ?? randomUUID();
|
|
623
632
|
client.send({
|
|
624
633
|
type: RoomClientMessageType.POST,
|
|
625
634
|
payload: {
|
|
626
635
|
room,
|
|
627
636
|
...(text ? { text } : {}),
|
|
628
637
|
...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
|
|
629
|
-
clientMsgId,
|
|
638
|
+
clientMsgId: msgId,
|
|
630
639
|
...(inReplyTo !== undefined ? { inReplyTo } : {}),
|
|
631
640
|
...(topicId ? { topicId } : {}),
|
|
632
641
|
},
|
|
633
642
|
timestamp: Date.now(),
|
|
634
643
|
});
|
|
635
|
-
return
|
|
644
|
+
return msgId;
|
|
636
645
|
}
|
|
637
646
|
export function sendRoomTyping(client, room, state) {
|
|
638
647
|
if (client.isGuest())
|
|
@@ -643,10 +652,16 @@ export function sendRoomTyping(client, room, state) {
|
|
|
643
652
|
timestamp: Date.now(),
|
|
644
653
|
});
|
|
645
654
|
}
|
|
646
|
-
export function sendRoomStatus(client, status, statusText) {
|
|
655
|
+
export function sendRoomStatus(client, status, statusText, platform) {
|
|
647
656
|
client.send({
|
|
648
657
|
type: RoomClientMessageType.STATUS,
|
|
649
|
-
payload: {
|
|
658
|
+
payload: {
|
|
659
|
+
status,
|
|
660
|
+
...(statusText !== undefined ? { statusText } : {}),
|
|
661
|
+
// Session identity, declared once per connection (or on change): the
|
|
662
|
+
// relay carries it onto presence member entries.
|
|
663
|
+
...(platform ? { platform } : {}),
|
|
664
|
+
},
|
|
650
665
|
timestamp: Date.now(),
|
|
651
666
|
});
|
|
652
667
|
}
|
|
@@ -769,11 +784,11 @@ export async function requestRoomReactors(client, request) {
|
|
|
769
784
|
* persists it durably; a one-shot socket has no joined rooms, so live
|
|
770
785
|
* rosters pick the change up from the store (the TUI/GUI send STATUS on
|
|
771
786
|
* their live connection instead, which also broadcasts PRESENCE_DELTA). */
|
|
772
|
-
export async function setRoomStatusOnce(config, status, statusText) {
|
|
787
|
+
export async function setRoomStatusOnce(config, status, statusText, platform) {
|
|
773
788
|
const client = new RoomWsClient(config);
|
|
774
789
|
try {
|
|
775
790
|
await client.connect();
|
|
776
|
-
sendRoomStatus(client, status, statusText);
|
|
791
|
+
sendRoomStatus(client, status, statusText, platform);
|
|
777
792
|
// STATUS has no ack frame: give the socket a beat to flush before close.
|
|
778
793
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
779
794
|
}
|
|
@@ -781,6 +796,24 @@ export async function setRoomStatusOnce(config, status, statusText) {
|
|
|
781
796
|
client.close();
|
|
782
797
|
}
|
|
783
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Set the caller's room-level attention (notification posture) on a live
|
|
801
|
+
* client. The relay validates room participation, persists through the
|
|
802
|
+
* store, and acks with a READ_STATE frame scoped to this room whose
|
|
803
|
+
* attention map carries the persisted value (readState stays empty: cursors
|
|
804
|
+
* are untouched). Private per-user state; nothing broadcasts.
|
|
805
|
+
*/
|
|
806
|
+
export async function setRoomAttentionOnClient(client, request) {
|
|
807
|
+
const requestId = randomUUID();
|
|
808
|
+
const room = normalizeRoomName(request.room);
|
|
809
|
+
client.send({
|
|
810
|
+
type: RoomClientMessageType.ROOM_SET_ATTENTION,
|
|
811
|
+
payload: { requestId, room, attention: request.attention },
|
|
812
|
+
timestamp: Date.now(),
|
|
813
|
+
});
|
|
814
|
+
const result = await client.waitFor(RoomServerMessageType.READ_STATE, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
815
|
+
return result.payload;
|
|
816
|
+
}
|
|
784
817
|
/** One-shot session helper for the config-based mutation verbs below (the
|
|
785
818
|
* agent tools' transport: connect, join, act, wait for the echo, close). */
|
|
786
819
|
async function withRoomEcho(config, room, seq, send) {
|
|
@@ -48,6 +48,7 @@ export declare const RoomClientMessageType: {
|
|
|
48
48
|
readonly TOPIC_RESOLVE: "TOPIC_RESOLVE";
|
|
49
49
|
readonly TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION";
|
|
50
50
|
readonly TOPIC_LIST: "TOPIC_LIST";
|
|
51
|
+
readonly ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION";
|
|
51
52
|
};
|
|
52
53
|
export type RoomClientMessageType = (typeof RoomClientMessageType)[keyof typeof RoomClientMessageType];
|
|
53
54
|
export declare const RoomServerMessageType: {
|
|
@@ -127,6 +128,9 @@ export type RoomRole = "owner" | "admin" | "mod" | "member";
|
|
|
127
128
|
* semantics of an absent value). */
|
|
128
129
|
export type RoomTopicsPolicy = "off" | "allowed" | "required";
|
|
129
130
|
export type RoomTopicAttention = "follow" | "default" | "mute";
|
|
131
|
+
/** Self-declared client class on STATUS frames; presence member entries
|
|
132
|
+
* carry it through so rosters can render where someone is connected from. */
|
|
133
|
+
export type RoomClientPlatform = "web" | "desktop" | "mobile" | "tui";
|
|
130
134
|
export interface RoomMember {
|
|
131
135
|
userId: string;
|
|
132
136
|
username: string;
|
|
@@ -140,6 +144,8 @@ export interface RoomMember {
|
|
|
140
144
|
joinedAt: number;
|
|
141
145
|
avatarUrl?: string | null;
|
|
142
146
|
statusText?: string | null;
|
|
147
|
+
/** Present when the member's session declared one via STATUS. */
|
|
148
|
+
platform?: RoomClientPlatform | undefined;
|
|
143
149
|
}
|
|
144
150
|
export interface RoomMeta {
|
|
145
151
|
name: string;
|
|
@@ -164,6 +170,10 @@ export interface RoomMeta {
|
|
|
164
170
|
* 2026-07-12 dial-default flip (post-flip relays always advertise the
|
|
165
171
|
* stored value, "off" included); explicit "off" means topics are inert. */
|
|
166
172
|
topicsPolicy?: RoomTopicsPolicy | undefined;
|
|
173
|
+
/** The viewer's room attention (notification posture), a per-viewer field
|
|
174
|
+
* like viewerRole: surfaced on rooms listings for the caller. Absent
|
|
175
|
+
* reads as "default". */
|
|
176
|
+
attention?: RoomTopicAttention | undefined;
|
|
167
177
|
tags?: string[] | undefined;
|
|
168
178
|
entities?: string[] | undefined;
|
|
169
179
|
sourceRefs?: Array<Record<string, unknown>> | undefined;
|
|
@@ -217,13 +227,27 @@ export interface RoomLedgerEntry {
|
|
|
217
227
|
roomRole?: RoomRole | null | undefined;
|
|
218
228
|
}
|
|
219
229
|
export interface RoomAttachment {
|
|
220
|
-
/**
|
|
221
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
232
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
233
|
+
* store withholds the signable key until the scan clears, so render the
|
|
234
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
235
|
+
*/
|
|
236
|
+
key?: string | undefined;
|
|
222
237
|
name: string;
|
|
223
238
|
size: number;
|
|
224
239
|
mime: string;
|
|
225
240
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
226
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* A postable attachment reference. Outbound frames must always carry the
|
|
244
|
+
* stored key (senders reference their own uploads, which include it); only
|
|
245
|
+
* read views of pending entries may be keyless, and those must never be
|
|
246
|
+
* forwarded back into a post.
|
|
247
|
+
*/
|
|
248
|
+
export type RoomAttachmentInput = RoomAttachment & {
|
|
249
|
+
key: string;
|
|
250
|
+
};
|
|
227
251
|
/**
|
|
228
252
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
229
253
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -281,7 +305,12 @@ export interface RoomPinsPayload {
|
|
|
281
305
|
requestId?: string | undefined;
|
|
282
306
|
}
|
|
283
307
|
export interface RoomReadStatePayload {
|
|
308
|
+
/** Partial map: rooms absent from a reply are unchanged. */
|
|
284
309
|
readState: Record<string, number>;
|
|
310
|
+
/** Per-room attention for the requesting user, keyed like readState.
|
|
311
|
+
* Present when the store surfaces it and on ROOM_SET_ATTENTION acks
|
|
312
|
+
* (which reply READ_STATE scoped to the one room, readState empty). */
|
|
313
|
+
attention?: Record<string, RoomTopicAttention> | undefined;
|
|
285
314
|
requestId?: string | undefined;
|
|
286
315
|
}
|
|
287
316
|
export interface RoomSearchResultsPayload {
|
|
@@ -729,6 +758,15 @@ export interface RoomTopicSetAttentionPayload {
|
|
|
729
758
|
topicId: string;
|
|
730
759
|
attention: RoomTopicAttention;
|
|
731
760
|
}
|
|
761
|
+
/** Room-level attention (notification posture). Private per-user state on
|
|
762
|
+
* the caller's read row; topic attention wins for topic-scoped messages
|
|
763
|
+
* and room attention wins over space attention. The relay acks with a
|
|
764
|
+
* READ_STATE frame scoped to the room (see RoomReadStatePayload). */
|
|
765
|
+
export interface RoomSetAttentionPayload {
|
|
766
|
+
requestId?: string;
|
|
767
|
+
room: string;
|
|
768
|
+
attention: RoomTopicAttention;
|
|
769
|
+
}
|
|
732
770
|
export interface RoomTopicListPayload {
|
|
733
771
|
requestId?: string;
|
|
734
772
|
room: string;
|
|
@@ -843,7 +881,7 @@ export type RoomClientMessage = {
|
|
|
843
881
|
payload: {
|
|
844
882
|
room: string;
|
|
845
883
|
text?: string;
|
|
846
|
-
attachments?:
|
|
884
|
+
attachments?: RoomAttachmentInput[];
|
|
847
885
|
inReplyTo?: number;
|
|
848
886
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
849
887
|
topicId?: string;
|
|
@@ -1003,6 +1041,7 @@ export type RoomClientMessage = {
|
|
|
1003
1041
|
payload: {
|
|
1004
1042
|
status: RoomUserStatus;
|
|
1005
1043
|
statusText?: string | null;
|
|
1044
|
+
platform?: RoomClientPlatform;
|
|
1006
1045
|
};
|
|
1007
1046
|
timestamp?: number;
|
|
1008
1047
|
} | {
|
|
@@ -1078,6 +1117,10 @@ export type RoomClientMessage = {
|
|
|
1078
1117
|
type: typeof RoomClientMessageType.TOPIC_LIST;
|
|
1079
1118
|
payload: RoomTopicListPayload;
|
|
1080
1119
|
timestamp?: number;
|
|
1120
|
+
} | {
|
|
1121
|
+
type: typeof RoomClientMessageType.ROOM_SET_ATTENTION;
|
|
1122
|
+
payload: RoomSetAttentionPayload;
|
|
1123
|
+
timestamp?: number;
|
|
1081
1124
|
};
|
|
1082
1125
|
export type RoomServerMessage = {
|
|
1083
1126
|
type: typeof RoomServerMessageType.AUTH_SUCCESS;
|
|
@@ -48,6 +48,7 @@ export const RoomClientMessageType = {
|
|
|
48
48
|
TOPIC_RESOLVE: "TOPIC_RESOLVE",
|
|
49
49
|
TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION",
|
|
50
50
|
TOPIC_LIST: "TOPIC_LIST",
|
|
51
|
+
ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION",
|
|
51
52
|
};
|
|
52
53
|
export const RoomServerMessageType = {
|
|
53
54
|
AUTH_SUCCESS: "AUTH_SUCCESS",
|
package/dist/social-client.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RoomDmConversation, RoomDmHistory, RoomDmSendResult, RoomDmUnreadConversation, RoomDoc, RoomDocAttribution, RoomDocChanges, RoomDocConflictHead, RoomDocEditingLease, RoomDocProvenance, RoomDocRevision, RoomDocRunRevertResult, RoomDocWriteResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequests, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomSocialConfig, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSuggestion, RoomSuggestionRequest } from "./social-types.js";
|
|
2
2
|
export type { RoomAttachment, RoomDmConversation, RoomDmHistory, RoomDmMessage, RoomDmParticipant, RoomDmSendResult, RoomDmUnreadConversation, RoomDocAttribution, RoomDocEditingLease, RoomDocMergeRegion, RoomDocRevisionSource, RoomDocRunRevertResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequest, RoomFriendRequests, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomReplyInboxItem, RoomSocialConfig, RoomSpaceRestoreAction, RoomSpaceRestoreActionKind, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSpaceRestoreSummary, RoomSuggestion, RoomSuggestionIntent, RoomSuggestionRequest, RoomSuggestionSource, } from "./social-types.js";
|
|
3
|
+
/** Largest per-file size any tier accepts (plus tier). The server enforces
|
|
4
|
+
* the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
|
|
5
|
+
* constant only pre-flights uploads no tier could ever accept. */
|
|
3
6
|
export declare const ROOM_ATTACHMENT_MAX_BYTES: number;
|
|
4
7
|
export declare class RoomSocialError extends Error {
|
|
5
8
|
readonly name = "RoomSocialError";
|
|
@@ -337,7 +340,10 @@ export declare function restoreRoomDoc(config: RoomSocialConfig, docId: string,
|
|
|
337
340
|
note?: string;
|
|
338
341
|
asAgent?: boolean;
|
|
339
342
|
}): Promise<RoomDoc>;
|
|
340
|
-
export declare function getRoomAttachmentUrl(config: RoomSocialConfig, room: string, key: string
|
|
343
|
+
export declare function getRoomAttachmentUrl(config: RoomSocialConfig, room: string, key: string, opts?: {
|
|
344
|
+
name?: string;
|
|
345
|
+
download?: boolean;
|
|
346
|
+
}): Promise<{
|
|
341
347
|
url: string;
|
|
342
348
|
expiresAt: string | null;
|
|
343
349
|
}>;
|
|
@@ -363,6 +369,7 @@ export declare function socialRequest<T>(config: RoomSocialConfig, path: string,
|
|
|
363
369
|
body?: unknown;
|
|
364
370
|
signal?: AbortSignal;
|
|
365
371
|
formData?: FormData;
|
|
372
|
+
headers?: Record<string, string>;
|
|
366
373
|
}): Promise<T>;
|
|
367
374
|
export declare function cleanUsername(username: string): string;
|
|
368
375
|
export declare function cleanRoomId(room: string): string;
|
package/dist/social-client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resolveRoomsWsUrl } from "./client.js";
|
|
2
2
|
import { envString, ROOM_CHAT_API_URL } from "./endpoints.js";
|
|
3
|
-
|
|
3
|
+
/** Largest per-file size any tier accepts (plus tier). The server enforces
|
|
4
|
+
* the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
|
|
5
|
+
* constant only pre-flights uploads no tier could ever accept. */
|
|
6
|
+
export const ROOM_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
|
|
4
7
|
export class RoomSocialError extends Error {
|
|
5
8
|
name = "RoomSocialError";
|
|
6
9
|
status;
|
|
@@ -517,9 +520,22 @@ export async function restoreRoomDoc(config, docId, input = {}) {
|
|
|
517
520
|
const result = await socialRequest(config, `/docs/${encodeURIComponent(docId)}/restore`, { method: "POST", body: input });
|
|
518
521
|
return result.doc;
|
|
519
522
|
}
|
|
520
|
-
export async function getRoomAttachmentUrl(config, room, key) {
|
|
523
|
+
export async function getRoomAttachmentUrl(config, room, key, opts = {}) {
|
|
524
|
+
// The object key identifies the object and stays in the query. The
|
|
525
|
+
// download filename and force-download flag ride REQUEST HEADERS, never
|
|
526
|
+
// the URL: a private-room filename in the query string leaked into the
|
|
527
|
+
// relay's access logs and error telemetry (Morgan, Sentry).
|
|
521
528
|
const params = new URLSearchParams({ key });
|
|
522
|
-
const
|
|
529
|
+
const headers = {};
|
|
530
|
+
// Percent-encode the name: Fetch/HTTP header values are byte strings, so a
|
|
531
|
+
// raw Unicode filename (报告.py, an emoji) throws a TypeError before the
|
|
532
|
+
// request is sent. The store decodes with decodeURIComponent.
|
|
533
|
+
if (opts.name)
|
|
534
|
+
headers["X-Om-Download-Name"] = encodeURIComponent(opts.name);
|
|
535
|
+
// Forces a Content-Disposition attachment even for inline mimes.
|
|
536
|
+
if (opts.download)
|
|
537
|
+
headers["X-Om-Download"] = "1";
|
|
538
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/attachment-url?${params.toString()}`, { method: "GET", headers });
|
|
523
539
|
if (!result.url) {
|
|
524
540
|
throw new RoomSocialError("Attachment sign failed", 502, result);
|
|
525
541
|
}
|
|
@@ -587,6 +603,7 @@ export async function socialRequest(config, path, init = {}) {
|
|
|
587
603
|
Authorization: `Bearer ${config.apiKey}`,
|
|
588
604
|
Accept: "application/json",
|
|
589
605
|
...(hasJsonBody ? { "Content-Type": "application/json" } : {}),
|
|
606
|
+
...(init.headers ?? {}),
|
|
590
607
|
},
|
|
591
608
|
...(hasJsonBody
|
|
592
609
|
? { body: JSON.stringify(init.body) }
|
|
@@ -658,6 +675,63 @@ function requestPath(url) {
|
|
|
658
675
|
function trimTrailingSlash(value) {
|
|
659
676
|
return value.replace(/\/+$/, "");
|
|
660
677
|
}
|
|
678
|
+
/** Source/config/text extensions declared as text/plain so the server's
|
|
679
|
+
* UTF-8 code-file lane accepts them. Markup extensions the server
|
|
680
|
+
* blocklists (html, svg, xml, ...) are deliberately absent: they fall
|
|
681
|
+
* through to application/octet-stream and the server rejects them. */
|
|
682
|
+
const CODE_TEXT_EXTENSIONS = new Set([
|
|
683
|
+
"js",
|
|
684
|
+
"jsx",
|
|
685
|
+
"ts",
|
|
686
|
+
"tsx",
|
|
687
|
+
"py",
|
|
688
|
+
"rb",
|
|
689
|
+
"go",
|
|
690
|
+
"rs",
|
|
691
|
+
"java",
|
|
692
|
+
"c",
|
|
693
|
+
"h",
|
|
694
|
+
"cc",
|
|
695
|
+
"cpp",
|
|
696
|
+
"hpp",
|
|
697
|
+
"cs",
|
|
698
|
+
"php",
|
|
699
|
+
"sh",
|
|
700
|
+
"bash",
|
|
701
|
+
"zsh",
|
|
702
|
+
"fish",
|
|
703
|
+
"ps1",
|
|
704
|
+
"sql",
|
|
705
|
+
"yaml",
|
|
706
|
+
"yml",
|
|
707
|
+
"toml",
|
|
708
|
+
"ini",
|
|
709
|
+
"cfg",
|
|
710
|
+
"conf",
|
|
711
|
+
"log",
|
|
712
|
+
"diff",
|
|
713
|
+
"patch",
|
|
714
|
+
"tex",
|
|
715
|
+
"r",
|
|
716
|
+
"jl",
|
|
717
|
+
"kt",
|
|
718
|
+
"kts",
|
|
719
|
+
"swift",
|
|
720
|
+
"scala",
|
|
721
|
+
"clj",
|
|
722
|
+
"cljs",
|
|
723
|
+
"ex",
|
|
724
|
+
"exs",
|
|
725
|
+
"erl",
|
|
726
|
+
"lua",
|
|
727
|
+
"pl",
|
|
728
|
+
"pm",
|
|
729
|
+
"dart",
|
|
730
|
+
"gradle",
|
|
731
|
+
"properties",
|
|
732
|
+
"bat",
|
|
733
|
+
"cmd",
|
|
734
|
+
]);
|
|
661
735
|
export function mimeFromFilename(filename) {
|
|
662
736
|
const lower = filename.toLowerCase();
|
|
663
737
|
if (lower.endsWith(".png"))
|
|
@@ -668,6 +742,10 @@ export function mimeFromFilename(filename) {
|
|
|
668
742
|
return "image/webp";
|
|
669
743
|
if (lower.endsWith(".gif"))
|
|
670
744
|
return "image/gif";
|
|
745
|
+
if (lower.endsWith(".mp4"))
|
|
746
|
+
return "video/mp4";
|
|
747
|
+
if (lower.endsWith(".webm"))
|
|
748
|
+
return "video/webm";
|
|
671
749
|
if (lower.endsWith(".pdf"))
|
|
672
750
|
return "application/pdf";
|
|
673
751
|
if (lower.endsWith(".txt"))
|
|
@@ -687,5 +765,8 @@ export function mimeFromFilename(filename) {
|
|
|
687
765
|
if (lower.endsWith(".pptx")) {
|
|
688
766
|
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
689
767
|
}
|
|
768
|
+
const dot = lower.lastIndexOf(".");
|
|
769
|
+
if (dot !== -1 && CODE_TEXT_EXTENSIONS.has(lower.slice(dot + 1)))
|
|
770
|
+
return "text/plain";
|
|
690
771
|
return "application/octet-stream";
|
|
691
772
|
}
|
package/dist/social-types.d.ts
CHANGED
|
@@ -44,16 +44,26 @@ export interface RoomFollowResult {
|
|
|
44
44
|
isNewFollow?: boolean;
|
|
45
45
|
}
|
|
46
46
|
export interface RoomAttachment {
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
49
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
50
|
+
* store withholds the signable key until the scan clears, so render the
|
|
51
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
52
|
+
*/
|
|
53
|
+
key?: string | undefined;
|
|
49
54
|
name: string;
|
|
50
55
|
size: number;
|
|
51
56
|
mime: string;
|
|
52
57
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
53
58
|
}
|
|
54
59
|
export interface RoomAttachmentUpload {
|
|
55
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The stored attachment plus its first signed URL, for immediate render.
|
|
62
|
+
* The uploader always receives the key; only member views of PENDING
|
|
63
|
+
* entries withhold it.
|
|
64
|
+
*/
|
|
56
65
|
attachment: RoomAttachment & {
|
|
66
|
+
key: string;
|
|
57
67
|
url: string;
|
|
58
68
|
expiresAt: string | null;
|
|
59
69
|
};
|
|
@@ -120,6 +130,9 @@ export interface RoomDmMessage {
|
|
|
120
130
|
emoji: string;
|
|
121
131
|
users: string[];
|
|
122
132
|
}>;
|
|
133
|
+
/** Client-minted idempotency tag echoed back on DM sends, so a sender's
|
|
134
|
+
* durable queue can settle its optimistic row. Additive (0.4.0). */
|
|
135
|
+
clientMsgId?: string;
|
|
123
136
|
}
|
|
124
137
|
export interface RoomDmHistory {
|
|
125
138
|
conversationId: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { RoomAccessMode, RoomAttachment, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
|
|
1
|
+
export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, 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, 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.
|
|
3
|
+
"version": "0.4.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",
|
package/src/client.ts
CHANGED
|
@@ -7,12 +7,13 @@ import { authFromApiKey } from "./jwt.js";
|
|
|
7
7
|
import { consumeRoomSuggestionAttribution } from "./suggestion-attribution.js";
|
|
8
8
|
import {
|
|
9
9
|
type RoomAccessMode,
|
|
10
|
-
type
|
|
10
|
+
type RoomAttachmentInput,
|
|
11
11
|
type RoomAuditLogPayload,
|
|
12
12
|
type RoomAuthSuccessPayload,
|
|
13
13
|
type RoomBackfillPayload,
|
|
14
14
|
type RoomClientMessage,
|
|
15
15
|
RoomClientMessageType,
|
|
16
|
+
type RoomClientPlatform,
|
|
16
17
|
type RoomDmHistoryResultPayload,
|
|
17
18
|
type RoomDmMessagePayload,
|
|
18
19
|
type RoomDmOpenedPayload,
|
|
@@ -82,7 +83,7 @@ export interface RoomJoinOptions {
|
|
|
82
83
|
|
|
83
84
|
export interface RoomSayOptions extends RoomJoinOptions {
|
|
84
85
|
text?: string;
|
|
85
|
-
attachments?:
|
|
86
|
+
attachments?: RoomAttachmentInput[];
|
|
86
87
|
inReplyTo?: number;
|
|
87
88
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
88
89
|
topicId?: string;
|
|
@@ -261,7 +262,9 @@ export interface RoomJoinedSession {
|
|
|
261
262
|
client: RoomWsClient;
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
function toRoomAttachmentPayloads(
|
|
265
|
+
function toRoomAttachmentPayloads(
|
|
266
|
+
attachments: readonly RoomAttachmentInput[],
|
|
267
|
+
): RoomAttachmentInput[] {
|
|
265
268
|
return attachments.map(({ key, name, size, mime }) => ({ key, name, size, mime }));
|
|
266
269
|
}
|
|
267
270
|
|
|
@@ -1145,28 +1148,38 @@ export function attachRejoinOnReconnect(
|
|
|
1145
1148
|
});
|
|
1146
1149
|
}
|
|
1147
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* Post into a joined room and return the frame's clientMsgId (what MESSAGE
|
|
1153
|
+
* echoes carry back for optimistic-send reconciliation). `clientMsgId` is
|
|
1154
|
+
* caller-suppliable so an offline send queue can retry one logical message
|
|
1155
|
+
* under one stable id (same contract as sayRoom / sendRoomDmOnClient);
|
|
1156
|
+
* omitted, it mints a UUID exactly as before. The relay validates the id
|
|
1157
|
+
* (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
|
|
1158
|
+
* work and NOT implied here.
|
|
1159
|
+
*/
|
|
1148
1160
|
export function postRoomMessage(
|
|
1149
1161
|
client: RoomWsClient,
|
|
1150
1162
|
room: string,
|
|
1151
1163
|
text: string,
|
|
1152
1164
|
inReplyTo?: number,
|
|
1153
|
-
attachments?:
|
|
1165
|
+
attachments?: RoomAttachmentInput[],
|
|
1154
1166
|
topicId?: string,
|
|
1167
|
+
clientMsgId?: string,
|
|
1155
1168
|
): string {
|
|
1156
|
-
const
|
|
1169
|
+
const msgId = clientMsgId ?? randomUUID();
|
|
1157
1170
|
client.send({
|
|
1158
1171
|
type: RoomClientMessageType.POST,
|
|
1159
1172
|
payload: {
|
|
1160
1173
|
room,
|
|
1161
1174
|
...(text ? { text } : {}),
|
|
1162
1175
|
...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
|
|
1163
|
-
clientMsgId,
|
|
1176
|
+
clientMsgId: msgId,
|
|
1164
1177
|
...(inReplyTo !== undefined ? { inReplyTo } : {}),
|
|
1165
1178
|
...(topicId ? { topicId } : {}),
|
|
1166
1179
|
},
|
|
1167
1180
|
timestamp: Date.now(),
|
|
1168
1181
|
});
|
|
1169
|
-
return
|
|
1182
|
+
return msgId;
|
|
1170
1183
|
}
|
|
1171
1184
|
|
|
1172
1185
|
export function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void {
|
|
@@ -1182,10 +1195,17 @@ export function sendRoomStatus(
|
|
|
1182
1195
|
client: RoomWsClient,
|
|
1183
1196
|
status: RoomUserStatus,
|
|
1184
1197
|
statusText?: string | null,
|
|
1198
|
+
platform?: RoomClientPlatform,
|
|
1185
1199
|
): void {
|
|
1186
1200
|
client.send({
|
|
1187
1201
|
type: RoomClientMessageType.STATUS,
|
|
1188
|
-
payload: {
|
|
1202
|
+
payload: {
|
|
1203
|
+
status,
|
|
1204
|
+
...(statusText !== undefined ? { statusText } : {}),
|
|
1205
|
+
// Session identity, declared once per connection (or on change): the
|
|
1206
|
+
// relay carries it onto presence member entries.
|
|
1207
|
+
...(platform ? { platform } : {}),
|
|
1208
|
+
},
|
|
1189
1209
|
timestamp: Date.now(),
|
|
1190
1210
|
});
|
|
1191
1211
|
}
|
|
@@ -1391,11 +1411,12 @@ export async function setRoomStatusOnce(
|
|
|
1391
1411
|
config: RoomClientConfig,
|
|
1392
1412
|
status: RoomUserStatus,
|
|
1393
1413
|
statusText?: string | null,
|
|
1414
|
+
platform?: RoomClientPlatform,
|
|
1394
1415
|
): Promise<void> {
|
|
1395
1416
|
const client = new RoomWsClient(config);
|
|
1396
1417
|
try {
|
|
1397
1418
|
await client.connect();
|
|
1398
|
-
sendRoomStatus(client, status, statusText);
|
|
1419
|
+
sendRoomStatus(client, status, statusText, platform);
|
|
1399
1420
|
// STATUS has no ack frame: give the socket a beat to flush before close.
|
|
1400
1421
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1401
1422
|
} finally {
|
|
@@ -1403,6 +1424,33 @@ export async function setRoomStatusOnce(
|
|
|
1403
1424
|
}
|
|
1404
1425
|
}
|
|
1405
1426
|
|
|
1427
|
+
/**
|
|
1428
|
+
* Set the caller's room-level attention (notification posture) on a live
|
|
1429
|
+
* client. The relay validates room participation, persists through the
|
|
1430
|
+
* store, and acks with a READ_STATE frame scoped to this room whose
|
|
1431
|
+
* attention map carries the persisted value (readState stays empty: cursors
|
|
1432
|
+
* are untouched). Private per-user state; nothing broadcasts.
|
|
1433
|
+
*/
|
|
1434
|
+
export async function setRoomAttentionOnClient(
|
|
1435
|
+
client: RoomWsClient,
|
|
1436
|
+
request: { room: string; attention: RoomTopicAttention },
|
|
1437
|
+
): Promise<RoomReadStatePayload> {
|
|
1438
|
+
const requestId = randomUUID();
|
|
1439
|
+
const room = normalizeRoomName(request.room);
|
|
1440
|
+
client.send({
|
|
1441
|
+
type: RoomClientMessageType.ROOM_SET_ATTENTION,
|
|
1442
|
+
payload: { requestId, room, attention: request.attention },
|
|
1443
|
+
timestamp: Date.now(),
|
|
1444
|
+
});
|
|
1445
|
+
const result = await client.waitFor(
|
|
1446
|
+
RoomServerMessageType.READ_STATE,
|
|
1447
|
+
(msg) => msg.payload.requestId === requestId,
|
|
1448
|
+
undefined,
|
|
1449
|
+
requestId,
|
|
1450
|
+
);
|
|
1451
|
+
return result.payload;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1406
1454
|
/** One-shot session helper for the config-based mutation verbs below (the
|
|
1407
1455
|
* agent tools' transport: connect, join, act, wait for the echo, close). */
|
|
1408
1456
|
async function withRoomEcho(
|
|
@@ -48,6 +48,7 @@ export const RoomClientMessageType = {
|
|
|
48
48
|
TOPIC_RESOLVE: "TOPIC_RESOLVE",
|
|
49
49
|
TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION",
|
|
50
50
|
TOPIC_LIST: "TOPIC_LIST",
|
|
51
|
+
ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION",
|
|
51
52
|
} as const;
|
|
52
53
|
|
|
53
54
|
export type RoomClientMessageType =
|
|
@@ -152,6 +153,9 @@ export type RoomRole = "owner" | "admin" | "mod" | "member";
|
|
|
152
153
|
* semantics of an absent value). */
|
|
153
154
|
export type RoomTopicsPolicy = "off" | "allowed" | "required";
|
|
154
155
|
export type RoomTopicAttention = "follow" | "default" | "mute";
|
|
156
|
+
/** Self-declared client class on STATUS frames; presence member entries
|
|
157
|
+
* carry it through so rosters can render where someone is connected from. */
|
|
158
|
+
export type RoomClientPlatform = "web" | "desktop" | "mobile" | "tui";
|
|
155
159
|
|
|
156
160
|
export interface RoomMember {
|
|
157
161
|
userId: string;
|
|
@@ -166,6 +170,8 @@ export interface RoomMember {
|
|
|
166
170
|
joinedAt: number;
|
|
167
171
|
avatarUrl?: string | null;
|
|
168
172
|
statusText?: string | null;
|
|
173
|
+
/** Present when the member's session declared one via STATUS. */
|
|
174
|
+
platform?: RoomClientPlatform | undefined;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
export interface RoomMeta {
|
|
@@ -191,6 +197,10 @@ export interface RoomMeta {
|
|
|
191
197
|
* 2026-07-12 dial-default flip (post-flip relays always advertise the
|
|
192
198
|
* stored value, "off" included); explicit "off" means topics are inert. */
|
|
193
199
|
topicsPolicy?: RoomTopicsPolicy | undefined;
|
|
200
|
+
/** The viewer's room attention (notification posture), a per-viewer field
|
|
201
|
+
* like viewerRole: surfaced on rooms listings for the caller. Absent
|
|
202
|
+
* reads as "default". */
|
|
203
|
+
attention?: RoomTopicAttention | undefined;
|
|
194
204
|
tags?: string[] | undefined;
|
|
195
205
|
entities?: string[] | undefined;
|
|
196
206
|
sourceRefs?: Array<Record<string, unknown>> | undefined;
|
|
@@ -246,14 +256,27 @@ export interface RoomLedgerEntry {
|
|
|
246
256
|
}
|
|
247
257
|
|
|
248
258
|
export interface RoomAttachment {
|
|
249
|
-
/**
|
|
250
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
261
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
262
|
+
* store withholds the signable key until the scan clears, so render the
|
|
263
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
264
|
+
*/
|
|
265
|
+
key?: string | undefined;
|
|
251
266
|
name: string;
|
|
252
267
|
size: number;
|
|
253
268
|
mime: string;
|
|
254
269
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
255
270
|
}
|
|
256
271
|
|
|
272
|
+
/**
|
|
273
|
+
* A postable attachment reference. Outbound frames must always carry the
|
|
274
|
+
* stored key (senders reference their own uploads, which include it); only
|
|
275
|
+
* read views of pending entries may be keyless, and those must never be
|
|
276
|
+
* forwarded back into a post.
|
|
277
|
+
*/
|
|
278
|
+
export type RoomAttachmentInput = RoomAttachment & { key: string };
|
|
279
|
+
|
|
257
280
|
/**
|
|
258
281
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
259
282
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -318,7 +341,12 @@ export interface RoomPinsPayload {
|
|
|
318
341
|
}
|
|
319
342
|
|
|
320
343
|
export interface RoomReadStatePayload {
|
|
344
|
+
/** Partial map: rooms absent from a reply are unchanged. */
|
|
321
345
|
readState: Record<string, number>;
|
|
346
|
+
/** Per-room attention for the requesting user, keyed like readState.
|
|
347
|
+
* Present when the store surfaces it and on ROOM_SET_ATTENTION acks
|
|
348
|
+
* (which reply READ_STATE scoped to the one room, readState empty). */
|
|
349
|
+
attention?: Record<string, RoomTopicAttention> | undefined;
|
|
322
350
|
requestId?: string | undefined;
|
|
323
351
|
}
|
|
324
352
|
|
|
@@ -849,6 +877,16 @@ export interface RoomTopicSetAttentionPayload {
|
|
|
849
877
|
attention: RoomTopicAttention;
|
|
850
878
|
}
|
|
851
879
|
|
|
880
|
+
/** Room-level attention (notification posture). Private per-user state on
|
|
881
|
+
* the caller's read row; topic attention wins for topic-scoped messages
|
|
882
|
+
* and room attention wins over space attention. The relay acks with a
|
|
883
|
+
* READ_STATE frame scoped to the room (see RoomReadStatePayload). */
|
|
884
|
+
export interface RoomSetAttentionPayload {
|
|
885
|
+
requestId?: string;
|
|
886
|
+
room: string;
|
|
887
|
+
attention: RoomTopicAttention;
|
|
888
|
+
}
|
|
889
|
+
|
|
852
890
|
export interface RoomTopicListPayload {
|
|
853
891
|
requestId?: string;
|
|
854
892
|
room: string;
|
|
@@ -968,7 +1006,7 @@ export type RoomClientMessage =
|
|
|
968
1006
|
payload: {
|
|
969
1007
|
room: string;
|
|
970
1008
|
text?: string;
|
|
971
|
-
attachments?:
|
|
1009
|
+
attachments?: RoomAttachmentInput[];
|
|
972
1010
|
inReplyTo?: number;
|
|
973
1011
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
974
1012
|
topicId?: string;
|
|
@@ -1116,7 +1154,11 @@ export type RoomClientMessage =
|
|
|
1116
1154
|
}
|
|
1117
1155
|
| {
|
|
1118
1156
|
type: typeof RoomClientMessageType.STATUS;
|
|
1119
|
-
payload: {
|
|
1157
|
+
payload: {
|
|
1158
|
+
status: RoomUserStatus;
|
|
1159
|
+
statusText?: string | null;
|
|
1160
|
+
platform?: RoomClientPlatform;
|
|
1161
|
+
};
|
|
1120
1162
|
timestamp?: number;
|
|
1121
1163
|
}
|
|
1122
1164
|
| {
|
|
@@ -1200,6 +1242,11 @@ export type RoomClientMessage =
|
|
|
1200
1242
|
type: typeof RoomClientMessageType.TOPIC_LIST;
|
|
1201
1243
|
payload: RoomTopicListPayload;
|
|
1202
1244
|
timestamp?: number;
|
|
1245
|
+
}
|
|
1246
|
+
| {
|
|
1247
|
+
type: typeof RoomClientMessageType.ROOM_SET_ATTENTION;
|
|
1248
|
+
payload: RoomSetAttentionPayload;
|
|
1249
|
+
timestamp?: number;
|
|
1203
1250
|
};
|
|
1204
1251
|
|
|
1205
1252
|
export type RoomServerMessage =
|
package/src/social-client.ts
CHANGED
|
@@ -62,7 +62,10 @@ export type {
|
|
|
62
62
|
RoomSuggestionSource,
|
|
63
63
|
} from "./social-types.js";
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
/** Largest per-file size any tier accepts (plus tier). The server enforces
|
|
66
|
+
* the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
|
|
67
|
+
* constant only pre-flights uploads no tier could ever accept. */
|
|
68
|
+
export const ROOM_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
|
|
66
69
|
|
|
67
70
|
export class RoomSocialError extends Error {
|
|
68
71
|
override readonly name = "RoomSocialError";
|
|
@@ -1097,12 +1100,24 @@ export async function getRoomAttachmentUrl(
|
|
|
1097
1100
|
config: RoomSocialConfig,
|
|
1098
1101
|
room: string,
|
|
1099
1102
|
key: string,
|
|
1103
|
+
opts: { name?: string; download?: boolean } = {},
|
|
1100
1104
|
): Promise<{ url: string; expiresAt: string | null }> {
|
|
1105
|
+
// The object key identifies the object and stays in the query. The
|
|
1106
|
+
// download filename and force-download flag ride REQUEST HEADERS, never
|
|
1107
|
+
// the URL: a private-room filename in the query string leaked into the
|
|
1108
|
+
// relay's access logs and error telemetry (Morgan, Sentry).
|
|
1101
1109
|
const params = new URLSearchParams({ key });
|
|
1110
|
+
const headers: Record<string, string> = {};
|
|
1111
|
+
// Percent-encode the name: Fetch/HTTP header values are byte strings, so a
|
|
1112
|
+
// raw Unicode filename (报告.py, an emoji) throws a TypeError before the
|
|
1113
|
+
// request is sent. The store decodes with decodeURIComponent.
|
|
1114
|
+
if (opts.name) headers["X-Om-Download-Name"] = encodeURIComponent(opts.name);
|
|
1115
|
+
// Forces a Content-Disposition attachment even for inline mimes.
|
|
1116
|
+
if (opts.download) headers["X-Om-Download"] = "1";
|
|
1102
1117
|
const result = await socialRequest<{ url?: string; expiresAt?: string | null }>(
|
|
1103
1118
|
config,
|
|
1104
1119
|
`/rooms/${encodeURIComponent(cleanRoomId(room))}/attachment-url?${params.toString()}`,
|
|
1105
|
-
{ method: "GET" },
|
|
1120
|
+
{ method: "GET", headers },
|
|
1106
1121
|
);
|
|
1107
1122
|
if (!result.url) {
|
|
1108
1123
|
throw new RoomSocialError("Attachment sign failed", 502, result);
|
|
@@ -1195,6 +1210,7 @@ export async function socialRequest<T>(
|
|
|
1195
1210
|
body?: unknown;
|
|
1196
1211
|
signal?: AbortSignal;
|
|
1197
1212
|
formData?: FormData;
|
|
1213
|
+
headers?: Record<string, string>;
|
|
1198
1214
|
} = {},
|
|
1199
1215
|
): Promise<T> {
|
|
1200
1216
|
const url = `${trimTrailingSlash(config.apiUrl)}${path}`;
|
|
@@ -1205,6 +1221,7 @@ export async function socialRequest<T>(
|
|
|
1205
1221
|
Authorization: `Bearer ${config.apiKey}`,
|
|
1206
1222
|
Accept: "application/json",
|
|
1207
1223
|
...(hasJsonBody ? { "Content-Type": "application/json" } : {}),
|
|
1224
|
+
...(init.headers ?? {}),
|
|
1208
1225
|
},
|
|
1209
1226
|
...(hasJsonBody
|
|
1210
1227
|
? { body: JSON.stringify(init.body) }
|
|
@@ -1284,12 +1301,72 @@ function trimTrailingSlash(value: string): string {
|
|
|
1284
1301
|
return value.replace(/\/+$/, "");
|
|
1285
1302
|
}
|
|
1286
1303
|
|
|
1304
|
+
/** Source/config/text extensions declared as text/plain so the server's
|
|
1305
|
+
* UTF-8 code-file lane accepts them. Markup extensions the server
|
|
1306
|
+
* blocklists (html, svg, xml, ...) are deliberately absent: they fall
|
|
1307
|
+
* through to application/octet-stream and the server rejects them. */
|
|
1308
|
+
const CODE_TEXT_EXTENSIONS = new Set([
|
|
1309
|
+
"js",
|
|
1310
|
+
"jsx",
|
|
1311
|
+
"ts",
|
|
1312
|
+
"tsx",
|
|
1313
|
+
"py",
|
|
1314
|
+
"rb",
|
|
1315
|
+
"go",
|
|
1316
|
+
"rs",
|
|
1317
|
+
"java",
|
|
1318
|
+
"c",
|
|
1319
|
+
"h",
|
|
1320
|
+
"cc",
|
|
1321
|
+
"cpp",
|
|
1322
|
+
"hpp",
|
|
1323
|
+
"cs",
|
|
1324
|
+
"php",
|
|
1325
|
+
"sh",
|
|
1326
|
+
"bash",
|
|
1327
|
+
"zsh",
|
|
1328
|
+
"fish",
|
|
1329
|
+
"ps1",
|
|
1330
|
+
"sql",
|
|
1331
|
+
"yaml",
|
|
1332
|
+
"yml",
|
|
1333
|
+
"toml",
|
|
1334
|
+
"ini",
|
|
1335
|
+
"cfg",
|
|
1336
|
+
"conf",
|
|
1337
|
+
"log",
|
|
1338
|
+
"diff",
|
|
1339
|
+
"patch",
|
|
1340
|
+
"tex",
|
|
1341
|
+
"r",
|
|
1342
|
+
"jl",
|
|
1343
|
+
"kt",
|
|
1344
|
+
"kts",
|
|
1345
|
+
"swift",
|
|
1346
|
+
"scala",
|
|
1347
|
+
"clj",
|
|
1348
|
+
"cljs",
|
|
1349
|
+
"ex",
|
|
1350
|
+
"exs",
|
|
1351
|
+
"erl",
|
|
1352
|
+
"lua",
|
|
1353
|
+
"pl",
|
|
1354
|
+
"pm",
|
|
1355
|
+
"dart",
|
|
1356
|
+
"gradle",
|
|
1357
|
+
"properties",
|
|
1358
|
+
"bat",
|
|
1359
|
+
"cmd",
|
|
1360
|
+
]);
|
|
1361
|
+
|
|
1287
1362
|
export function mimeFromFilename(filename: string): string {
|
|
1288
1363
|
const lower = filename.toLowerCase();
|
|
1289
1364
|
if (lower.endsWith(".png")) return "image/png";
|
|
1290
1365
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1291
1366
|
if (lower.endsWith(".webp")) return "image/webp";
|
|
1292
1367
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
1368
|
+
if (lower.endsWith(".mp4")) return "video/mp4";
|
|
1369
|
+
if (lower.endsWith(".webm")) return "video/webm";
|
|
1293
1370
|
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1294
1371
|
if (lower.endsWith(".txt")) return "text/plain";
|
|
1295
1372
|
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "text/markdown";
|
|
@@ -1304,5 +1381,7 @@ export function mimeFromFilename(filename: string): string {
|
|
|
1304
1381
|
if (lower.endsWith(".pptx")) {
|
|
1305
1382
|
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
1306
1383
|
}
|
|
1384
|
+
const dot = lower.lastIndexOf(".");
|
|
1385
|
+
if (dot !== -1 && CODE_TEXT_EXTENSIONS.has(lower.slice(dot + 1))) return "text/plain";
|
|
1307
1386
|
return "application/octet-stream";
|
|
1308
1387
|
}
|
package/src/social-types.ts
CHANGED
|
@@ -53,8 +53,13 @@ export interface RoomFollowResult {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export interface RoomAttachment {
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
58
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
59
|
+
* store withholds the signable key until the scan clears, so render the
|
|
60
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
61
|
+
*/
|
|
62
|
+
key?: string | undefined;
|
|
58
63
|
name: string;
|
|
59
64
|
size: number;
|
|
60
65
|
mime: string;
|
|
@@ -62,8 +67,16 @@ export interface RoomAttachment {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export interface RoomAttachmentUpload {
|
|
65
|
-
/**
|
|
66
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The stored attachment plus its first signed URL, for immediate render.
|
|
72
|
+
* The uploader always receives the key; only member views of PENDING
|
|
73
|
+
* entries withhold it.
|
|
74
|
+
*/
|
|
75
|
+
attachment: RoomAttachment & {
|
|
76
|
+
key: string;
|
|
77
|
+
url: string;
|
|
78
|
+
expiresAt: string | null;
|
|
79
|
+
};
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
export interface RoomPublicProfile {
|
|
@@ -131,6 +144,9 @@ export interface RoomDmMessage {
|
|
|
131
144
|
imageUrl?: string | null;
|
|
132
145
|
/** Aggregated reactions (per-emoji userId lists). Additive. */
|
|
133
146
|
reactions?: Array<{ emoji: string; users: string[] }>;
|
|
147
|
+
/** Client-minted idempotency tag echoed back on DM sends, so a sender's
|
|
148
|
+
* durable queue can settle its optimistic row. Additive (0.4.0). */
|
|
149
|
+
clientMsgId?: string;
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
export interface RoomDmHistory {
|
package/src/types.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
RoomAccessMode,
|
|
3
3
|
RoomAttachment,
|
|
4
|
+
RoomAttachmentInput,
|
|
4
5
|
RoomAuditEntry,
|
|
5
6
|
RoomAuditLogPayload,
|
|
6
7
|
RoomAuditLogRequestPayload,
|
|
7
8
|
RoomAuthSuccessPayload,
|
|
8
9
|
RoomBackfillPayload,
|
|
9
10
|
RoomClientMessage,
|
|
11
|
+
RoomClientPlatform,
|
|
10
12
|
RoomCreatedPayload,
|
|
11
13
|
RoomCreatePayload,
|
|
12
14
|
RoomDmHistoryPayload,
|
|
@@ -49,6 +51,7 @@ export type {
|
|
|
49
51
|
RoomSearchScope,
|
|
50
52
|
RoomServerMessage,
|
|
51
53
|
RoomServerMessageOf,
|
|
54
|
+
RoomSetAttentionPayload,
|
|
52
55
|
RoomSetReadOnlyPayload,
|
|
53
56
|
RoomSpace,
|
|
54
57
|
RoomSpaceJoinedPayload,
|