@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.cjs CHANGED
@@ -27,6 +27,200 @@ __export(index_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
29
 
30
+ // src/generated/rest.ts
31
+ function parseAppPolicy(j) {
32
+ return {
33
+ appId: j["app_id"],
34
+ allowMemberBulkDelete: j["allow_member_bulk_delete"],
35
+ readReceiptsEnabled: j["read_receipts_enabled"],
36
+ typingIndicatorsEnabled: j["typing_indicators_enabled"],
37
+ allowMemberDeleteAnyMessage: j["allow_member_delete_any_message"]
38
+ };
39
+ }
40
+ function parseUnreadSummary(j) {
41
+ return {
42
+ total: j["total"],
43
+ rooms: j["rooms"].map((e) => parseUnreadRoomItem(e))
44
+ };
45
+ }
46
+ function parseUnreadRoomItem(j) {
47
+ return {
48
+ roomId: j["room_id"],
49
+ name: j["name"],
50
+ roomType: j["room_type"],
51
+ unreadCount: j["unread_count"],
52
+ lastMessageId: j["last_message_id"],
53
+ lastReadMessageId: j["last_read_message_id"],
54
+ lastMessagePreview: j["last_message_preview"],
55
+ memberCount: j["member_count"],
56
+ membersPreview: j["members_preview"] == null ? void 0 : j["members_preview"].map((e) => parseMemberPreview(e))
57
+ };
58
+ }
59
+ function parseMemberPreview(j) {
60
+ return {
61
+ userId: j["user_id"],
62
+ nickname: j["nickname"],
63
+ profileImageUrl: j["profile_image_url"],
64
+ isOnline: j["is_online"]
65
+ };
66
+ }
67
+ function parseRoomUnread(j) {
68
+ return {
69
+ roomId: j["room_id"],
70
+ unreadCount: j["unread_count"],
71
+ lastReadMessageId: j["last_read_message_id"],
72
+ lastMessageId: j["last_message_id"]
73
+ };
74
+ }
75
+ function parseMessageOut(j) {
76
+ return {
77
+ appId: j["app_id"],
78
+ roomId: j["room_id"],
79
+ messageId: j["message_id"],
80
+ sender: j["sender"] == null ? null : parseSenderMini(j["sender"]),
81
+ senderId: j["sender_id"],
82
+ messageType: j["message_type"],
83
+ customType: j["custom_type"],
84
+ content: j["content"],
85
+ data: j["data"],
86
+ meta: j["meta"],
87
+ fileId: j["file_id"],
88
+ file: j["file"],
89
+ files: j["files"],
90
+ replyToId: j["reply_to_id"],
91
+ reactions: j["reactions"] == null ? void 0 : j["reactions"].map((e) => parseReactionEntry(e)),
92
+ createdAt: j["created_at"],
93
+ updatedAt: j["updated_at"],
94
+ deletedAt: j["deleted_at"],
95
+ deletedByUserId: j["deleted_by_user_id"],
96
+ deleteScope: j["delete_scope"],
97
+ editedCount: j["edited_count"],
98
+ createdAtMs: j["created_at_ms"],
99
+ updatedAtMs: j["updated_at_ms"]
100
+ };
101
+ }
102
+ function parseAnnouncementOut(j) {
103
+ return {
104
+ id: j["id"],
105
+ roomId: j["room_id"],
106
+ content: j["content"],
107
+ createdBy: j["created_by"],
108
+ createdAt: j["created_at"],
109
+ updatedAt: j["updated_at"]
110
+ };
111
+ }
112
+ function parseInviteTokenOut(j) {
113
+ return {
114
+ token: j["token"],
115
+ roomId: j["room_id"],
116
+ createdBy: j["created_by"],
117
+ createdAt: j["created_at"],
118
+ expiresAt: j["expires_at"],
119
+ maxUses: j["max_uses"],
120
+ usedCount: j["used_count"],
121
+ revokedAt: j["revoked_at"]
122
+ };
123
+ }
124
+ function parseInviteAcceptResponse(j) {
125
+ return {
126
+ roomId: j["room_id"],
127
+ roomName: j["room_name"],
128
+ wasAlreadyMember: j["was_already_member"]
129
+ };
130
+ }
131
+ function parseDeviceOut(j) {
132
+ return {
133
+ deviceId: j["device_id"],
134
+ tokenType: j["token_type"],
135
+ tokenPreview: j["token_preview"],
136
+ os: j["os"],
137
+ appVersion: j["app_version"],
138
+ pushEnabled: j["push_enabled"],
139
+ lastActiveAt: j["last_active_at"],
140
+ createdAt: j["created_at"]
141
+ };
142
+ }
143
+ function parseDeviceListResponse(j) {
144
+ return {
145
+ items: j["items"].map((e) => parseDeviceOut(e)),
146
+ total: j["total"]
147
+ };
148
+ }
149
+ function parsePushPreferencesOut(j) {
150
+ return {
151
+ pushEnabled: j["push_enabled"]
152
+ };
153
+ }
154
+ function parseFileOut(j) {
155
+ return {
156
+ id: j["id"],
157
+ kind: j["kind"],
158
+ mime: j["mime"],
159
+ sizeBytes: j["size_bytes"],
160
+ originalName: j["original_name"],
161
+ width: j["width"],
162
+ height: j["height"],
163
+ durationSec: j["duration_sec"],
164
+ thumbnailStatus: j["thumbnail_status"],
165
+ uploadStatus: j["upload_status"],
166
+ createdAt: j["created_at"],
167
+ committedAt: j["committed_at"],
168
+ forwardBlocked: j["forward_blocked"]
169
+ };
170
+ }
171
+ function parseFileCreateResponse(j) {
172
+ return {
173
+ fileId: j["file_id"],
174
+ uploadUrl: j["upload_url"],
175
+ headers: j["headers"],
176
+ expiresAtMs: j["expires_at_ms"]
177
+ };
178
+ }
179
+ function parseBlockOut(j) {
180
+ return {
181
+ blockedUserId: j["blocked_user_id"],
182
+ reason: j["reason"],
183
+ createdAt: j["created_at"]
184
+ };
185
+ }
186
+ function parseRoomMemberOut(j) {
187
+ return {
188
+ userId: j["user_id"],
189
+ nickname: j["nickname"],
190
+ role: j["role"],
191
+ pushTrigger: j["push_trigger"],
192
+ lastReadMessageId: j["last_read_message_id"],
193
+ joinedAt: j["joined_at"],
194
+ isOnline: j["is_online"],
195
+ mutedUntil: j["muted_until"]
196
+ };
197
+ }
198
+ function parseBulkDeleteResult(j) {
199
+ return {
200
+ cleared: j["cleared"],
201
+ upToMessageId: j["up_to_message_id"]
202
+ };
203
+ }
204
+ function parseBulkDeleteBySenderResult(j) {
205
+ return {
206
+ cleared: j["cleared"]
207
+ };
208
+ }
209
+ function parseSenderMini(j) {
210
+ return {
211
+ userId: j["user_id"],
212
+ nickname: j["nickname"],
213
+ profileImageUrl: j["profile_image_url"]
214
+ };
215
+ }
216
+ function parseReactionEntry(j) {
217
+ return {
218
+ key: j["key"],
219
+ userIds: j["user_ids"],
220
+ updatedAt: j["updated_at"]
221
+ };
222
+ }
223
+
30
224
  // src/errors.ts
31
225
  var ErrorCode = {
32
226
  INTERNAL: "NC-GEN-001",
@@ -322,64 +516,31 @@ function tempId() {
322
516
  }
323
517
 
324
518
  // src/features/room.ts
325
- function toMessageOut(r) {
326
- const dataObj = r.data ?? null;
519
+ function toFileInline(f) {
520
+ return {
521
+ id: f.id,
522
+ kind: f.kind,
523
+ mime: f.mime,
524
+ size: f.size,
525
+ name: f.name ?? null,
526
+ width: f.width ?? null,
527
+ height: f.height ?? null,
528
+ durationSec: f.duration_sec ?? null,
529
+ thumbnailStatus: f.thumbnail_status,
530
+ forwardBlocked: Boolean(f.forward_blocked)
531
+ };
532
+ }
533
+ function toMessageOut(j) {
534
+ const base = parseMessageOut(j);
535
+ const raw = j;
536
+ const dataObj = base.data ?? null;
327
537
  const customMeta = dataObj && typeof dataObj === "object" && "_customMeta" in dataObj ? dataObj._customMeta : null;
328
538
  return {
329
- appId: r.app_id,
330
- roomId: r.room_id,
331
- messageId: r.message_id,
332
- sender: r.sender ? {
333
- userId: r.sender.user_id,
334
- nickname: r.sender.nickname ?? null,
335
- profileImageUrl: r.sender.profile_image_url ?? null
336
- } : null,
337
- senderId: r.sender_id ?? null,
338
- messageType: r.message_type,
339
- customType: r.custom_type ?? null,
340
- content: r.content ?? null,
341
- data: dataObj,
342
- meta: r.meta ?? null,
343
- customMeta,
344
- fileId: r.file_id ?? null,
345
- file: r.file ? {
346
- id: r.file.id,
347
- kind: r.file.kind,
348
- mime: r.file.mime,
349
- size: r.file.size,
350
- name: r.file.name ?? null,
351
- width: r.file.width ?? null,
352
- height: r.file.height ?? null,
353
- durationSec: r.file.duration_sec ?? null,
354
- thumbnailStatus: r.file.thumbnail_status,
355
- forwardBlocked: Boolean(r.file.forward_blocked)
356
- } : null,
357
- files: r.files && r.files.length > 0 ? r.files.map((f) => ({
358
- id: f.id,
359
- kind: f.kind,
360
- mime: f.mime,
361
- size: f.size,
362
- name: f.name ?? null,
363
- width: f.width ?? null,
364
- height: f.height ?? null,
365
- durationSec: f.duration_sec ?? null,
366
- thumbnailStatus: f.thumbnail_status,
367
- forwardBlocked: Boolean(f.forward_blocked)
368
- })) : null,
369
- replyToId: r.reply_to_id ?? null,
370
- reactions: (r.reactions ?? []).map((x) => ({
371
- key: x.key,
372
- userIds: x.user_ids,
373
- updatedAt: x.updated_at
374
- })),
375
- createdAt: r.created_at,
376
- createdAtMs: r.created_at_ms,
377
- updatedAt: r.updated_at ?? null,
378
- updatedAtMs: r.updated_at_ms ?? null,
379
- deletedAt: r.deleted_at ?? null,
380
- deletedByUserId: r.deleted_by_user_id ?? null,
381
- deleteScope: r.delete_scope ?? null,
382
- editedCount: r.edited_count ?? 0
539
+ ...base,
540
+ file: raw.file ? toFileInline(raw.file) : null,
541
+ files: raw.files && raw.files.length > 0 ? raw.files.map(toFileInline) : null,
542
+ reactions: base.reactions ?? [],
543
+ customMeta
383
544
  };
384
545
  }
385
546
  var Room = class _Room {
@@ -565,20 +726,24 @@ var Room = class _Room {
565
726
  async sendFile(file, opts = {}) {
566
727
  const kind = opts.kind ?? (file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : "file");
567
728
  const name = opts.name ?? file.name;
568
- const presign = await this.http.request("POST", "/api/v1/files", {
569
- kind,
570
- mime: file.type || "application/octet-stream",
571
- size: file.size,
572
- original_name: name,
573
- // 파일 전달 제한 — uploader-set at presign time. When true,
574
- // other users can't forward this file to a different room.
575
- forward_blocked: Boolean(opts.forwardBlocked)
576
- });
729
+ const created = parseFileCreateResponse(
730
+ await this.http.request("POST", "/api/v1/files", {
731
+ kind,
732
+ mime: file.type || "application/octet-stream",
733
+ size: file.size,
734
+ original_name: name,
735
+ // 파일 전달 제한 uploader-set at presign time. When true,
736
+ // other users can't forward this file to a different room.
737
+ forward_blocked: Boolean(opts.forwardBlocked)
738
+ })
739
+ );
577
740
  const presigned = {
578
- fileId: presign.file_id,
579
- uploadUrl: presign.upload_url,
580
- headers: presign.headers,
581
- expiresAtMs: presign.expires_at_ms
741
+ fileId: created.fileId,
742
+ uploadUrl: created.uploadUrl,
743
+ // Presigned S3 headers are always string-valued; the generated
744
+ // model types the JSON object loosely as Record<string, unknown>.
745
+ headers: created.headers,
746
+ expiresAtMs: created.expiresAtMs
582
747
  };
583
748
  await new Promise((resolve, reject) => {
584
749
  const xhr = new XMLHttpRequest();
@@ -633,17 +798,19 @@ var Room = class _Room {
633
798
  async uploadFileOnly(file, opts = {}) {
634
799
  const kind = opts.kind ?? (file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : "file");
635
800
  const name = opts.name ?? file.name;
636
- const presign = await this.http.request("POST", "/api/v1/files", {
637
- kind,
638
- mime: file.type || "application/octet-stream",
639
- size: file.size,
640
- original_name: name,
641
- forward_blocked: Boolean(opts.forwardBlocked)
642
- });
801
+ const created = parseFileCreateResponse(
802
+ await this.http.request("POST", "/api/v1/files", {
803
+ kind,
804
+ mime: file.type || "application/octet-stream",
805
+ size: file.size,
806
+ original_name: name,
807
+ forward_blocked: Boolean(opts.forwardBlocked)
808
+ })
809
+ );
643
810
  await new Promise((resolve, reject) => {
644
811
  const xhr = new XMLHttpRequest();
645
- xhr.open("PUT", presign.upload_url);
646
- for (const [k, v] of Object.entries(presign.headers)) {
812
+ xhr.open("PUT", created.uploadUrl);
813
+ for (const [k, v] of Object.entries(created.headers)) {
647
814
  xhr.setRequestHeader(k, v);
648
815
  }
649
816
  if (opts.onProgress) {
@@ -670,10 +837,10 @@ var Room = class _Room {
670
837
  });
671
838
  await this.http.request(
672
839
  "POST",
673
- `/api/v1/files/${presign.file_id}/commit`,
840
+ `/api/v1/files/${created.fileId}/commit`,
674
841
  {}
675
842
  );
676
- return presign.file_id;
843
+ return created.fileId;
677
844
  }
678
845
  /**
679
846
  * List files shared in this room — newest-first, paginated.
@@ -698,23 +865,26 @@ var Room = class _Room {
698
865
  }
699
866
  );
700
867
  return {
701
- items: raw.items.map((r) => ({
702
- messageId: r.message_id,
703
- senderId: r.sender_id,
704
- createdAtMs: r.created_at_ms,
705
- file: {
706
- id: r.file.id,
707
- kind: r.file.kind,
708
- mime: r.file.mime,
709
- size: r.file.size_bytes,
710
- name: r.file.original_name ?? null,
711
- width: r.file.width ?? null,
712
- height: r.file.height ?? null,
713
- durationSec: r.file.duration_sec ?? null,
714
- thumbnailStatus: r.file.thumbnail_status,
715
- forwardBlocked: Boolean(r.file.forward_blocked)
716
- }
717
- })),
868
+ items: raw.items.map((r) => {
869
+ const f = parseFileOut(r.file);
870
+ return {
871
+ messageId: r.message_id,
872
+ senderId: r.sender_id,
873
+ createdAtMs: r.created_at_ms,
874
+ file: {
875
+ id: f.id,
876
+ kind: f.kind,
877
+ mime: f.mime,
878
+ size: f.sizeBytes,
879
+ name: f.originalName ?? null,
880
+ width: f.width ?? null,
881
+ height: f.height ?? null,
882
+ durationSec: f.durationSec ?? null,
883
+ thumbnailStatus: f.thumbnailStatus,
884
+ forwardBlocked: Boolean(f.forwardBlocked)
885
+ }
886
+ };
887
+ }),
718
888
  nextCursor: raw.next_cursor,
719
889
  hasMore: raw.has_more
720
890
  };
@@ -752,11 +922,12 @@ var Room = class _Room {
752
922
  this.ws.send({ type: "typing", room_id: this.roomId, is_typing: isTyping });
753
923
  }
754
924
  async edit(messageId, patch) {
755
- return this.http.request(
925
+ const raw = await this.http.request(
756
926
  "PUT",
757
927
  `/api/v1/rooms/${this.roomId}/messages/${messageId}`,
758
928
  patch
759
929
  );
930
+ return toMessageOut(raw);
760
931
  }
761
932
  async delete(messageId, scope = "ALL") {
762
933
  await this.http.request(
@@ -781,12 +952,39 @@ var Room = class _Room {
781
952
  * Other members get a `MEMBER_LEFT` [`roomEvent`](../reference/noverachat.md#events).
782
953
  * Idempotent if already a non-member; throws 409 if the caller was KICKED
783
954
  * (ask an operator to restore first). For GROUP/SUPER_GROUP, re-entry needs
784
- * an operator add; for ONE, re-creating the 1:1 rejoins with history kept. */
785
- async leave() {
955
+ * an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
956
+ *
957
+ * Optional `deleteMessages` — atomically clean up chat history as part
958
+ * of leaving. Default is `"none"` (messages survive so the room's
959
+ * history keeps making sense to remaining members).
960
+ * - `"none"` — leave only.
961
+ * - `"mine"` — soft-delete every message YOU sent, then leave. Other
962
+ * members' messages are untouched. Broadcasts
963
+ * `messages_cleared_by_sender` so connected clients hide
964
+ * them locally.
965
+ * - `"all"` — wipe every message in the room, then leave. Subject to
966
+ * the app's `allow_member_bulk_delete` gate and per-room
967
+ * OPERATOR check — a plain member gets 403 back. */
968
+ async leave(options) {
969
+ const mode = options?.deleteMessages ?? "none";
970
+ const query = mode === "none" ? "" : `?delete_messages=${encodeURIComponent(mode)}`;
786
971
  await this.http.request(
787
972
  "POST",
788
- `/api/v1/rooms/${this.roomId}/leave`
973
+ `/api/v1/rooms/${this.roomId}/leave${query}`
974
+ );
975
+ }
976
+ /** Delete every message YOU sent in this room. Standalone version of
977
+ * `leave({ deleteMessages: "mine" })` — useful when a user wants to
978
+ * scrub their history without actually leaving. Other members'
979
+ * messages are untouched; broadcasts `messages_cleared_by_sender`
980
+ * so connected clients hide the vanished ones locally.
981
+ * No OPERATOR gate — you own your messages. */
982
+ async deleteMyMessages() {
983
+ const raw = await this.http.request(
984
+ "DELETE",
985
+ `/api/v1/rooms/${this.roomId}/messages/mine`
789
986
  );
987
+ return parseBulkDeleteBySenderResult(raw);
790
988
  }
791
989
  /**
792
990
  * Set the caller's per-room push preference — like long-pressing a
@@ -812,11 +1010,11 @@ var Room = class _Room {
812
1010
  * hide rendered messages up to (and including) that id.
813
1011
  */
814
1012
  async deleteAllMessages() {
815
- const raw = await this.http.request("DELETE", `/api/v1/rooms/${this.roomId}/messages`);
816
- return {
817
- cleared: raw.cleared,
818
- upToMessageId: raw.up_to_message_id
819
- };
1013
+ const raw = await this.http.request(
1014
+ "DELETE",
1015
+ `/api/v1/rooms/${this.roomId}/messages`
1016
+ );
1017
+ return parseBulkDeleteResult(raw);
820
1018
  }
821
1019
  async react(messageId, key) {
822
1020
  await this.http.request(
@@ -903,17 +1101,11 @@ var Room = class _Room {
903
1101
  * rooms, so the caller must already be a member to read this.
904
1102
  */
905
1103
  async listMembers() {
906
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/members`);
907
- return raw.map((m) => ({
908
- userId: m.user_id,
909
- nickname: m.nickname ?? null,
910
- role: m.role,
911
- pushTrigger: m.push_trigger,
912
- lastReadMessageId: m.last_read_message_id,
913
- joinedAt: m.joined_at,
914
- isOnline: m.is_online ?? null,
915
- mutedUntil: m.muted_until ?? null
916
- }));
1104
+ const raw = await this.http.request(
1105
+ "GET",
1106
+ `/api/v1/rooms/${this.roomId}/members`
1107
+ );
1108
+ return raw.map((m) => parseRoomMemberOut(m));
917
1109
  }
918
1110
  // -------- mute --------
919
1111
  /** OPERATOR-only: mute a member so they can't send messages in this
@@ -941,13 +1133,11 @@ var Room = class _Room {
941
1133
  * newer than the caller's `read_watermark`, plus the room's
942
1134
  * `last_message_id` so the UI can show a "•" dot regardless of count. */
943
1135
  async unread() {
944
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/unread`);
945
- return {
946
- roomId: raw.room_id,
947
- unreadCount: raw.unread_count,
948
- lastReadMessageId: raw.last_read_message_id,
949
- lastMessageId: raw.last_message_id
950
- };
1136
+ const raw = await this.http.request(
1137
+ "GET",
1138
+ `/api/v1/rooms/${this.roomId}/unread`
1139
+ );
1140
+ return parseRoomUnread(raw);
951
1141
  }
952
1142
  // -------- freeze --------
953
1143
  /** OPERATOR-only moderation: freeze this room. Non-admin messages are
@@ -982,49 +1172,35 @@ var Room = class _Room {
982
1172
  }
983
1173
  // -------- announcements --------
984
1174
  async listAnnouncements() {
985
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/announcements`);
986
- return raw.map((a) => ({
987
- id: a.id,
988
- roomId: a.room_id,
989
- content: a.content,
990
- createdBy: a.created_by,
991
- createdAt: a.created_at,
992
- updatedAt: a.updated_at
993
- }));
1175
+ const raw = await this.http.request(
1176
+ "GET",
1177
+ `/api/v1/rooms/${this.roomId}/announcements`
1178
+ );
1179
+ return raw.map((a) => parseAnnouncementOut(a));
994
1180
  }
995
1181
  async getCurrentAnnouncement() {
996
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/announcements/current`);
1182
+ const raw = await this.http.request(
1183
+ "GET",
1184
+ `/api/v1/rooms/${this.roomId}/announcements/current`
1185
+ );
997
1186
  if (!raw) return null;
998
- return {
999
- id: raw.id,
1000
- roomId: raw.room_id,
1001
- content: raw.content,
1002
- createdBy: raw.created_by,
1003
- createdAt: raw.created_at,
1004
- updatedAt: raw.updated_at
1005
- };
1187
+ return parseAnnouncementOut(raw);
1006
1188
  }
1007
1189
  async postAnnouncement(content) {
1008
- const raw = await this.http.request("POST", `/api/v1/rooms/${this.roomId}/announcements`, { content });
1009
- return {
1010
- id: raw.id,
1011
- roomId: raw.room_id,
1012
- content: raw.content,
1013
- createdBy: raw.created_by,
1014
- createdAt: raw.created_at,
1015
- updatedAt: raw.updated_at
1016
- };
1190
+ const raw = await this.http.request(
1191
+ "POST",
1192
+ `/api/v1/rooms/${this.roomId}/announcements`,
1193
+ { content }
1194
+ );
1195
+ return parseAnnouncementOut(raw);
1017
1196
  }
1018
1197
  async updateAnnouncement(id, content) {
1019
- const raw = await this.http.request("PATCH", `/api/v1/rooms/${this.roomId}/announcements/${id}`, { content });
1020
- return {
1021
- id: raw.id,
1022
- roomId: raw.room_id,
1023
- content: raw.content,
1024
- createdBy: raw.created_by,
1025
- createdAt: raw.created_at,
1026
- updatedAt: raw.updated_at
1027
- };
1198
+ const raw = await this.http.request(
1199
+ "PATCH",
1200
+ `/api/v1/rooms/${this.roomId}/announcements/${id}`,
1201
+ { content }
1202
+ );
1203
+ return parseAnnouncementOut(raw);
1028
1204
  }
1029
1205
  async deleteAnnouncement(id) {
1030
1206
  await this.http.request(
@@ -1046,32 +1222,21 @@ var Room = class _Room {
1046
1222
  const body = {};
1047
1223
  if (opts && "expiresInHours" in opts) body.expires_in_hours = opts.expiresInHours;
1048
1224
  if (opts && "maxUses" in opts) body.max_uses = opts.maxUses;
1049
- const raw = await this.http.request("POST", `/api/v1/rooms/${this.roomId}/invites`, body);
1050
- return {
1051
- token: raw.token,
1052
- roomId: raw.room_id,
1053
- createdBy: raw.created_by,
1054
- createdAt: raw.created_at,
1055
- expiresAt: raw.expires_at,
1056
- maxUses: raw.max_uses,
1057
- usedCount: raw.used_count,
1058
- revokedAt: raw.revoked_at
1059
- };
1225
+ const raw = await this.http.request(
1226
+ "POST",
1227
+ `/api/v1/rooms/${this.roomId}/invites`,
1228
+ body
1229
+ );
1230
+ return parseInviteTokenOut(raw);
1060
1231
  }
1061
1232
  /** OPERATOR-only: list all invite tokens for this room (including
1062
1233
  * revoked / expired / exhausted), newest first. */
1063
1234
  async listInvites() {
1064
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/invites`);
1065
- return raw.map((r) => ({
1066
- token: r.token,
1067
- roomId: r.room_id,
1068
- createdBy: r.created_by,
1069
- createdAt: r.created_at,
1070
- expiresAt: r.expires_at,
1071
- maxUses: r.max_uses,
1072
- usedCount: r.used_count,
1073
- revokedAt: r.revoked_at
1074
- }));
1235
+ const raw = await this.http.request(
1236
+ "GET",
1237
+ `/api/v1/rooms/${this.roomId}/invites`
1238
+ );
1239
+ return raw.map((r) => parseInviteTokenOut(r));
1075
1240
  }
1076
1241
  /** OPERATOR-only: revoke a token immediately. Idempotent — revoking
1077
1242
  * an already-revoked token returns 204 as well. */
@@ -1329,13 +1494,7 @@ var NoveraChat = class {
1329
1494
  * destructive buttons to expose. */
1330
1495
  async appPolicy() {
1331
1496
  const raw = await this.http.request("GET", "/api/v1/app/policy");
1332
- return {
1333
- appId: raw.app_id,
1334
- allowMemberBulkDelete: raw.allow_member_bulk_delete,
1335
- readReceiptsEnabled: raw.read_receipts_enabled ?? true,
1336
- typingIndicatorsEnabled: raw.typing_indicators_enabled ?? true,
1337
- deleteAnyMessageEnabled: raw.allow_member_delete_any_message ?? false
1338
- };
1497
+ return parseAppPolicy(raw);
1339
1498
  }
1340
1499
  // ---- device tokens (push) ----
1341
1500
  /**
@@ -1345,19 +1504,23 @@ var NoveraChat = class {
1345
1504
  * safe. Returns the stored device record (token shown only as a prefix).
1346
1505
  */
1347
1506
  async registerDevice(params) {
1348
- const raw = await this.http.request("POST", "/api/v1/users/me/devices", {
1349
- device_id: params.deviceId,
1350
- token: params.token,
1351
- token_type: params.tokenType,
1352
- os: params.os ?? null,
1353
- app_version: params.appVersion ?? null
1354
- });
1355
- return this._toDeviceInfo(raw);
1507
+ const raw = await this.http.request(
1508
+ "POST",
1509
+ "/api/v1/users/me/devices",
1510
+ {
1511
+ device_id: params.deviceId,
1512
+ token: params.token,
1513
+ token_type: params.tokenType,
1514
+ os: params.os ?? null,
1515
+ app_version: params.appVersion ?? null
1516
+ }
1517
+ );
1518
+ return parseDeviceOut(raw);
1356
1519
  }
1357
1520
  /** List the devices registered for the authenticated user. */
1358
1521
  async listDevices() {
1359
1522
  const raw = await this.http.request("GET", "/api/v1/users/me/devices");
1360
- return raw.items.map((d) => this._toDeviceInfo(d));
1523
+ return parseDeviceListResponse(raw).items;
1361
1524
  }
1362
1525
  /**
1363
1526
  * Turn push delivery on/off for one device without unregistering it —
@@ -1395,7 +1558,7 @@ var NoveraChat = class {
1395
1558
  "GET",
1396
1559
  "/api/v1/users/me/push-preferences"
1397
1560
  );
1398
- return { pushEnabled: Boolean(raw.push_enabled) };
1561
+ return parsePushPreferencesOut(raw);
1399
1562
  }
1400
1563
  /**
1401
1564
  * Flip the caller's global push kill switch. Pass `false` to mute
@@ -1408,7 +1571,7 @@ var NoveraChat = class {
1408
1571
  "/api/v1/users/me/push-preferences",
1409
1572
  { push_enabled: enabled }
1410
1573
  );
1411
- return { pushEnabled: Boolean(raw.push_enabled) };
1574
+ return parsePushPreferencesOut(raw);
1412
1575
  }
1413
1576
  // -------------------------------------------------------------------------
1414
1577
  // User blocks — KakaoTalk-style one-way block.
@@ -1432,11 +1595,7 @@ var NoveraChat = class {
1432
1595
  `/api/v1/users/me/blocks/${encodeURIComponent(userId)}`,
1433
1596
  { reason: reason ?? null }
1434
1597
  );
1435
- return {
1436
- blockedUserId: raw.blocked_user_id,
1437
- reason: raw.reason,
1438
- createdAt: raw.created_at
1439
- };
1598
+ return parseBlockOut(raw);
1440
1599
  }
1441
1600
  /** Unblock a user. Idempotent — resolves either way. */
1442
1601
  async unblockUser(userId) {
@@ -1448,23 +1607,7 @@ var NoveraChat = class {
1448
1607
  /** List every user the caller has blocked, newest first. */
1449
1608
  async listBlocks() {
1450
1609
  const raw = await this.http.request("GET", "/api/v1/users/me/blocks");
1451
- return raw.map((r) => ({
1452
- blockedUserId: r.blocked_user_id,
1453
- reason: r.reason,
1454
- createdAt: r.created_at
1455
- }));
1456
- }
1457
- _toDeviceInfo(raw) {
1458
- return {
1459
- deviceId: raw.device_id,
1460
- tokenType: raw.token_type,
1461
- tokenPreview: raw.token_preview,
1462
- os: raw.os,
1463
- appVersion: raw.app_version,
1464
- pushEnabled: raw.push_enabled,
1465
- lastActiveAt: raw.last_active_at,
1466
- createdAt: raw.created_at
1467
- };
1610
+ return raw.map((r) => parseBlockOut(r));
1468
1611
  }
1469
1612
  /**
1470
1613
  * Authorization-gated URL for downloading a file attachment.
@@ -1522,41 +1665,27 @@ var NoveraChat = class {
1522
1665
  * shipping date.
1523
1666
  */
1524
1667
  async getFileMeta(fileId) {
1525
- const raw = await this.http.request("GET", `/api/v1/files/${encodeURIComponent(fileId)}/meta`);
1668
+ const raw = await this.http.request(
1669
+ "GET",
1670
+ `/api/v1/files/${encodeURIComponent(fileId)}/meta`
1671
+ );
1672
+ const f = parseFileOut(raw);
1526
1673
  return {
1527
- id: raw.id,
1528
- kind: raw.kind,
1529
- mime: raw.mime,
1530
- size: raw.size_bytes,
1531
- name: raw.original_name ?? null,
1532
- width: raw.width ?? null,
1533
- height: raw.height ?? null,
1534
- durationSec: raw.duration_sec ?? null,
1535
- thumbnailStatus: raw.thumbnail_status,
1536
- forwardBlocked: Boolean(raw.forward_blocked)
1674
+ id: f.id,
1675
+ kind: f.kind,
1676
+ mime: f.mime,
1677
+ size: f.sizeBytes,
1678
+ name: f.originalName ?? null,
1679
+ width: f.width ?? null,
1680
+ height: f.height ?? null,
1681
+ durationSec: f.durationSec ?? null,
1682
+ thumbnailStatus: f.thumbnailStatus,
1683
+ forwardBlocked: Boolean(f.forwardBlocked)
1537
1684
  };
1538
1685
  }
1539
1686
  async unreadSummary() {
1540
1687
  const raw = await this.http.request("GET", "/api/v1/users/me/unread");
1541
- return {
1542
- total: raw.total,
1543
- rooms: raw.rooms.map((r) => ({
1544
- roomId: r.room_id,
1545
- name: r.name,
1546
- roomType: r.room_type ?? null,
1547
- unreadCount: r.unread_count,
1548
- lastMessageId: r.last_message_id,
1549
- lastMessagePreview: r.last_message_preview ?? null,
1550
- lastReadMessageId: r.last_read_message_id,
1551
- memberCount: r.member_count ?? 0,
1552
- members: (r.members_preview ?? []).map((m) => ({
1553
- userId: m.user_id,
1554
- nickname: m.nickname ?? null,
1555
- profileImageUrl: m.profile_image_url ?? null,
1556
- isOnline: m.is_online ?? null
1557
- }))
1558
- }))
1559
- };
1688
+ return parseUnreadSummary(raw);
1560
1689
  }
1561
1690
  /**
1562
1691
  * Accept an invite link — call this in flows where you receive a
@@ -1574,12 +1703,11 @@ var NoveraChat = class {
1574
1703
  * kicked from the target room.
1575
1704
  */
1576
1705
  async acceptInvite(token) {
1577
- const raw = await this.http.request("POST", `/api/v1/invites/${encodeURIComponent(token)}/accept`);
1578
- return {
1579
- roomId: raw.room_id,
1580
- roomName: raw.room_name,
1581
- wasAlreadyMember: raw.was_already_member
1582
- };
1706
+ const raw = await this.http.request(
1707
+ "POST",
1708
+ `/api/v1/invites/${encodeURIComponent(token)}/accept`
1709
+ );
1710
+ return parseInviteAcceptResponse(raw);
1583
1711
  }
1584
1712
  // ---- internal ----
1585
1713
  dispatch(msg) {