@openmarket/rooms-client 0.2.1 → 0.3.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 +3 -3
- package/dist/shared/rooms-protocol.d.ts +17 -3
- package/dist/social-client.d.ts +8 -1
- package/dist/social-client.js +84 -3
- package/dist/social-types.d.ts +13 -3
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/client.ts +6 -4
- package/src/shared/rooms-protocol.ts +16 -3
- package/src/social-client.ts +81 -2
- package/src/social-types.ts +17 -4
- package/src/types.ts +1 -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 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,7 +283,7 @@ 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
|
-
export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?:
|
|
286
|
+
export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string): string;
|
|
287
287
|
export declare function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void;
|
|
288
288
|
export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null): void;
|
|
289
289
|
/**
|
|
@@ -217,13 +217,27 @@ export interface RoomLedgerEntry {
|
|
|
217
217
|
roomRole?: RoomRole | null | undefined;
|
|
218
218
|
}
|
|
219
219
|
export interface RoomAttachment {
|
|
220
|
-
/**
|
|
221
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
222
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
223
|
+
* store withholds the signable key until the scan clears, so render the
|
|
224
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
225
|
+
*/
|
|
226
|
+
key?: string | undefined;
|
|
222
227
|
name: string;
|
|
223
228
|
size: number;
|
|
224
229
|
mime: string;
|
|
225
230
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
226
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* A postable attachment reference. Outbound frames must always carry the
|
|
234
|
+
* stored key (senders reference their own uploads, which include it); only
|
|
235
|
+
* read views of pending entries may be keyless, and those must never be
|
|
236
|
+
* forwarded back into a post.
|
|
237
|
+
*/
|
|
238
|
+
export type RoomAttachmentInput = RoomAttachment & {
|
|
239
|
+
key: string;
|
|
240
|
+
};
|
|
227
241
|
/**
|
|
228
242
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
229
243
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -843,7 +857,7 @@ export type RoomClientMessage = {
|
|
|
843
857
|
payload: {
|
|
844
858
|
room: string;
|
|
845
859
|
text?: string;
|
|
846
|
-
attachments?:
|
|
860
|
+
attachments?: RoomAttachmentInput[];
|
|
847
861
|
inReplyTo?: number;
|
|
848
862
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
849
863
|
topicId?: string;
|
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
|
};
|
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, 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";
|
|
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.3.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,7 +7,7 @@ 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,
|
|
@@ -82,7 +82,7 @@ export interface RoomJoinOptions {
|
|
|
82
82
|
|
|
83
83
|
export interface RoomSayOptions extends RoomJoinOptions {
|
|
84
84
|
text?: string;
|
|
85
|
-
attachments?:
|
|
85
|
+
attachments?: RoomAttachmentInput[];
|
|
86
86
|
inReplyTo?: number;
|
|
87
87
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
88
88
|
topicId?: string;
|
|
@@ -261,7 +261,9 @@ export interface RoomJoinedSession {
|
|
|
261
261
|
client: RoomWsClient;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
function toRoomAttachmentPayloads(
|
|
264
|
+
function toRoomAttachmentPayloads(
|
|
265
|
+
attachments: readonly RoomAttachmentInput[],
|
|
266
|
+
): RoomAttachmentInput[] {
|
|
265
267
|
return attachments.map(({ key, name, size, mime }) => ({ key, name, size, mime }));
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -1150,7 +1152,7 @@ export function postRoomMessage(
|
|
|
1150
1152
|
room: string,
|
|
1151
1153
|
text: string,
|
|
1152
1154
|
inReplyTo?: number,
|
|
1153
|
-
attachments?:
|
|
1155
|
+
attachments?: RoomAttachmentInput[],
|
|
1154
1156
|
topicId?: string,
|
|
1155
1157
|
): string {
|
|
1156
1158
|
const clientMsgId = randomUUID();
|
|
@@ -246,14 +246,27 @@ export interface RoomLedgerEntry {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
export interface RoomAttachment {
|
|
249
|
-
/**
|
|
250
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
251
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
252
|
+
* store withholds the signable key until the scan clears, so render the
|
|
253
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
254
|
+
*/
|
|
255
|
+
key?: string | undefined;
|
|
251
256
|
name: string;
|
|
252
257
|
size: number;
|
|
253
258
|
mime: string;
|
|
254
259
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
255
260
|
}
|
|
256
261
|
|
|
262
|
+
/**
|
|
263
|
+
* A postable attachment reference. Outbound frames must always carry the
|
|
264
|
+
* stored key (senders reference their own uploads, which include it); only
|
|
265
|
+
* read views of pending entries may be keyless, and those must never be
|
|
266
|
+
* forwarded back into a post.
|
|
267
|
+
*/
|
|
268
|
+
export type RoomAttachmentInput = RoomAttachment & { key: string };
|
|
269
|
+
|
|
257
270
|
/**
|
|
258
271
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
259
272
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -968,7 +981,7 @@ export type RoomClientMessage =
|
|
|
968
981
|
payload: {
|
|
969
982
|
room: string;
|
|
970
983
|
text?: string;
|
|
971
|
-
attachments?:
|
|
984
|
+
attachments?: RoomAttachmentInput[];
|
|
972
985
|
inReplyTo?: number;
|
|
973
986
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
974
987
|
topicId?: string;
|
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 {
|