@noverachat/sdk-web 0.0.3 → 0.1.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/index.d.ts CHANGED
@@ -204,34 +204,49 @@ interface WsPing {
204
204
  client_ts: number;
205
205
  }
206
206
 
207
- /**
208
- * Shared wire types. Must stay in lock-step with:
209
- * noverachat-backend/src/noverachat/ws/protocol.py
210
- * noverachat-backend/src/noverachat/modules/message/schemas.py
211
- *
212
- * NOTE on `message_id` / `reply_to_id` / `last_*_message_id`: server-side
213
- * these are 63-bit Snowflake integers. On the wire they are JSON STRINGS
214
- * because JS `Number` can only represent integers up to 2^53 exactly.
215
- * Stringifying preserves precision; the SDK never converts them back to
216
- * Number. Compare with `BigInt(a) > BigInt(b)`.
217
- */
218
- /** 63-bit Snowflake serialized as a JSON string. Treat as opaque. */
219
- type MessageIdStr = string;
220
-
221
- interface SenderMini {
207
+ /** Client-facing feature flags for the caller's app. */
208
+ interface AppPolicy {
209
+ appId: string;
210
+ allowMemberBulkDelete: boolean;
211
+ readReceiptsEnabled: boolean;
212
+ typingIndicatorsEnabled: boolean;
213
+ allowMemberDeleteAnyMessage: boolean;
214
+ }
215
+ /** Aggregate unread state across every room the calling user is in. */
216
+ interface UnreadSummary {
217
+ total: number;
218
+ rooms: UnreadRoomItem[];
219
+ }
220
+ interface UnreadRoomItem {
221
+ roomId: string;
222
+ name: string | null;
223
+ roomType?: string | null;
224
+ unreadCount: number;
225
+ lastMessageId?: string | null;
226
+ lastReadMessageId?: string | null;
227
+ lastMessagePreview?: string | null;
228
+ memberCount?: number;
229
+ membersPreview?: MemberPreview[];
230
+ }
231
+ /** A single member shown in a channel-list avatar mosaic. */
232
+ interface MemberPreview {
222
233
  userId: string;
223
234
  nickname?: string | null;
224
235
  profileImageUrl?: string | null;
236
+ isOnline?: boolean | null;
225
237
  }
226
- interface ReactionEntry {
227
- key: string;
228
- userIds: string[];
229
- updatedAt: number;
238
+ /** Per-room unread state for the calling user. */
239
+ interface RoomUnread {
240
+ roomId: string;
241
+ unreadCount: number;
242
+ lastReadMessageId?: string | null;
243
+ lastMessageId?: string | null;
230
244
  }
231
- interface MessageOut {
245
+ /** Canonical NoveraChat message response. Bemily clients hit the compat adapter which translates this into Sendbird wire-format (channelUrl, data as JSON string, etc.). */
246
+ interface MessageOut$1 {
232
247
  appId: string;
233
248
  roomId: string;
234
- messageId: MessageIdStr;
249
+ messageId: string;
235
250
  sender?: SenderMini | null;
236
251
  senderId?: string | null;
237
252
  messageType: MessageType;
@@ -239,44 +254,22 @@ interface MessageOut {
239
254
  content?: string | null;
240
255
  data?: Record<string, unknown> | null;
241
256
  meta?: Record<string, unknown> | null;
242
- /** Custom metadata attached by the sender (extracted from `data._customMeta`).
243
- * Null when the sender didn't attach anything. */
244
- customMeta?: Record<string, unknown> | null;
245
- /** Snowflake bigint (string) of the attached file, when this is a file
246
- * message. Use ``chat.getFileUrl(fileId)`` to obtain a short-lived
247
- * signed download URL, or ``chat.getFileMeta(fileId)`` for metadata
248
- * without the download. Null for text-only messages. */
249
- fileId?: MessageIdStr | null;
250
- /** Inline file metadata embedded by the server on chat_receive — lets
251
- * the UI render a placeholder (filename, size, dimensions, thumbnail
252
- * status) without an extra round-trip. May be null even when `fileId`
253
- * is set (e.g. for messages loaded via REST history before the inline
254
- * metadata feature shipped); call ``getFileMeta()`` in that case. */
255
- file?: FileInline | null;
256
- /** Multi-file bundle — set when the message references multiple files
257
- * via ``data.file_ids`` (kind = "file_group"). Each entry has the
258
- * same shape as ``file``. Null for single-file / text-only messages.
259
- * UIs should check ``files?.length > 0`` first and only fall through
260
- * to the ``file`` singleton when the bundle field is absent. */
261
- files?: FileInline[] | null;
262
- replyToId?: MessageIdStr | null;
263
- reactions: ReactionEntry[];
257
+ fileId?: string | null;
258
+ file?: Record<string, unknown> | null;
259
+ files?: Record<string, unknown>[] | null;
260
+ replyToId?: string | null;
261
+ reactions?: ReactionEntry[];
264
262
  createdAt: string;
265
- createdAtMs: number;
266
263
  updatedAt?: string | null;
267
- updatedAtMs?: number | null;
268
264
  deletedAt?: string | null;
269
265
  deletedByUserId?: string | null;
270
- deleteScope?: DeleteScope | null;
271
- editedCount: number;
272
- }
273
- interface RoomUnread {
274
- roomId: string;
275
- unreadCount: number;
276
- lastReadMessageId: MessageIdStr | null;
277
- lastMessageId: MessageIdStr | null;
266
+ deleteScope?: string | null;
267
+ editedCount?: number;
268
+ createdAtMs: number;
269
+ updatedAtMs: number | null;
278
270
  }
279
- interface Announcement {
271
+ /** 공지사항 (KakaoTalk 스타일) — 방마다 다수. `deleted_at` 이 세팅되면 소프트 삭제된 것으로 리스트/current 조회에서 제외된다. */
272
+ interface AnnouncementOut {
280
273
  id: number;
281
274
  roomId: string;
282
275
  content: string;
@@ -284,98 +277,101 @@ interface Announcement {
284
277
  createdAt: string;
285
278
  updatedAt: string;
286
279
  }
287
- /** One-way user block KakaoTalk-style.
288
- *
289
- * A row in the caller's block list. The blocked user is NOT told they're
290
- * blocked; they keep sending messages that just don't reach the blocker.
291
- * See ``chat.blockUser`` / ``chat.unblockUser`` / ``chat.listBlocks``.
292
- */
293
- interface Block {
294
- blockedUserId: string;
295
- reason: string | null;
296
- /** ISO-8601 UTC. */
297
- createdAt: string;
298
- }
299
- /** 방 초대 링크 (카톡 오픈채팅 URL 스타일). Minted by an OPERATOR via
300
- * ``room.createInvite()``; consumed by any user via ``chat.acceptInvite()``. */
301
- interface InviteToken {
302
- /** URL-safe short id (~22 chars). Embed in the shareable URL as-is. */
280
+ /** 초대 링크 (카톡 오픈채팅 URL 스타일). */
281
+ interface InviteTokenOut {
303
282
  token: string;
304
283
  roomId: string;
305
284
  createdBy: string;
306
- /** ISO-8601 UTC. */
307
285
  createdAt: string;
308
- /** ISO-8601 UTC. `null` = never expires. */
309
286
  expiresAt: string | null;
310
- /** `null` = unlimited uses. */
311
287
  maxUses: number | null;
312
- /** Successful `accept` calls so far. */
313
288
  usedCount: number;
314
- /** ISO-8601 UTC. `null` = still active (assuming not expired / exhausted). */
315
289
  revokedAt: string | null;
316
290
  }
317
- /** Result of `chat.acceptInvite(token)`. */
318
- interface AcceptInviteResult {
291
+ /** `POST /invites/{token}/accept` 성공 응답. 가입한 방 정보. */
292
+ interface InviteAcceptResponse {
319
293
  roomId: string;
320
294
  roomName: string | null;
321
- /** True when the caller was already an active member — the call is a
322
- * no-op and does NOT bump the invite's `used_count`. Useful for
323
- * deep-link flows that may be triggered on re-open. */
324
295
  wasAlreadyMember: boolean;
325
296
  }
326
- /** One member shown in a channel-list avatar mosaic. */
327
- interface MemberPreview {
297
+ interface DeviceOut {
298
+ deviceId: string;
299
+ tokenType: string;
300
+ tokenPreview: string;
301
+ os: string | null;
302
+ appVersion: string | null;
303
+ pushEnabled: boolean;
304
+ lastActiveAt: string | null;
305
+ createdAt: string;
306
+ }
307
+ /** One row from ``user_blocks`` for the caller. We DON'T echo ``blocker_user_id`` — it's always the caller, and including it in every list entry would just add noise. ``model_config`` lets the router return ORM instances directly via ``response_model``. */
308
+ interface BlockOut {
309
+ blockedUserId: string;
310
+ reason: string | null;
311
+ createdAt: string;
312
+ }
313
+ interface RoomMemberOut {
328
314
  userId: string;
329
- nickname: string | null;
330
- profileImageUrl: string | null;
331
- /** Cluster-wide presence dot. `null` = not resolved (endpoint didn't
332
- * include it). Consumers rendering a green/gray dot should treat
333
- * `null` and `false` differently only if they care about the
334
- * "unknown" state. */
335
- isOnline: boolean | null;
315
+ nickname?: string | null;
316
+ role: MemberRole;
317
+ pushTrigger: PushTrigger;
318
+ lastReadMessageId?: string | null;
319
+ joinedAt: string;
320
+ isOnline?: boolean | null;
321
+ mutedUntil?: string | null;
336
322
  }
337
- interface UnreadRoomItem {
338
- roomId: string;
339
- name: string | null;
340
- /** "ONE" | "GROUP" | "SUPER_GROUP" — lets the UI pick a 1-up vs mosaic avatar. */
341
- roomType: string | null;
342
- unreadCount: number;
343
- lastMessageId: MessageIdStr | null;
344
- /** One-line preview shown under the room name in the channel list.
345
- * TEXT/SYSTEM → first 200 chars, EMOTICON → "[이모티콘]", images/videos/
346
- * files → "[사진]" / "[동영상]" / "[파일] name". Null when the room has
347
- * no messages yet (or was just cleared). */
348
- lastMessagePreview: string | null;
349
- lastReadMessageId: MessageIdStr | null;
350
- /** Total active (non-KICKED) members incl. the caller — the count badge. */
351
- memberCount: number;
352
- /** Up to 4 OTHER members (caller excluded) for the avatar mosaic. */
353
- members: MemberPreview[];
323
+ /** Minimal sender identity — no friend/block/profile-url fluff. */
324
+ interface SenderMini {
325
+ userId: string;
326
+ nickname?: string | null;
327
+ profileImageUrl?: string | null;
354
328
  }
355
- interface UnreadSummary {
356
- total: number;
357
- rooms: UnreadRoomItem[];
329
+ interface ReactionEntry {
330
+ key: string;
331
+ userIds: string[];
332
+ updatedAt: number;
358
333
  }
359
- interface AppPolicy {
360
- appId: string;
361
- /** When true, any non-KICKED room member can call
362
- * `room.deleteAllMessages()`. When false, only operators (or super-admin)
363
- * can. UIs should gate the destructive button on this. */
364
- allowMemberBulkDelete: boolean;
365
- /** When false, server does NOT broadcast `sync_tick` frames — hide
366
- * read-receipt badges client-side. Per-user unread counts via REST
367
- * still work (DB tracking is independent of the broadcast). */
368
- readReceiptsEnabled: boolean;
369
- /** When false, the server silently drops inbound `typing` frames and
370
- * no peer ever sees "alice is typing…". SDK callers should also
371
- * short-circuit `room.setTyping()` based on this flag so a disabled
372
- * app doesn't pay the WS frame cost per keystroke. */
373
- typingIndicatorsEnabled: boolean;
374
- /** When true, any non-KICKED room member can hard-delete another
375
- * member's message (scope=ALL). Default false — sender-or-operator
376
- * only. UIs should gate the "Delete for everyone" button on other
377
- * users' messages by this flag. */
378
- deleteAnyMessageEnabled: boolean;
334
+ type MemberRole = "OPERATOR" | "MEMBER" | "KICKED";
335
+ type PushTrigger = "ALL" | "MENTION_ONLY" | "OFF";
336
+
337
+ /**
338
+ * Shared wire types. Must stay in lock-step with:
339
+ * noverachat-backend/src/noverachat/ws/protocol.py
340
+ * noverachat-backend/src/noverachat/modules/message/schemas.py
341
+ *
342
+ * NOTE on `message_id` / `reply_to_id` / `last_*_message_id`: server-side
343
+ * these are 63-bit Snowflake integers. On the wire they are JSON STRINGS
344
+ * because JS `Number` can only represent integers up to 2^53 exactly.
345
+ * Stringifying preserves precision; the SDK never converts them back to
346
+ * Number. Compare with `BigInt(a) > BigInt(b)`.
347
+ */
348
+ /** 63-bit Snowflake serialized as a JSON string. Treat as opaque. */
349
+ type MessageIdStr = string;
350
+
351
+ /**
352
+ * Canonical message shape surfaced to SDK callers. Structurally the
353
+ * GENERATED REST `MessageOut` PLUS two client-side enhancements the server
354
+ * doesn't send verbatim:
355
+ * - `file` / `files` parsed into the camelCase `FileInline` shape (the
356
+ * generated model leaves them free-form since the OpenAPI schema does),
357
+ * - `customMeta` lifted out of `data._customMeta`.
358
+ * Built by `toMessageOut` in features/room.ts.
359
+ */
360
+ interface MessageOut extends Omit<MessageOut$1, "file" | "files" | "reactions"> {
361
+ /** Inline file metadata embedded by the server on chat_receive / REST
362
+ * history — lets the UI render a placeholder (filename, size, dims,
363
+ * thumbnail status) without an extra round-trip. May be null even when
364
+ * `fileId` is set (older history predating inline meta); call
365
+ * ``getFileMeta()`` in that case. */
366
+ file?: FileInline | null;
367
+ /** Multi-file bundle (kind = "file_group"). Null for single-file /
368
+ * text-only messages. Check ``files?.length`` first, then fall back to
369
+ * the ``file`` singleton. */
370
+ files?: FileInline[] | null;
371
+ reactions: ReactionEntry[];
372
+ /** Custom metadata attached by the sender (extracted from
373
+ * `data._customMeta`). Null when the sender didn't attach anything. */
374
+ customMeta?: Record<string, unknown> | null;
379
375
  }
380
376
  /** Push providers we can deliver to. Matches the backend `TokenType`. */
381
377
  type DeviceTokenType = "FCM" | "APNS" | "HUAWEI";
@@ -393,18 +389,6 @@ interface RegisterDeviceParams {
393
389
  os?: DeviceOS;
394
390
  appVersion?: string;
395
391
  }
396
- interface DeviceInfo {
397
- deviceId: string;
398
- tokenType: DeviceTokenType;
399
- /** Server returns only a short prefix of the token — it's a credential
400
- * and the caller already holds the full value. */
401
- tokenPreview: string;
402
- os: DeviceOS | null;
403
- appVersion: string | null;
404
- pushEnabled: boolean;
405
- lastActiveAt: string | null;
406
- createdAt: string;
407
- }
408
392
  interface SendParams {
409
393
  content?: string;
410
394
  messageType?: "TEXT" | "FILE" | "EMOTICON";
@@ -735,8 +719,31 @@ declare class Room {
735
719
  * Other members get a `MEMBER_LEFT` [`roomEvent`](../reference/noverachat.md#events).
736
720
  * Idempotent if already a non-member; throws 409 if the caller was KICKED
737
721
  * (ask an operator to restore first). For GROUP/SUPER_GROUP, re-entry needs
738
- * an operator add; for ONE, re-creating the 1:1 rejoins with history kept. */
739
- leave(): Promise<void>;
722
+ * an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
723
+ *
724
+ * Optional `deleteMessages` — atomically clean up chat history as part
725
+ * of leaving. Default is `"none"` (messages survive so the room's
726
+ * history keeps making sense to remaining members).
727
+ * - `"none"` — leave only.
728
+ * - `"mine"` — soft-delete every message YOU sent, then leave. Other
729
+ * members' messages are untouched. Broadcasts
730
+ * `messages_cleared_by_sender` so connected clients hide
731
+ * them locally.
732
+ * - `"all"` — wipe every message in the room, then leave. Subject to
733
+ * the app's `allow_member_bulk_delete` gate and per-room
734
+ * OPERATOR check — a plain member gets 403 back. */
735
+ leave(options?: {
736
+ deleteMessages?: "none" | "mine" | "all";
737
+ }): Promise<void>;
738
+ /** Delete every message YOU sent in this room. Standalone version of
739
+ * `leave({ deleteMessages: "mine" })` — useful when a user wants to
740
+ * scrub their history without actually leaving. Other members'
741
+ * messages are untouched; broadcasts `messages_cleared_by_sender`
742
+ * so connected clients hide the vanished ones locally.
743
+ * No OPERATOR gate — you own your messages. */
744
+ deleteMyMessages(): Promise<{
745
+ cleared: number;
746
+ }>;
740
747
  /**
741
748
  * Set the caller's per-room push preference — like long-pressing a
742
749
  * chat and tapping "notifications off".
@@ -811,20 +818,7 @@ declare class Room {
811
818
  * `room.deleteAllMessages()`. Server gates by membership for non-public
812
819
  * rooms, so the caller must already be a member to read this.
813
820
  */
814
- listMembers(): Promise<Array<{
815
- userId: string;
816
- nickname: string | null;
817
- role: "OPERATOR" | "MEMBER" | "KICKED";
818
- pushTrigger: "ALL" | "MENTION" | "OFF";
819
- lastReadMessageId: MessageIdStr | null;
820
- joinedAt: string;
821
- /** Cluster-wide presence dot. `null` when the server didn't resolve it. */
822
- isOnline: boolean | null;
823
- /** Per-room mute expiry. `null` = not muted. A far-future sentinel
824
- * (year 9999) means the operator picked "indefinite" — clients
825
- * should render that as "무기한" instead of a real timestamp. */
826
- mutedUntil: string | null;
827
- }>>;
821
+ listMembers(): Promise<RoomMemberOut[]>;
828
822
  /** OPERATOR-only: mute a member so they can't send messages in this
829
823
  * room. `durationMinutes=null` → indefinite (until an explicit unmute).
830
824
  * Emits a `MEMBER_MUTED` [`roomEvent`](../reference/noverachat.md#events)
@@ -853,10 +847,10 @@ declare class Room {
853
847
  * approval). Auto-inserts a SYSTEM chat message and broadcasts a
854
848
  * `ROOM_VISIBILITY_CHANGED` roomEvent so peers update in real time. */
855
849
  setPublic(isPublic: boolean): Promise<void>;
856
- listAnnouncements(): Promise<Announcement[]>;
857
- getCurrentAnnouncement(): Promise<Announcement | null>;
858
- postAnnouncement(content: string): Promise<Announcement>;
859
- updateAnnouncement(id: number, content: string): Promise<Announcement>;
850
+ listAnnouncements(): Promise<AnnouncementOut[]>;
851
+ getCurrentAnnouncement(): Promise<AnnouncementOut | null>;
852
+ postAnnouncement(content: string): Promise<AnnouncementOut>;
853
+ updateAnnouncement(id: number, content: string): Promise<AnnouncementOut>;
860
854
  deleteAnnouncement(id: number): Promise<void>;
861
855
  /**
862
856
  * OPERATOR-only: mint a new invite link for this room. Default:
@@ -874,10 +868,10 @@ declare class Room {
874
868
  /** Max successful `acceptInvite` calls before the link
875
869
  * auto-exhausts. Default null (unlimited). */
876
870
  maxUses?: number | null;
877
- }): Promise<InviteToken>;
871
+ }): Promise<InviteTokenOut>;
878
872
  /** OPERATOR-only: list all invite tokens for this room (including
879
873
  * revoked / expired / exhausted), newest first. */
880
- listInvites(): Promise<InviteToken[]>;
874
+ listInvites(): Promise<InviteTokenOut[]>;
881
875
  /** OPERATOR-only: revoke a token immediately. Idempotent — revoking
882
876
  * an already-revoked token returns 204 as well. */
883
877
  revokeInvite(token: string): Promise<void>;
@@ -1005,9 +999,9 @@ declare class NoveraChat {
1005
999
  * on `deviceId` — the server upserts, so re-registering is cheap and
1006
1000
  * safe. Returns the stored device record (token shown only as a prefix).
1007
1001
  */
1008
- registerDevice(params: RegisterDeviceParams): Promise<DeviceInfo>;
1002
+ registerDevice(params: RegisterDeviceParams): Promise<DeviceOut>;
1009
1003
  /** List the devices registered for the authenticated user. */
1010
- listDevices(): Promise<DeviceInfo[]>;
1004
+ listDevices(): Promise<DeviceOut[]>;
1011
1005
  /**
1012
1006
  * Turn push delivery on/off for one device without unregistering it —
1013
1007
  * the token stays stored, the push pipeline just skips it. Use for an
@@ -1046,12 +1040,11 @@ declare class NoveraChat {
1046
1040
  * a validation error server-side.
1047
1041
  * @param reason Optional local note ≤500 chars.
1048
1042
  */
1049
- blockUser(userId: string, reason?: string): Promise<Block>;
1043
+ blockUser(userId: string, reason?: string): Promise<BlockOut>;
1050
1044
  /** Unblock a user. Idempotent — resolves either way. */
1051
1045
  unblockUser(userId: string): Promise<void>;
1052
1046
  /** List every user the caller has blocked, newest first. */
1053
- listBlocks(): Promise<Block[]>;
1054
- private _toDeviceInfo;
1047
+ listBlocks(): Promise<BlockOut[]>;
1055
1048
  /**
1056
1049
  * Authorization-gated URL for downloading a file attachment.
1057
1050
  *
@@ -1108,7 +1101,7 @@ declare class NoveraChat {
1108
1101
  * invalid, expired, revoked, exhausted, or when the caller has been
1109
1102
  * kicked from the target room.
1110
1103
  */
1111
- acceptInvite(token: string): Promise<AcceptInviteResult>;
1104
+ acceptInvite(token: string): Promise<InviteAcceptResponse>;
1112
1105
  private dispatch;
1113
1106
  private highestKnownMessageId;
1114
1107
  private backfillAllRooms;
@@ -1148,4 +1141,4 @@ declare class NoveraChatError extends Error {
1148
1141
  static fromResponse(status: number, body: unknown): NoveraChatError;
1149
1142
  }
1150
1143
 
1151
- export { type AcceptInviteResult, type Announcement, type AppPolicy, type Block, type ClientOptions, type DeleteScope, ErrorCode, type ErrorCodeValue, type InviteToken, type MessageOut, type MessageType, NoveraChat, NoveraChatError, type ReactionEntry, Room, type RoomEvents, type RoomUnread, type SendParams, type SenderMini, type UnreadRoomItem, type UnreadSummary, type WsAnnouncementEvent, type WsInbound, type WsOutbound };
1144
+ export { type InviteAcceptResponse as AcceptInviteResult, type AnnouncementOut as Announcement, type AppPolicy, type BlockOut as Block, type ClientOptions, type DeleteScope, ErrorCode, type ErrorCodeValue, type InviteTokenOut as InviteToken, type MessageOut, type MessageType, NoveraChat, NoveraChatError, type ReactionEntry, Room, type RoomEvents, type RoomMemberOut, type RoomUnread, type SendParams, type SenderMini, type UnreadRoomItem, type UnreadSummary, type WsAnnouncementEvent, type WsInbound, type WsOutbound };