@prismer/sdk 1.3.4 → 1.7.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.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,25 +17,41 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
33
  AccountClient: () => AccountClient,
34
+ AttachmentQueue: () => AttachmentQueue,
24
35
  BindingsClient: () => BindingsClient,
25
36
  ContactsClient: () => ContactsClient,
26
37
  ConversationsClient: () => ConversationsClient,
27
38
  CreditsClient: () => CreditsClient,
28
39
  DirectClient: () => DirectClient,
40
+ E2EEncryption: () => E2EEncryption,
29
41
  ENVIRONMENTS: () => ENVIRONMENTS,
42
+ FilesClient: () => FilesClient,
30
43
  GroupsClient: () => GroupsClient,
31
44
  IMClient: () => IMClient,
32
45
  IMRealtimeClient: () => IMRealtimeClient,
46
+ IndexedDBStorage: () => IndexedDBStorage,
47
+ MemoryStorage: () => MemoryStorage,
33
48
  MessagesClient: () => MessagesClient,
49
+ OfflineManager: () => OfflineManager,
34
50
  PrismerClient: () => PrismerClient,
35
51
  RealtimeSSEClient: () => RealtimeSSEClient,
36
52
  RealtimeWSClient: () => RealtimeWSClient,
53
+ SQLiteStorage: () => SQLiteStorage,
54
+ TabCoordinator: () => TabCoordinator,
37
55
  WorkspaceClient: () => WorkspaceClient,
38
56
  createClient: () => createClient,
39
57
  default: () => index_default
@@ -467,11 +485,1844 @@ var RealtimeSSEClient = class extends TypedEmitter {
467
485
  }
468
486
  };
469
487
 
488
+ // src/offline.ts
489
+ var OfflineEmitter = class {
490
+ constructor() {
491
+ this.listeners = /* @__PURE__ */ new Map();
492
+ }
493
+ on(event, cb) {
494
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
495
+ this.listeners.get(event).add(cb);
496
+ return this;
497
+ }
498
+ off(event, cb) {
499
+ this.listeners.get(event)?.delete(cb);
500
+ return this;
501
+ }
502
+ emit(event, payload) {
503
+ const set = this.listeners.get(event);
504
+ if (set) for (const cb of set) {
505
+ try {
506
+ cb(payload);
507
+ } catch {
508
+ }
509
+ }
510
+ }
511
+ removeAllListeners() {
512
+ this.listeners.clear();
513
+ }
514
+ };
515
+ function generateId() {
516
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
517
+ return crypto.randomUUID();
518
+ }
519
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
520
+ const r = Math.random() * 16 | 0;
521
+ return (c === "x" ? r : r & 3 | 8).toString(16);
522
+ });
523
+ }
524
+ var WRITE_PATTERNS = [
525
+ { method: "POST", pattern: /\/api\/im\/(messages|direct|groups)\//, opType: "message.send" },
526
+ { method: "PATCH", pattern: /\/api\/im\/messages\//, opType: "message.edit" },
527
+ { method: "DELETE", pattern: /\/api\/im\/messages\//, opType: "message.delete" },
528
+ { method: "POST", pattern: /\/api\/im\/conversations\/[^/]+\/read/, opType: "conversation.read" }
529
+ ];
530
+ function matchWriteOp(method, path) {
531
+ for (const { method: m, pattern, opType } of WRITE_PATTERNS) {
532
+ if (method === m && pattern.test(path)) return opType;
533
+ }
534
+ return null;
535
+ }
536
+ var OfflineManager = class extends OfflineEmitter {
537
+ constructor(storage, networkRequest, options = {}) {
538
+ super();
539
+ this.flushTimer = null;
540
+ this.flushing = false;
541
+ this._isOnline = true;
542
+ this._syncState = "idle";
543
+ this.sseSource = null;
544
+ this.sseReconnectTimer = null;
545
+ this.sseReconnectAttempts = 0;
546
+ /** Presence cache for realtime presence events */
547
+ this.presenceCache = /* @__PURE__ */ new Map();
548
+ this.storage = storage;
549
+ this.networkRequest = networkRequest;
550
+ this.options = {
551
+ syncOnConnect: options.syncOnConnect ?? true,
552
+ outboxRetryLimit: options.outboxRetryLimit ?? 5,
553
+ outboxFlushInterval: options.outboxFlushInterval ?? 1e3,
554
+ conflictStrategy: options.conflictStrategy ?? "server",
555
+ onConflict: options.onConflict,
556
+ syncMode: options.syncMode ?? "push",
557
+ quota: options.quota ? {
558
+ maxStorageBytes: options.quota.maxStorageBytes ?? 500 * 1024 * 1024,
559
+ warningThreshold: options.quota.warningThreshold ?? 0.9
560
+ } : void 0
561
+ };
562
+ }
563
+ get isOnline() {
564
+ return this._isOnline;
565
+ }
566
+ get syncState() {
567
+ return this._syncState;
568
+ }
569
+ async init() {
570
+ await this.storage.init();
571
+ this.startFlushTimer();
572
+ }
573
+ async destroy() {
574
+ this.stopFlushTimer();
575
+ this.stopContinuousSync();
576
+ this.removeAllListeners();
577
+ }
578
+ // ── Network state ─────────────────────────────────────────
579
+ setOnline(online) {
580
+ if (this._isOnline === online) return;
581
+ this._isOnline = online;
582
+ this.emit(online ? "network.online" : "network.offline", void 0);
583
+ if (online) {
584
+ this.flush();
585
+ if (this.options.syncOnConnect) {
586
+ if (this.options.syncMode === "push") {
587
+ this.startContinuousSync();
588
+ } else {
589
+ this.sync();
590
+ }
591
+ }
592
+ } else {
593
+ this.stopContinuousSync();
594
+ }
595
+ }
596
+ // ── Request dispatch ──────────────────────────────────────
597
+ /**
598
+ * Dispatch an IM request. Write ops go through outbox; reads check local cache.
599
+ */
600
+ async dispatch(method, path, body, query) {
601
+ const opType = matchWriteOp(method, path);
602
+ if (opType) {
603
+ return this.dispatchWrite(opType, method, path, body, query);
604
+ }
605
+ if (method === "GET") {
606
+ const cached = await this.readFromCache(path, query);
607
+ if (cached !== null) return cached;
608
+ }
609
+ try {
610
+ const result = await this.networkRequest(method, path, body, query);
611
+ if (method === "GET") this.cacheReadResult(path, query, result);
612
+ return result;
613
+ } catch {
614
+ if (!this._isOnline) {
615
+ return { ok: true, data: [] };
616
+ }
617
+ throw new Error("Network request failed");
618
+ }
619
+ }
620
+ // ── Outbox: write operations ──────────────────────────────
621
+ async dispatchWrite(opType, method, path, body, query) {
622
+ const clientId = generateId();
623
+ const idempotencyKey = `sdk-${clientId}`;
624
+ let enrichedBody = body;
625
+ if (body && typeof body === "object" && (opType === "message.send" || opType === "message.edit")) {
626
+ enrichedBody = { ...body };
627
+ enrichedBody.metadata = {
628
+ ...body.metadata,
629
+ _idempotencyKey: idempotencyKey
630
+ };
631
+ }
632
+ let localMessage;
633
+ if (opType === "message.send" && body && typeof body === "object") {
634
+ const b = body;
635
+ const convIdMatch = path.match(/\/(?:messages|direct|groups)\/([^/]+)/);
636
+ const conversationId = convIdMatch?.[1] ?? "";
637
+ localMessage = {
638
+ id: `local-${clientId}`,
639
+ clientId,
640
+ conversationId,
641
+ content: b.content ?? "",
642
+ type: b.type ?? "text",
643
+ senderId: "__self__",
644
+ parentId: b.parentId ?? null,
645
+ status: "pending",
646
+ metadata: b.metadata,
647
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
648
+ };
649
+ await this.storage.putMessages([localMessage]);
650
+ this.emit("message.local", localMessage);
651
+ }
652
+ const op = {
653
+ id: clientId,
654
+ type: opType,
655
+ method,
656
+ path,
657
+ body: enrichedBody,
658
+ query,
659
+ status: "pending",
660
+ createdAt: Date.now(),
661
+ retries: 0,
662
+ maxRetries: this.options.outboxRetryLimit,
663
+ idempotencyKey,
664
+ localData: localMessage
665
+ };
666
+ await this.storage.enqueue(op);
667
+ if (this._isOnline) this.flush();
668
+ const optimisticResult = {
669
+ ok: true,
670
+ data: localMessage ? { conversationId: localMessage.conversationId, message: localMessage } : void 0,
671
+ _pending: true,
672
+ _clientId: clientId
673
+ };
674
+ return optimisticResult;
675
+ }
676
+ // ── Outbox flush ──────────────────────────────────────────
677
+ startFlushTimer() {
678
+ this.stopFlushTimer();
679
+ this.flushTimer = setInterval(() => this.flush(), this.options.outboxFlushInterval);
680
+ }
681
+ stopFlushTimer() {
682
+ if (this.flushTimer) {
683
+ clearInterval(this.flushTimer);
684
+ this.flushTimer = null;
685
+ }
686
+ }
687
+ async flush() {
688
+ if (this.flushing || !this._isOnline) return;
689
+ this.flushing = true;
690
+ try {
691
+ const ops = await this.storage.dequeueReady(10);
692
+ for (const op of ops) {
693
+ this.emit("outbox.sending", { opId: op.id, type: op.type });
694
+ try {
695
+ const result = await this.networkRequest(
696
+ op.method,
697
+ op.path,
698
+ op.body,
699
+ op.query
700
+ );
701
+ if (result.ok) {
702
+ await this.storage.ack(op.id);
703
+ this.emit("outbox.confirmed", { opId: op.id, serverData: result.data });
704
+ if (op.type === "message.send" && op.localData) {
705
+ const local = op.localData;
706
+ const serverMsg = result.data?.message;
707
+ if (serverMsg) {
708
+ await this.storage.deleteMessage(local.id);
709
+ await this.storage.putMessages([{
710
+ id: serverMsg.id,
711
+ clientId: op.id,
712
+ conversationId: serverMsg.conversationId ?? local.conversationId,
713
+ content: serverMsg.content ?? local.content,
714
+ type: serverMsg.type ?? local.type,
715
+ senderId: serverMsg.senderId ?? local.senderId,
716
+ parentId: serverMsg.parentId,
717
+ status: "confirmed",
718
+ metadata: serverMsg.metadata ? typeof serverMsg.metadata === "string" ? JSON.parse(serverMsg.metadata) : serverMsg.metadata : void 0,
719
+ createdAt: serverMsg.createdAt ?? local.createdAt
720
+ }]);
721
+ this.emit("message.confirmed", { clientId: op.id, serverMessage: serverMsg });
722
+ }
723
+ }
724
+ } else {
725
+ const errCode = result.error?.code;
726
+ if (errCode && !errCode.includes("TIMEOUT") && !errCode.includes("NETWORK")) {
727
+ await this.storage.nack(op.id, result.error?.message ?? "Request failed", op.maxRetries);
728
+ this.emit("outbox.failed", { opId: op.id, error: result.error?.message ?? "Request failed", retriesLeft: 0 });
729
+ if (op.type === "message.send") {
730
+ this.emit("message.failed", { clientId: op.id, error: result.error?.message ?? "Request failed" });
731
+ }
732
+ } else {
733
+ await this.storage.nack(op.id, result.error?.message ?? "Transient error", op.retries + 1);
734
+ this.emit("outbox.failed", {
735
+ opId: op.id,
736
+ error: result.error?.message ?? "Transient error",
737
+ retriesLeft: op.maxRetries - op.retries - 1
738
+ });
739
+ }
740
+ }
741
+ } catch (err) {
742
+ const msg = err instanceof Error ? err.message : "Unknown error";
743
+ await this.storage.nack(op.id, msg, op.retries + 1);
744
+ if (op.retries + 1 >= op.maxRetries) {
745
+ this.emit("outbox.failed", { opId: op.id, error: msg, retriesLeft: 0 });
746
+ if (op.type === "message.send") {
747
+ this.emit("message.failed", { clientId: op.id, error: msg });
748
+ }
749
+ }
750
+ }
751
+ }
752
+ } finally {
753
+ this.flushing = false;
754
+ }
755
+ }
756
+ get outboxSize() {
757
+ return this.storage.getPendingCount();
758
+ }
759
+ // ── Sync engine ───────────────────────────────────────────
760
+ async sync() {
761
+ if (this._syncState === "syncing" || !this._isOnline) return;
762
+ this._syncState = "syncing";
763
+ this.emit("sync.start", void 0);
764
+ let totalNew = 0;
765
+ let totalUpdated = 0;
766
+ try {
767
+ let cursor = await this.storage.getCursor("global_sync") ?? "0";
768
+ let hasMore = true;
769
+ while (hasMore) {
770
+ const result = await this.networkRequest(
771
+ "GET",
772
+ "/api/im/sync",
773
+ void 0,
774
+ { since: cursor, limit: "100" }
775
+ );
776
+ if (!result.ok || !result.data) {
777
+ throw new Error(result.error?.message ?? "Sync failed");
778
+ }
779
+ const { events, cursor: newCursor, hasMore: more } = result.data;
780
+ for (const event of events) {
781
+ await this.applySyncEvent(event);
782
+ if (event.type === "message.new") totalNew++;
783
+ if (event.type.startsWith("conversation.")) totalUpdated++;
784
+ }
785
+ cursor = String(newCursor);
786
+ await this.storage.setCursor("global_sync", cursor);
787
+ hasMore = more;
788
+ this.emit("sync.progress", { synced: events.length, total: events.length });
789
+ }
790
+ this._syncState = "idle";
791
+ this.emit("sync.complete", { newMessages: totalNew, updatedConversations: totalUpdated });
792
+ } catch (err) {
793
+ this._syncState = "error";
794
+ this.emit("sync.error", {
795
+ error: err instanceof Error ? err.message : "Sync failed",
796
+ willRetry: false
797
+ });
798
+ }
799
+ }
800
+ async applySyncEvent(event) {
801
+ switch (event.type) {
802
+ case "message.new": {
803
+ const msg = event.data;
804
+ await this.storage.putMessages([{
805
+ id: msg.id,
806
+ conversationId: msg.conversationId ?? event.conversationId ?? "",
807
+ content: msg.content ?? "",
808
+ type: msg.type ?? "text",
809
+ senderId: msg.senderId ?? "",
810
+ parentId: msg.parentId ?? null,
811
+ status: "confirmed",
812
+ metadata: msg.metadata,
813
+ createdAt: msg.createdAt ?? event.at,
814
+ syncSeq: event.seq
815
+ }]);
816
+ break;
817
+ }
818
+ case "message.edit": {
819
+ const existing = await this.storage.getMessage(event.data.id);
820
+ if (existing) {
821
+ const hasLocalEdits = existing.status !== "confirmed";
822
+ if (hasLocalEdits && this.options.onConflict) {
823
+ const resolution = this.options.onConflict(existing, event);
824
+ if (resolution === "keep_local") break;
825
+ if (resolution !== "accept_remote" && typeof resolution === "object") {
826
+ resolution.syncSeq = event.seq;
827
+ await this.storage.putMessages([resolution]);
828
+ break;
829
+ }
830
+ }
831
+ existing.content = event.data.content ?? existing.content;
832
+ existing.updatedAt = event.at;
833
+ existing.syncSeq = event.seq;
834
+ await this.storage.putMessages([existing]);
835
+ }
836
+ break;
837
+ }
838
+ case "message.delete": {
839
+ if (event.data?.id) await this.storage.deleteMessage(event.data.id);
840
+ break;
841
+ }
842
+ case "conversation.create":
843
+ case "conversation.update": {
844
+ const conv = event.data;
845
+ await this.storage.putConversations([{
846
+ id: conv.id ?? event.conversationId ?? "",
847
+ type: conv.type ?? "direct",
848
+ title: conv.title,
849
+ unreadCount: conv.unreadCount ?? 0,
850
+ members: conv.members,
851
+ metadata: conv.metadata,
852
+ syncSeq: event.seq,
853
+ updatedAt: event.at,
854
+ lastMessageAt: conv.lastMessageAt
855
+ }]);
856
+ break;
857
+ }
858
+ case "conversation.archive": {
859
+ const convId = event.data?.id ?? event.conversationId;
860
+ if (convId) {
861
+ const existing = await this.storage.getConversation(convId);
862
+ if (existing) {
863
+ existing.metadata = { ...existing.metadata, _archived: true };
864
+ existing.syncSeq = event.seq;
865
+ existing.updatedAt = event.at;
866
+ await this.storage.putConversations([existing]);
867
+ }
868
+ }
869
+ break;
870
+ }
871
+ case "participant.add": {
872
+ const convId = event.data?.conversationId ?? event.conversationId;
873
+ if (convId) {
874
+ const existing = await this.storage.getConversation(convId);
875
+ if (existing && existing.members) {
876
+ const already = existing.members.find((m) => m.userId === event.data.userId);
877
+ if (!already) {
878
+ existing.members.push({
879
+ userId: event.data.userId,
880
+ username: event.data.username ?? "",
881
+ displayName: event.data.displayName,
882
+ role: event.data.role ?? "member"
883
+ });
884
+ existing.syncSeq = event.seq;
885
+ existing.updatedAt = event.at;
886
+ await this.storage.putConversations([existing]);
887
+ }
888
+ }
889
+ }
890
+ break;
891
+ }
892
+ case "participant.remove": {
893
+ const convId = event.data?.conversationId ?? event.conversationId;
894
+ if (convId) {
895
+ const existing = await this.storage.getConversation(convId);
896
+ if (existing && existing.members) {
897
+ existing.members = existing.members.filter((m) => m.userId !== event.data.userId);
898
+ existing.syncSeq = event.seq;
899
+ existing.updatedAt = event.at;
900
+ await this.storage.putConversations([existing]);
901
+ }
902
+ }
903
+ break;
904
+ }
905
+ }
906
+ }
907
+ /**
908
+ * Handle a realtime event (from WS/SSE) and store locally.
909
+ */
910
+ async handleRealtimeEvent(type, payload) {
911
+ if (type === "message.new" && payload) {
912
+ await this.storage.putMessages([{
913
+ id: payload.id,
914
+ conversationId: payload.conversationId ?? "",
915
+ content: payload.content ?? "",
916
+ type: payload.type ?? "text",
917
+ senderId: payload.senderId ?? "",
918
+ parentId: payload.parentId ?? null,
919
+ status: "confirmed",
920
+ metadata: payload.metadata,
921
+ createdAt: payload.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
922
+ }]);
923
+ }
924
+ if (type === "presence.changed" && payload?.userId) {
925
+ this.presenceCache.set(payload.userId, {
926
+ status: payload.status ?? "offline",
927
+ lastSeen: payload.lastSeen ?? (/* @__PURE__ */ new Date()).toISOString()
928
+ });
929
+ this.emit("presence.changed", payload);
930
+ }
931
+ }
932
+ /**
933
+ * Get cached presence status for a user.
934
+ */
935
+ getPresence(userId) {
936
+ return this.presenceCache.get(userId) ?? null;
937
+ }
938
+ /**
939
+ * Search messages in local storage.
940
+ */
941
+ async searchMessages(query, opts) {
942
+ if (this.storage.searchMessages) {
943
+ return this.storage.searchMessages(query, opts);
944
+ }
945
+ return [];
946
+ }
947
+ /**
948
+ * Get storage size and quota info.
949
+ */
950
+ async getQuotaStatus() {
951
+ const limit = this.options.quota?.maxStorageBytes ?? 500 * 1024 * 1024;
952
+ const threshold = this.options.quota?.warningThreshold ?? 0.9;
953
+ if (this.storage.getStorageSize) {
954
+ const size = await this.storage.getStorageSize();
955
+ const percentage = size.total / limit;
956
+ return {
957
+ used: size.total,
958
+ limit,
959
+ percentage,
960
+ warning: percentage >= threshold,
961
+ exceeded: percentage >= 1
962
+ };
963
+ }
964
+ return { used: 0, limit, percentage: 0, warning: false, exceeded: false };
965
+ }
966
+ /**
967
+ * Clear old messages for a conversation (user-initiated quota management).
968
+ */
969
+ async clearOldMessages(conversationId, keepCount) {
970
+ if (this.storage.clearOldMessages) {
971
+ return this.storage.clearOldMessages(conversationId, keepCount);
972
+ }
973
+ return 0;
974
+ }
975
+ // ── Read cache ────────────────────────────────────────────
976
+ async readFromCache(path, query) {
977
+ if (/\/api\/im\/conversations$/.test(path)) {
978
+ const convos = await this.storage.getConversations({ limit: 50 });
979
+ if (convos.length > 0) return { ok: true, data: convos };
980
+ }
981
+ const msgMatch = path.match(/\/api\/im\/messages\/([^/]+)$/);
982
+ if (msgMatch) {
983
+ const convId = msgMatch[1];
984
+ const limit = query?.limit ? parseInt(query.limit) : 50;
985
+ const messages = await this.storage.getMessages(convId, { limit, before: query?.before });
986
+ if (messages.length > 0) return { ok: true, data: messages };
987
+ }
988
+ if (/\/api\/im\/contacts$/.test(path)) {
989
+ const contacts = await this.storage.getContacts();
990
+ if (contacts.length > 0) return { ok: true, data: contacts };
991
+ }
992
+ return null;
993
+ }
994
+ async cacheReadResult(path, _query, result) {
995
+ if (!result?.ok || !result?.data) return;
996
+ try {
997
+ if (/\/api\/im\/conversations$/.test(path) && Array.isArray(result.data)) {
998
+ const convos = result.data.map((c) => ({
999
+ id: c.id,
1000
+ type: c.type ?? "direct",
1001
+ title: c.title,
1002
+ lastMessage: c.lastMessage,
1003
+ lastMessageAt: c.lastMessageAt ?? c.updatedAt,
1004
+ unreadCount: c.unreadCount ?? 0,
1005
+ members: c.members,
1006
+ metadata: c.metadata,
1007
+ updatedAt: c.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1008
+ }));
1009
+ await this.storage.putConversations(convos);
1010
+ }
1011
+ const msgMatch = path.match(/\/api\/im\/messages\/([^/]+)$/);
1012
+ if (msgMatch && Array.isArray(result.data)) {
1013
+ const messages = result.data.map((m) => ({
1014
+ id: m.id,
1015
+ conversationId: m.conversationId ?? msgMatch[1],
1016
+ content: m.content ?? "",
1017
+ type: m.type ?? "text",
1018
+ senderId: m.senderId ?? "",
1019
+ parentId: m.parentId ?? null,
1020
+ status: "confirmed",
1021
+ metadata: m.metadata,
1022
+ createdAt: m.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
1023
+ }));
1024
+ await this.storage.putMessages(messages);
1025
+ }
1026
+ if (/\/api\/im\/contacts$/.test(path) && Array.isArray(result.data)) {
1027
+ await this.storage.putContacts(result.data);
1028
+ }
1029
+ } catch {
1030
+ }
1031
+ }
1032
+ // ── SSE continuous sync ────────────────────────────────────
1033
+ /**
1034
+ * Start continuous sync via SSE (Server-Sent Events).
1035
+ * Replaces polling with real-time push when syncMode is 'push'.
1036
+ */
1037
+ async startContinuousSync() {
1038
+ if (this.sseSource) return;
1039
+ if (typeof EventSource === "undefined") {
1040
+ return this.sync();
1041
+ }
1042
+ const token = this.tokenProvider?.();
1043
+ if (!token) {
1044
+ return this.sync();
1045
+ }
1046
+ const cursor = await this.storage.getCursor("global_sync") ?? "0";
1047
+ const baseUrl = this.getBaseUrl();
1048
+ const url = `${baseUrl}/api/im/sync/stream?token=${encodeURIComponent(token)}&since=${cursor}`;
1049
+ this._syncState = "syncing";
1050
+ this.emit("sync.start", void 0);
1051
+ this.sseReconnectAttempts = 0;
1052
+ try {
1053
+ this.sseSource = new EventSource(url);
1054
+ let totalNew = 0;
1055
+ let totalUpdated = 0;
1056
+ this.sseSource.addEventListener("sync", async (e) => {
1057
+ try {
1058
+ const event = JSON.parse(e.data);
1059
+ await this.applySyncEvent(event);
1060
+ await this.storage.setCursor("global_sync", String(event.seq));
1061
+ if (event.type === "message.new") totalNew++;
1062
+ if (event.type.startsWith("conversation.")) totalUpdated++;
1063
+ this.emit("sync.progress", { synced: 1, total: 1 });
1064
+ if (this.options.quota) {
1065
+ await this.checkQuota();
1066
+ }
1067
+ } catch {
1068
+ }
1069
+ });
1070
+ this.sseSource.addEventListener("caught_up", () => {
1071
+ this._syncState = "idle";
1072
+ this.sseReconnectAttempts = 0;
1073
+ this.emit("sync.complete", { newMessages: totalNew, updatedConversations: totalUpdated });
1074
+ totalNew = 0;
1075
+ totalUpdated = 0;
1076
+ });
1077
+ this.sseSource.addEventListener("error", () => {
1078
+ this._syncState = "error";
1079
+ this.emit("sync.error", { error: "SSE connection error", willRetry: true });
1080
+ });
1081
+ this.sseSource.onerror = () => {
1082
+ if (this.sseSource?.readyState === EventSource.CLOSED) {
1083
+ this.sseSource = null;
1084
+ this._syncState = "error";
1085
+ this.scheduleSseReconnect();
1086
+ }
1087
+ };
1088
+ } catch (err) {
1089
+ this._syncState = "error";
1090
+ this.emit("sync.error", {
1091
+ error: err instanceof Error ? err.message : "SSE init failed",
1092
+ willRetry: true
1093
+ });
1094
+ this.scheduleSseReconnect();
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Stop the SSE continuous sync connection.
1099
+ */
1100
+ stopContinuousSync() {
1101
+ if (this.sseSource) {
1102
+ this.sseSource.close();
1103
+ this.sseSource = null;
1104
+ }
1105
+ if (this.sseReconnectTimer) {
1106
+ clearTimeout(this.sseReconnectTimer);
1107
+ this.sseReconnectTimer = null;
1108
+ }
1109
+ this._syncState = "idle";
1110
+ }
1111
+ scheduleSseReconnect() {
1112
+ if (!this._isOnline) return;
1113
+ this.sseReconnectAttempts++;
1114
+ const delay = Math.min(1e3 * Math.pow(2, this.sseReconnectAttempts - 1), 3e4);
1115
+ this.sseReconnectTimer = setTimeout(() => {
1116
+ this.sseReconnectTimer = null;
1117
+ if (this._isOnline) this.startContinuousSync();
1118
+ }, delay);
1119
+ }
1120
+ /** Get the base URL for SSE connections (strip /api/im prefix). */
1121
+ getBaseUrl() {
1122
+ return typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
1123
+ }
1124
+ // ── Quota check ─────────────────────────────────────────────
1125
+ async checkQuota() {
1126
+ if (!this.options.quota || !this.storage.getStorageSize) return;
1127
+ const size = await this.storage.getStorageSize();
1128
+ const limit = this.options.quota.maxStorageBytes;
1129
+ const threshold = this.options.quota.warningThreshold;
1130
+ const pct = size.total / limit;
1131
+ if (pct >= 1) {
1132
+ this.emit("quota.exceeded", { used: size.total, limit });
1133
+ } else if (pct >= threshold) {
1134
+ this.emit("quota.warning", { used: size.total, limit, percentage: pct });
1135
+ }
1136
+ }
1137
+ };
1138
+ var AttachmentQueue = class {
1139
+ constructor(offline, networkRequest) {
1140
+ this.offline = offline;
1141
+ this.networkRequest = networkRequest;
1142
+ this.queue = /* @__PURE__ */ new Map();
1143
+ this.uploading = false;
1144
+ }
1145
+ /**
1146
+ * Queue a file attachment for offline upload.
1147
+ * Returns the queued attachment with a local ID.
1148
+ */
1149
+ async queueAttachment(conversationId, file, messageContent) {
1150
+ const id = generateId();
1151
+ const attachment = {
1152
+ id,
1153
+ conversationId,
1154
+ file: { name: file.name, size: file.size, type: file.type },
1155
+ data: file.data,
1156
+ status: "pending",
1157
+ progress: 0,
1158
+ messageClientId: generateId(),
1159
+ createdAt: Date.now()
1160
+ };
1161
+ this.queue.set(id, attachment);
1162
+ await this.offline.storage.putMessages([{
1163
+ id: `local-${attachment.messageClientId}`,
1164
+ clientId: attachment.messageClientId,
1165
+ conversationId,
1166
+ content: messageContent ?? `[File: ${file.name}]`,
1167
+ type: "file",
1168
+ senderId: "__self__",
1169
+ status: "pending",
1170
+ metadata: { _attachmentId: id, fileName: file.name, fileSize: file.size },
1171
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1172
+ }]);
1173
+ if (this.offline.isOnline) this.processQueue();
1174
+ return attachment;
1175
+ }
1176
+ /** Process pending uploads. */
1177
+ async processQueue() {
1178
+ if (this.uploading || !this.offline.isOnline) return;
1179
+ this.uploading = true;
1180
+ try {
1181
+ for (const [id, att] of this.queue) {
1182
+ if (att.status !== "pending") continue;
1183
+ att.status = "uploading";
1184
+ try {
1185
+ const presign = await this.networkRequest(
1186
+ "POST",
1187
+ "/api/im/files/presign",
1188
+ { fileName: att.file.name, fileSize: att.file.size, mimeType: att.file.type }
1189
+ );
1190
+ if (!presign.ok || !presign.data?.uploadUrl) {
1191
+ throw new Error(presign.error?.message ?? "Presign failed");
1192
+ }
1193
+ if (att.data) {
1194
+ await fetch(presign.data.uploadUrl, {
1195
+ method: "PUT",
1196
+ body: att.data,
1197
+ headers: { "Content-Type": att.file.type }
1198
+ });
1199
+ }
1200
+ att.progress = 80;
1201
+ const confirm = await this.networkRequest(
1202
+ "POST",
1203
+ "/api/im/files/confirm",
1204
+ { uploadId: presign.data.uploadId }
1205
+ );
1206
+ if (!confirm.ok) {
1207
+ throw new Error(confirm.error?.message ?? "Confirm failed");
1208
+ }
1209
+ att.status = "uploaded";
1210
+ att.progress = 100;
1211
+ await this.networkRequest(
1212
+ "POST",
1213
+ `/api/im/messages/${att.conversationId}`,
1214
+ {
1215
+ type: "file",
1216
+ content: `[File: ${att.file.name}]`,
1217
+ metadata: {
1218
+ fileUrl: confirm.data?.url ?? presign.data.downloadUrl,
1219
+ fileName: att.file.name,
1220
+ fileSize: att.file.size,
1221
+ mimeType: att.file.type,
1222
+ uploadId: presign.data.uploadId
1223
+ }
1224
+ }
1225
+ );
1226
+ await this.offline.storage.deleteMessage(`local-${att.messageClientId}`);
1227
+ this.queue.delete(id);
1228
+ } catch (err) {
1229
+ att.status = "failed";
1230
+ att.error = err instanceof Error ? err.message : "Upload failed";
1231
+ }
1232
+ }
1233
+ } finally {
1234
+ this.uploading = false;
1235
+ }
1236
+ }
1237
+ /** Get all queued attachments. */
1238
+ getQueue() {
1239
+ return Array.from(this.queue.values());
1240
+ }
1241
+ /** Retry a failed attachment upload. */
1242
+ async retry(attachmentId) {
1243
+ const att = this.queue.get(attachmentId);
1244
+ if (att && att.status === "failed") {
1245
+ att.status = "pending";
1246
+ att.error = void 0;
1247
+ if (this.offline.isOnline) this.processQueue();
1248
+ }
1249
+ }
1250
+ /** Cancel and remove a queued attachment. */
1251
+ async cancel(attachmentId) {
1252
+ const att = this.queue.get(attachmentId);
1253
+ if (att) {
1254
+ await this.offline.storage.deleteMessage(`local-${att.messageClientId}`);
1255
+ this.queue.delete(attachmentId);
1256
+ }
1257
+ }
1258
+ };
1259
+
470
1260
  // src/types.ts
471
1261
  var ENVIRONMENTS = {
472
1262
  production: "https://prismer.cloud"
473
1263
  };
474
1264
 
1265
+ // src/storage.ts
1266
+ var MemoryStorage = class {
1267
+ constructor() {
1268
+ this.messages = /* @__PURE__ */ new Map();
1269
+ this.conversations = /* @__PURE__ */ new Map();
1270
+ this.contacts = /* @__PURE__ */ new Map();
1271
+ this.cursors = /* @__PURE__ */ new Map();
1272
+ this.outbox = /* @__PURE__ */ new Map();
1273
+ }
1274
+ async init() {
1275
+ }
1276
+ // ── Messages ────────────────────────────────────────────────
1277
+ async putMessages(messages) {
1278
+ for (const m of messages) this.messages.set(m.id, { ...m });
1279
+ }
1280
+ async getMessages(conversationId, opts) {
1281
+ const all = Array.from(this.messages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1282
+ if (opts.before) {
1283
+ const idx = all.findIndex((m) => m.id === opts.before);
1284
+ if (idx > 0) return all.slice(Math.max(0, idx - opts.limit), idx);
1285
+ }
1286
+ return all.slice(-opts.limit);
1287
+ }
1288
+ async getMessage(messageId) {
1289
+ return this.messages.get(messageId) ?? null;
1290
+ }
1291
+ async deleteMessage(messageId) {
1292
+ this.messages.delete(messageId);
1293
+ }
1294
+ // ── Conversations ───────────────────────────────────────────
1295
+ async putConversations(conversations) {
1296
+ for (const c of conversations) this.conversations.set(c.id, { ...c });
1297
+ }
1298
+ async getConversations(opts) {
1299
+ const all = Array.from(this.conversations.values()).sort((a, b) => (b.lastMessageAt ?? b.updatedAt).localeCompare(a.lastMessageAt ?? a.updatedAt));
1300
+ const offset = opts?.offset ?? 0;
1301
+ const limit = opts?.limit ?? 50;
1302
+ return all.slice(offset, offset + limit);
1303
+ }
1304
+ async getConversation(id) {
1305
+ return this.conversations.get(id) ?? null;
1306
+ }
1307
+ // ── Contacts ────────────────────────────────────────────────
1308
+ async putContacts(contacts) {
1309
+ for (const c of contacts) this.contacts.set(c.userId, { ...c });
1310
+ }
1311
+ async getContacts() {
1312
+ return Array.from(this.contacts.values());
1313
+ }
1314
+ // ── Cursors ─────────────────────────────────────────────────
1315
+ async getCursor(key) {
1316
+ return this.cursors.get(key) ?? null;
1317
+ }
1318
+ async setCursor(key, value) {
1319
+ this.cursors.set(key, value);
1320
+ }
1321
+ // ── Outbox ──────────────────────────────────────────────────
1322
+ async enqueue(op) {
1323
+ this.outbox.set(op.id, { ...op });
1324
+ }
1325
+ async dequeueReady(limit) {
1326
+ const ready = Array.from(this.outbox.values()).filter((op) => op.status === "pending").sort((a, b) => a.createdAt - b.createdAt).slice(0, limit);
1327
+ for (const op of ready) {
1328
+ op.status = "inflight";
1329
+ this.outbox.set(op.id, op);
1330
+ }
1331
+ return ready;
1332
+ }
1333
+ async ack(opId) {
1334
+ this.outbox.delete(opId);
1335
+ }
1336
+ async nack(opId, error, retries) {
1337
+ const op = this.outbox.get(opId);
1338
+ if (!op) return;
1339
+ op.retries = retries;
1340
+ op.lastError = error;
1341
+ op.status = retries >= op.maxRetries ? "failed" : "pending";
1342
+ this.outbox.set(opId, op);
1343
+ }
1344
+ async getPendingCount() {
1345
+ return Array.from(this.outbox.values()).filter((op) => op.status === "pending" || op.status === "inflight").length;
1346
+ }
1347
+ // ── Search ─────────────────────────────────────────────────
1348
+ async searchMessages(query, opts) {
1349
+ const lower = query.toLowerCase();
1350
+ const limit = opts?.limit ?? 50;
1351
+ return Array.from(this.messages.values()).filter((m) => {
1352
+ if (opts?.conversationId && m.conversationId !== opts.conversationId) return false;
1353
+ return (m.content ?? "").toLowerCase().includes(lower);
1354
+ }).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit);
1355
+ }
1356
+ // ── Quota ─────────────────────────────────────────────────
1357
+ async getStorageSize() {
1358
+ const msgSize = this.messages.size * 500;
1359
+ const convSize = this.conversations.size * 200;
1360
+ return { messages: msgSize, conversations: convSize, total: msgSize + convSize };
1361
+ }
1362
+ async clearOldMessages(conversationId, keepCount) {
1363
+ const msgs = Array.from(this.messages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1364
+ const toDelete = msgs.slice(keepCount);
1365
+ for (const m of toDelete) this.messages.delete(m.id);
1366
+ return toDelete.length;
1367
+ }
1368
+ // ── Lifecycle ───────────────────────────────────────────────
1369
+ async clear() {
1370
+ this.messages.clear();
1371
+ this.conversations.clear();
1372
+ this.contacts.clear();
1373
+ this.cursors.clear();
1374
+ this.outbox.clear();
1375
+ }
1376
+ };
1377
+ var IDB_STORES = ["messages", "conversations", "contacts", "cursors", "outbox"];
1378
+ var IndexedDBStorage = class {
1379
+ constructor(dbName = "prismer-offline", version = 1) {
1380
+ this.dbName = dbName;
1381
+ this.version = version;
1382
+ this.db = null;
1383
+ }
1384
+ async init() {
1385
+ if (typeof indexedDB === "undefined") {
1386
+ throw new Error("IndexedDB is not available in this environment. Use MemoryStorage or SQLiteStorage instead.");
1387
+ }
1388
+ return new Promise((resolve, reject) => {
1389
+ const req = indexedDB.open(this.dbName, this.version);
1390
+ req.onupgradeneeded = () => {
1391
+ const db = req.result;
1392
+ if (!db.objectStoreNames.contains("messages")) {
1393
+ const store = db.createObjectStore("messages", { keyPath: "id" });
1394
+ store.createIndex("conversationId", "conversationId", { unique: false });
1395
+ store.createIndex("createdAt", "createdAt", { unique: false });
1396
+ }
1397
+ if (!db.objectStoreNames.contains("conversations")) {
1398
+ db.createObjectStore("conversations", { keyPath: "id" });
1399
+ }
1400
+ if (!db.objectStoreNames.contains("contacts")) {
1401
+ db.createObjectStore("contacts", { keyPath: "userId" });
1402
+ }
1403
+ if (!db.objectStoreNames.contains("cursors")) {
1404
+ db.createObjectStore("cursors", { keyPath: "key" });
1405
+ }
1406
+ if (!db.objectStoreNames.contains("outbox")) {
1407
+ const store = db.createObjectStore("outbox", { keyPath: "id" });
1408
+ store.createIndex("status", "status", { unique: false });
1409
+ store.createIndex("createdAt", "createdAt", { unique: false });
1410
+ }
1411
+ };
1412
+ req.onsuccess = () => {
1413
+ this.db = req.result;
1414
+ resolve();
1415
+ };
1416
+ req.onerror = () => reject(req.error);
1417
+ });
1418
+ }
1419
+ tx(stores, mode = "readonly") {
1420
+ if (!this.db) throw new Error("IndexedDB not initialized. Call init() first.");
1421
+ return this.db.transaction(stores, mode);
1422
+ }
1423
+ req(request) {
1424
+ return new Promise((resolve, reject) => {
1425
+ request.onsuccess = () => resolve(request.result);
1426
+ request.onerror = () => reject(request.error);
1427
+ });
1428
+ }
1429
+ // ── Messages ────────────────────────────────────────────────
1430
+ async putMessages(messages) {
1431
+ const tx = this.tx("messages", "readwrite");
1432
+ const store = tx.objectStore("messages");
1433
+ for (const m of messages) store.put(m);
1434
+ return new Promise((resolve, reject) => {
1435
+ tx.oncomplete = () => resolve();
1436
+ tx.onerror = () => reject(tx.error);
1437
+ });
1438
+ }
1439
+ async getMessages(conversationId, opts) {
1440
+ const tx = this.tx("messages");
1441
+ const store = tx.objectStore("messages");
1442
+ const idx = store.index("conversationId");
1443
+ const all = await this.req(idx.getAll(conversationId));
1444
+ all.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1445
+ if (opts.before) {
1446
+ const i = all.findIndex((m) => m.id === opts.before);
1447
+ if (i > 0) return all.slice(Math.max(0, i - opts.limit), i);
1448
+ }
1449
+ return all.slice(-opts.limit);
1450
+ }
1451
+ async getMessage(messageId) {
1452
+ const tx = this.tx("messages");
1453
+ const result = await this.req(tx.objectStore("messages").get(messageId));
1454
+ return result ?? null;
1455
+ }
1456
+ async deleteMessage(messageId) {
1457
+ const tx = this.tx("messages", "readwrite");
1458
+ tx.objectStore("messages").delete(messageId);
1459
+ return new Promise((resolve, reject) => {
1460
+ tx.oncomplete = () => resolve();
1461
+ tx.onerror = () => reject(tx.error);
1462
+ });
1463
+ }
1464
+ // ── Conversations ───────────────────────────────────────────
1465
+ async putConversations(conversations) {
1466
+ const tx = this.tx("conversations", "readwrite");
1467
+ const store = tx.objectStore("conversations");
1468
+ for (const c of conversations) store.put(c);
1469
+ return new Promise((resolve, reject) => {
1470
+ tx.oncomplete = () => resolve();
1471
+ tx.onerror = () => reject(tx.error);
1472
+ });
1473
+ }
1474
+ async getConversations(opts) {
1475
+ const tx = this.tx("conversations");
1476
+ const all = await this.req(tx.objectStore("conversations").getAll());
1477
+ all.sort((a, b) => (b.lastMessageAt ?? b.updatedAt).localeCompare(a.lastMessageAt ?? a.updatedAt));
1478
+ const offset = opts?.offset ?? 0;
1479
+ const limit = opts?.limit ?? 50;
1480
+ return all.slice(offset, offset + limit);
1481
+ }
1482
+ async getConversation(id) {
1483
+ const tx = this.tx("conversations");
1484
+ const result = await this.req(tx.objectStore("conversations").get(id));
1485
+ return result ?? null;
1486
+ }
1487
+ // ── Contacts ────────────────────────────────────────────────
1488
+ async putContacts(contacts) {
1489
+ const tx = this.tx("contacts", "readwrite");
1490
+ const store = tx.objectStore("contacts");
1491
+ for (const c of contacts) store.put(c);
1492
+ return new Promise((resolve, reject) => {
1493
+ tx.oncomplete = () => resolve();
1494
+ tx.onerror = () => reject(tx.error);
1495
+ });
1496
+ }
1497
+ async getContacts() {
1498
+ const tx = this.tx("contacts");
1499
+ return this.req(tx.objectStore("contacts").getAll());
1500
+ }
1501
+ // ── Cursors ─────────────────────────────────────────────────
1502
+ async getCursor(key) {
1503
+ const tx = this.tx("cursors");
1504
+ const result = await this.req(tx.objectStore("cursors").get(key));
1505
+ return result?.value ?? null;
1506
+ }
1507
+ async setCursor(key, value) {
1508
+ const tx = this.tx("cursors", "readwrite");
1509
+ tx.objectStore("cursors").put({ key, value });
1510
+ return new Promise((resolve, reject) => {
1511
+ tx.oncomplete = () => resolve();
1512
+ tx.onerror = () => reject(tx.error);
1513
+ });
1514
+ }
1515
+ // ── Outbox ──────────────────────────────────────────────────
1516
+ async enqueue(op) {
1517
+ const tx = this.tx("outbox", "readwrite");
1518
+ tx.objectStore("outbox").put(op);
1519
+ return new Promise((resolve, reject) => {
1520
+ tx.oncomplete = () => resolve();
1521
+ tx.onerror = () => reject(tx.error);
1522
+ });
1523
+ }
1524
+ async dequeueReady(limit) {
1525
+ const tx = this.tx("outbox", "readwrite");
1526
+ const store = tx.objectStore("outbox");
1527
+ const idx = store.index("status");
1528
+ const pending = await this.req(idx.getAll("pending"));
1529
+ pending.sort((a, b) => a.createdAt - b.createdAt);
1530
+ const batch = pending.slice(0, limit);
1531
+ for (const op of batch) {
1532
+ op.status = "inflight";
1533
+ store.put(op);
1534
+ }
1535
+ return new Promise((resolve, reject) => {
1536
+ tx.oncomplete = () => resolve(batch);
1537
+ tx.onerror = () => reject(tx.error);
1538
+ });
1539
+ }
1540
+ async ack(opId) {
1541
+ const tx = this.tx("outbox", "readwrite");
1542
+ tx.objectStore("outbox").delete(opId);
1543
+ return new Promise((resolve, reject) => {
1544
+ tx.oncomplete = () => resolve();
1545
+ tx.onerror = () => reject(tx.error);
1546
+ });
1547
+ }
1548
+ async nack(opId, error, retries) {
1549
+ const tx = this.tx("outbox", "readwrite");
1550
+ const store = tx.objectStore("outbox");
1551
+ const op = await this.req(store.get(opId));
1552
+ if (!op) return;
1553
+ op.retries = retries;
1554
+ op.lastError = error;
1555
+ op.status = retries >= op.maxRetries ? "failed" : "pending";
1556
+ store.put(op);
1557
+ return new Promise((resolve, reject) => {
1558
+ tx.oncomplete = () => resolve();
1559
+ tx.onerror = () => reject(tx.error);
1560
+ });
1561
+ }
1562
+ async getPendingCount() {
1563
+ const tx = this.tx("outbox");
1564
+ const idx = tx.objectStore("outbox").index("status");
1565
+ const pending = await this.req(idx.count("pending"));
1566
+ const inflight = await this.req(idx.count("inflight"));
1567
+ return pending + inflight;
1568
+ }
1569
+ // ── Search ─────────────────────────────────────────────────
1570
+ async searchMessages(query, opts) {
1571
+ const lower = query.toLowerCase();
1572
+ const limit = opts?.limit ?? 50;
1573
+ const tx = this.tx("messages");
1574
+ let all;
1575
+ if (opts?.conversationId) {
1576
+ const idx = tx.objectStore("messages").index("conversationId");
1577
+ all = await this.req(idx.getAll(opts.conversationId));
1578
+ } else {
1579
+ all = await this.req(tx.objectStore("messages").getAll());
1580
+ }
1581
+ return all.filter((m) => (m.content ?? "").toLowerCase().includes(lower)).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit);
1582
+ }
1583
+ // ── Quota ─────────────────────────────────────────────────
1584
+ async getStorageSize() {
1585
+ if (typeof navigator !== "undefined" && navigator.storage?.estimate) {
1586
+ const est = await navigator.storage.estimate();
1587
+ const total = est.usage ?? 0;
1588
+ return { messages: Math.floor(total * 0.8), conversations: Math.floor(total * 0.2), total };
1589
+ }
1590
+ const tx = this.tx(["messages", "conversations"]);
1591
+ const msgCount = await this.req(tx.objectStore("messages").count());
1592
+ const convCount = await this.req(tx.objectStore("conversations").count());
1593
+ return { messages: msgCount * 500, conversations: convCount * 200, total: msgCount * 500 + convCount * 200 };
1594
+ }
1595
+ async clearOldMessages(conversationId, keepCount) {
1596
+ const tx = this.tx("messages", "readwrite");
1597
+ const store = tx.objectStore("messages");
1598
+ const idx = store.index("conversationId");
1599
+ const all = await this.req(idx.getAll(conversationId));
1600
+ all.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1601
+ const toDelete = all.slice(keepCount);
1602
+ for (const m of toDelete) store.delete(m.id);
1603
+ return new Promise((resolve, reject) => {
1604
+ tx.oncomplete = () => resolve(toDelete.length);
1605
+ tx.onerror = () => reject(tx.error);
1606
+ });
1607
+ }
1608
+ // ── Lifecycle ───────────────────────────────────────────────
1609
+ async clear() {
1610
+ const tx = this.tx(IDB_STORES, "readwrite");
1611
+ for (const name of IDB_STORES) tx.objectStore(name).clear();
1612
+ return new Promise((resolve, reject) => {
1613
+ tx.oncomplete = () => resolve();
1614
+ tx.onerror = () => reject(tx.error);
1615
+ });
1616
+ }
1617
+ };
1618
+ var SQLiteStorage = class {
1619
+ constructor(dbPath = "prismer-offline.db") {
1620
+ this.db = null;
1621
+ this.dbPath = dbPath;
1622
+ }
1623
+ async init() {
1624
+ let Database;
1625
+ try {
1626
+ Database = require("better-sqlite3");
1627
+ } catch {
1628
+ throw new Error(
1629
+ 'SQLiteStorage requires the "better-sqlite3" package. Install it with: npm install better-sqlite3\nFor browser environments, use IndexedDBStorage instead.'
1630
+ );
1631
+ }
1632
+ this.db = new Database(this.dbPath);
1633
+ this.db.pragma("journal_mode = WAL");
1634
+ this.db.pragma("synchronous = NORMAL");
1635
+ this.db.exec(`
1636
+ CREATE TABLE IF NOT EXISTS messages (
1637
+ id TEXT PRIMARY KEY,
1638
+ clientId TEXT,
1639
+ conversationId TEXT NOT NULL,
1640
+ content TEXT,
1641
+ type TEXT DEFAULT 'text',
1642
+ senderId TEXT,
1643
+ parentId TEXT,
1644
+ status TEXT DEFAULT 'confirmed',
1645
+ metadata TEXT,
1646
+ createdAt TEXT,
1647
+ updatedAt TEXT,
1648
+ syncSeq INTEGER
1649
+ );
1650
+ CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversationId, createdAt);
1651
+ CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(createdAt);
1652
+
1653
+ CREATE TABLE IF NOT EXISTS conversations (
1654
+ id TEXT PRIMARY KEY,
1655
+ type TEXT DEFAULT 'direct',
1656
+ title TEXT,
1657
+ lastMessage TEXT,
1658
+ lastMessageAt TEXT,
1659
+ unreadCount INTEGER DEFAULT 0,
1660
+ lastReadMessageId TEXT,
1661
+ members TEXT,
1662
+ metadata TEXT,
1663
+ syncSeq INTEGER,
1664
+ updatedAt TEXT
1665
+ );
1666
+
1667
+ CREATE TABLE IF NOT EXISTS contacts (
1668
+ userId TEXT PRIMARY KEY,
1669
+ username TEXT,
1670
+ displayName TEXT,
1671
+ role TEXT,
1672
+ conversationId TEXT,
1673
+ lastMessageAt TEXT,
1674
+ unreadCount INTEGER DEFAULT 0,
1675
+ syncSeq INTEGER
1676
+ );
1677
+
1678
+ CREATE TABLE IF NOT EXISTS cursors (
1679
+ key TEXT PRIMARY KEY,
1680
+ value TEXT
1681
+ );
1682
+
1683
+ CREATE TABLE IF NOT EXISTS outbox (
1684
+ id TEXT PRIMARY KEY,
1685
+ type TEXT,
1686
+ method TEXT,
1687
+ path TEXT,
1688
+ body TEXT,
1689
+ query TEXT,
1690
+ status TEXT DEFAULT 'pending',
1691
+ createdAt INTEGER,
1692
+ retries INTEGER DEFAULT 0,
1693
+ maxRetries INTEGER DEFAULT 5,
1694
+ lastError TEXT,
1695
+ idempotencyKey TEXT,
1696
+ localData TEXT
1697
+ );
1698
+ CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status, createdAt);
1699
+ `);
1700
+ this.db.exec(`
1701
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
1702
+ content, id UNINDEXED, conversationId UNINDEXED
1703
+ );
1704
+ `);
1705
+ }
1706
+ ensureDb() {
1707
+ if (!this.db) throw new Error("SQLiteStorage not initialized. Call init() first.");
1708
+ return this.db;
1709
+ }
1710
+ // ── Messages ────────────────────────────────────────────────
1711
+ async putMessages(messages) {
1712
+ const db = this.ensureDb();
1713
+ const insert = db.prepare(`
1714
+ INSERT OR REPLACE INTO messages (id, clientId, conversationId, content, type, senderId, parentId, status, metadata, createdAt, updatedAt, syncSeq)
1715
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1716
+ `);
1717
+ const insertFts = db.prepare(`
1718
+ INSERT OR REPLACE INTO messages_fts (rowid, content, id, conversationId)
1719
+ VALUES ((SELECT rowid FROM messages WHERE id = ?), ?, ?, ?)
1720
+ `);
1721
+ const txn = db.transaction((msgs) => {
1722
+ for (const m of msgs) {
1723
+ insert.run(
1724
+ m.id,
1725
+ m.clientId ?? null,
1726
+ m.conversationId,
1727
+ m.content,
1728
+ m.type,
1729
+ m.senderId,
1730
+ m.parentId ?? null,
1731
+ m.status,
1732
+ m.metadata ? JSON.stringify(m.metadata) : null,
1733
+ m.createdAt,
1734
+ m.updatedAt ?? null,
1735
+ m.syncSeq ?? null
1736
+ );
1737
+ if (m.content) {
1738
+ insertFts.run(m.id, m.content, m.id, m.conversationId);
1739
+ }
1740
+ }
1741
+ });
1742
+ txn(messages);
1743
+ }
1744
+ async getMessages(conversationId, opts) {
1745
+ const db = this.ensureDb();
1746
+ let rows;
1747
+ if (opts.before) {
1748
+ const beforeRow = db.prepare("SELECT createdAt FROM messages WHERE id = ?").get(opts.before);
1749
+ if (beforeRow) {
1750
+ rows = db.prepare(
1751
+ "SELECT * FROM messages WHERE conversationId = ? AND createdAt < ? ORDER BY createdAt DESC LIMIT ?"
1752
+ ).all(conversationId, beforeRow.createdAt, opts.limit);
1753
+ } else {
1754
+ rows = db.prepare(
1755
+ "SELECT * FROM messages WHERE conversationId = ? ORDER BY createdAt DESC LIMIT ?"
1756
+ ).all(conversationId, opts.limit);
1757
+ }
1758
+ } else {
1759
+ rows = db.prepare(
1760
+ "SELECT * FROM messages WHERE conversationId = ? ORDER BY createdAt DESC LIMIT ?"
1761
+ ).all(conversationId, opts.limit);
1762
+ }
1763
+ return rows.reverse().map(this.rowToMessage);
1764
+ }
1765
+ async getMessage(messageId) {
1766
+ const db = this.ensureDb();
1767
+ const row = db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
1768
+ return row ? this.rowToMessage(row) : null;
1769
+ }
1770
+ async deleteMessage(messageId) {
1771
+ const db = this.ensureDb();
1772
+ db.prepare("DELETE FROM messages WHERE id = ?").run(messageId);
1773
+ db.prepare("DELETE FROM messages_fts WHERE id = ?").run(messageId);
1774
+ }
1775
+ rowToMessage(row) {
1776
+ return {
1777
+ id: row.id,
1778
+ clientId: row.clientId ?? void 0,
1779
+ conversationId: row.conversationId,
1780
+ content: row.content ?? "",
1781
+ type: row.type ?? "text",
1782
+ senderId: row.senderId ?? "",
1783
+ parentId: row.parentId ?? null,
1784
+ status: row.status ?? "confirmed",
1785
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1786
+ createdAt: row.createdAt ?? "",
1787
+ updatedAt: row.updatedAt ?? void 0,
1788
+ syncSeq: row.syncSeq ?? void 0
1789
+ };
1790
+ }
1791
+ // ── Conversations ───────────────────────────────────────────
1792
+ async putConversations(conversations) {
1793
+ const db = this.ensureDb();
1794
+ const insert = db.prepare(`
1795
+ INSERT OR REPLACE INTO conversations (id, type, title, lastMessage, lastMessageAt, unreadCount, lastReadMessageId, members, metadata, syncSeq, updatedAt)
1796
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1797
+ `);
1798
+ const txn = db.transaction((convs) => {
1799
+ for (const c of convs) {
1800
+ insert.run(
1801
+ c.id,
1802
+ c.type,
1803
+ c.title ?? null,
1804
+ c.lastMessage ? JSON.stringify(c.lastMessage) : null,
1805
+ c.lastMessageAt ?? null,
1806
+ c.unreadCount,
1807
+ c.lastReadMessageId ?? null,
1808
+ c.members ? JSON.stringify(c.members) : null,
1809
+ c.metadata ? JSON.stringify(c.metadata) : null,
1810
+ c.syncSeq ?? null,
1811
+ c.updatedAt
1812
+ );
1813
+ }
1814
+ });
1815
+ txn(conversations);
1816
+ }
1817
+ async getConversations(opts) {
1818
+ const db = this.ensureDb();
1819
+ const limit = opts?.limit ?? 50;
1820
+ const offset = opts?.offset ?? 0;
1821
+ const rows = db.prepare(
1822
+ "SELECT * FROM conversations ORDER BY COALESCE(lastMessageAt, updatedAt) DESC LIMIT ? OFFSET ?"
1823
+ ).all(limit, offset);
1824
+ return rows.map(this.rowToConversation);
1825
+ }
1826
+ async getConversation(id) {
1827
+ const db = this.ensureDb();
1828
+ const row = db.prepare("SELECT * FROM conversations WHERE id = ?").get(id);
1829
+ return row ? this.rowToConversation(row) : null;
1830
+ }
1831
+ rowToConversation(row) {
1832
+ return {
1833
+ id: row.id,
1834
+ type: row.type ?? "direct",
1835
+ title: row.title ?? void 0,
1836
+ lastMessage: row.lastMessage ? JSON.parse(row.lastMessage) : void 0,
1837
+ lastMessageAt: row.lastMessageAt ?? void 0,
1838
+ unreadCount: row.unreadCount ?? 0,
1839
+ lastReadMessageId: row.lastReadMessageId ?? void 0,
1840
+ members: row.members ? JSON.parse(row.members) : void 0,
1841
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1842
+ syncSeq: row.syncSeq ?? void 0,
1843
+ updatedAt: row.updatedAt ?? ""
1844
+ };
1845
+ }
1846
+ // ── Contacts ────────────────────────────────────────────────
1847
+ async putContacts(contacts) {
1848
+ const db = this.ensureDb();
1849
+ const insert = db.prepare(`
1850
+ INSERT OR REPLACE INTO contacts (userId, username, displayName, role, conversationId, lastMessageAt, unreadCount, syncSeq)
1851
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1852
+ `);
1853
+ const txn = db.transaction((cs) => {
1854
+ for (const c of cs) {
1855
+ insert.run(
1856
+ c.userId,
1857
+ c.username,
1858
+ c.displayName,
1859
+ c.role,
1860
+ c.conversationId,
1861
+ c.lastMessageAt ?? null,
1862
+ c.unreadCount,
1863
+ c.syncSeq ?? null
1864
+ );
1865
+ }
1866
+ });
1867
+ txn(contacts);
1868
+ }
1869
+ async getContacts() {
1870
+ const db = this.ensureDb();
1871
+ return db.prepare("SELECT * FROM contacts").all().map((row) => ({
1872
+ userId: row.userId,
1873
+ username: row.username ?? "",
1874
+ displayName: row.displayName ?? "",
1875
+ role: row.role ?? "member",
1876
+ conversationId: row.conversationId ?? "",
1877
+ lastMessageAt: row.lastMessageAt ?? void 0,
1878
+ unreadCount: row.unreadCount ?? 0,
1879
+ syncSeq: row.syncSeq ?? void 0
1880
+ }));
1881
+ }
1882
+ // ── Cursors ─────────────────────────────────────────────────
1883
+ async getCursor(key) {
1884
+ const db = this.ensureDb();
1885
+ const row = db.prepare("SELECT value FROM cursors WHERE key = ?").get(key);
1886
+ return row?.value ?? null;
1887
+ }
1888
+ async setCursor(key, value) {
1889
+ const db = this.ensureDb();
1890
+ db.prepare("INSERT OR REPLACE INTO cursors (key, value) VALUES (?, ?)").run(key, value);
1891
+ }
1892
+ // ── Outbox ──────────────────────────────────────────────────
1893
+ async enqueue(op) {
1894
+ const db = this.ensureDb();
1895
+ db.prepare(`
1896
+ INSERT OR REPLACE INTO outbox (id, type, method, path, body, query, status, createdAt, retries, maxRetries, lastError, idempotencyKey, localData)
1897
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1898
+ `).run(
1899
+ op.id,
1900
+ op.type,
1901
+ op.method,
1902
+ op.path,
1903
+ op.body ? JSON.stringify(op.body) : null,
1904
+ op.query ? JSON.stringify(op.query) : null,
1905
+ op.status,
1906
+ op.createdAt,
1907
+ op.retries,
1908
+ op.maxRetries,
1909
+ op.lastError ?? null,
1910
+ op.idempotencyKey,
1911
+ op.localData ? JSON.stringify(op.localData) : null
1912
+ );
1913
+ }
1914
+ async dequeueReady(limit) {
1915
+ const db = this.ensureDb();
1916
+ const rows = db.prepare(
1917
+ "SELECT * FROM outbox WHERE status = ? ORDER BY createdAt ASC LIMIT ?"
1918
+ ).all("pending", limit);
1919
+ const ops = rows.map(this.rowToOutbox);
1920
+ const update = db.prepare("UPDATE outbox SET status = ? WHERE id = ?");
1921
+ const txn = db.transaction((items) => {
1922
+ for (const op of items) update.run("inflight", op.id);
1923
+ });
1924
+ txn(ops);
1925
+ return ops.map((op) => ({ ...op, status: "inflight" }));
1926
+ }
1927
+ async ack(opId) {
1928
+ const db = this.ensureDb();
1929
+ db.prepare("DELETE FROM outbox WHERE id = ?").run(opId);
1930
+ }
1931
+ async nack(opId, error, retries) {
1932
+ const db = this.ensureDb();
1933
+ const row = db.prepare("SELECT maxRetries FROM outbox WHERE id = ?").get(opId);
1934
+ const newStatus = row && retries >= row.maxRetries ? "failed" : "pending";
1935
+ db.prepare("UPDATE outbox SET retries = ?, lastError = ?, status = ? WHERE id = ?").run(retries, error, newStatus, opId);
1936
+ }
1937
+ async getPendingCount() {
1938
+ const db = this.ensureDb();
1939
+ const row = db.prepare(
1940
+ "SELECT COUNT(*) as cnt FROM outbox WHERE status IN ('pending', 'inflight')"
1941
+ ).get();
1942
+ return row?.cnt ?? 0;
1943
+ }
1944
+ // ── Search (FTS5) ─────────────────────────────────────────
1945
+ async searchMessages(query, opts) {
1946
+ const db = this.ensureDb();
1947
+ const limit = opts?.limit ?? 50;
1948
+ const safeQuery = query.replace(/['"*(){}[\]^~\\]/g, " ").trim();
1949
+ if (!safeQuery) return [];
1950
+ let rows;
1951
+ if (opts?.conversationId) {
1952
+ rows = db.prepare(`
1953
+ SELECT m.* FROM messages m
1954
+ JOIN messages_fts f ON m.id = f.id
1955
+ WHERE messages_fts MATCH ? AND m.conversationId = ?
1956
+ ORDER BY m.createdAt DESC LIMIT ?
1957
+ `).all(safeQuery, opts.conversationId, limit);
1958
+ } else {
1959
+ rows = db.prepare(`
1960
+ SELECT m.* FROM messages m
1961
+ JOIN messages_fts f ON m.id = f.id
1962
+ WHERE messages_fts MATCH ?
1963
+ ORDER BY m.createdAt DESC LIMIT ?
1964
+ `).all(safeQuery, limit);
1965
+ }
1966
+ return rows.map(this.rowToMessage);
1967
+ }
1968
+ // ── Quota ─────────────────────────────────────────────────
1969
+ async getStorageSize() {
1970
+ const db = this.ensureDb();
1971
+ const pageSize = db.pragma("page_size", { simple: true }) ?? 4096;
1972
+ const pageCount = db.pragma("page_count", { simple: true }) ?? 0;
1973
+ const total = pageSize * pageCount;
1974
+ const msgCount = db.prepare("SELECT COUNT(*) as cnt FROM messages").get()?.cnt ?? 0;
1975
+ const convCount = db.prepare("SELECT COUNT(*) as cnt FROM conversations").get()?.cnt ?? 0;
1976
+ const totalRecords = msgCount + convCount;
1977
+ const msgRatio = totalRecords > 0 ? msgCount / totalRecords : 0.8;
1978
+ return {
1979
+ messages: Math.floor(total * msgRatio),
1980
+ conversations: Math.floor(total * (1 - msgRatio)),
1981
+ total
1982
+ };
1983
+ }
1984
+ async clearOldMessages(conversationId, keepCount) {
1985
+ const db = this.ensureDb();
1986
+ const keepIds = db.prepare(
1987
+ "SELECT id FROM messages WHERE conversationId = ? ORDER BY createdAt DESC LIMIT ?"
1988
+ ).all(conversationId, keepCount).map((r) => r.id);
1989
+ if (keepIds.length === 0) return 0;
1990
+ const placeholders = keepIds.map(() => "?").join(",");
1991
+ const result = db.prepare(
1992
+ `DELETE FROM messages WHERE conversationId = ? AND id NOT IN (${placeholders})`
1993
+ ).run(conversationId, ...keepIds);
1994
+ db.prepare(
1995
+ `DELETE FROM messages_fts WHERE conversationId = ? AND id NOT IN (${placeholders})`
1996
+ ).run(conversationId, ...keepIds);
1997
+ return result.changes;
1998
+ }
1999
+ // ── Lifecycle ───────────────────────────────────────────────
2000
+ async clear() {
2001
+ const db = this.ensureDb();
2002
+ db.exec("DELETE FROM messages; DELETE FROM messages_fts; DELETE FROM conversations; DELETE FROM contacts; DELETE FROM cursors; DELETE FROM outbox;");
2003
+ }
2004
+ rowToOutbox(row) {
2005
+ return {
2006
+ id: row.id,
2007
+ type: row.type,
2008
+ method: row.method,
2009
+ path: row.path,
2010
+ body: row.body ? JSON.parse(row.body) : void 0,
2011
+ query: row.query ? JSON.parse(row.query) : void 0,
2012
+ status: row.status,
2013
+ createdAt: row.createdAt,
2014
+ retries: row.retries ?? 0,
2015
+ maxRetries: row.maxRetries ?? 5,
2016
+ lastError: row.lastError ?? void 0,
2017
+ idempotencyKey: row.idempotencyKey ?? "",
2018
+ localData: row.localData ? JSON.parse(row.localData) : void 0
2019
+ };
2020
+ }
2021
+ };
2022
+
2023
+ // src/multitab.ts
2024
+ var TabCoordinator = class {
2025
+ constructor(offline, channelName = "prismer-tab-sync") {
2026
+ this.offline = offline;
2027
+ this.channelName = channelName;
2028
+ this.channel = null;
2029
+ this._isLeader = false;
2030
+ this.disposed = false;
2031
+ this.tabId = `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2032
+ }
2033
+ get isLeader() {
2034
+ return this._isLeader;
2035
+ }
2036
+ /**
2037
+ * Initialize tab coordination.
2038
+ * Claims leadership immediately (last-login-wins).
2039
+ */
2040
+ init() {
2041
+ if (typeof BroadcastChannel === "undefined") {
2042
+ this._isLeader = true;
2043
+ return;
2044
+ }
2045
+ this.channel = new BroadcastChannel(this.channelName);
2046
+ this.channel.onmessage = (e) => this.handleMessage(e.data);
2047
+ this.claimLeadership();
2048
+ }
2049
+ /**
2050
+ * Release leadership and clean up.
2051
+ */
2052
+ destroy() {
2053
+ this.disposed = true;
2054
+ if (this.channel) {
2055
+ if (this._isLeader) {
2056
+ this.broadcast({ type: "tab.release", tabId: this.tabId });
2057
+ }
2058
+ this.channel.close();
2059
+ this.channel = null;
2060
+ }
2061
+ this._isLeader = false;
2062
+ }
2063
+ /**
2064
+ * Relay a sync event to passive tabs.
2065
+ * Called by the leader tab after processing a sync event.
2066
+ */
2067
+ relaySyncEvent(event) {
2068
+ if (this._isLeader && this.channel) {
2069
+ this.broadcast({ type: "sync.event", tabId: this.tabId, payload: event });
2070
+ }
2071
+ }
2072
+ // ── Private ──────────────────────────────────────────────────
2073
+ claimLeadership() {
2074
+ this._isLeader = true;
2075
+ this.broadcast({ type: "tab.claim", tabId: this.tabId });
2076
+ this.onBecomeLeader();
2077
+ }
2078
+ demoteToPassive() {
2079
+ if (!this._isLeader) return;
2080
+ this._isLeader = false;
2081
+ this.onBecomePassive();
2082
+ }
2083
+ handleMessage(msg) {
2084
+ if (this.disposed) return;
2085
+ switch (msg.type) {
2086
+ case "tab.claim": {
2087
+ if (msg.tabId !== this.tabId) {
2088
+ this.demoteToPassive();
2089
+ this.broadcast({ type: "tab.ack", tabId: this.tabId });
2090
+ }
2091
+ break;
2092
+ }
2093
+ case "tab.release": {
2094
+ if (!this._isLeader) {
2095
+ this.claimLeadership();
2096
+ }
2097
+ break;
2098
+ }
2099
+ case "sync.event": {
2100
+ if (!this._isLeader && msg.payload) {
2101
+ this.offline["applySyncEvent"](msg.payload).catch(() => {
2102
+ });
2103
+ }
2104
+ break;
2105
+ }
2106
+ }
2107
+ }
2108
+ onBecomeLeader() {
2109
+ }
2110
+ onBecomePassive() {
2111
+ this.offline.stopContinuousSync();
2112
+ }
2113
+ broadcast(msg) {
2114
+ try {
2115
+ this.channel?.postMessage(msg);
2116
+ } catch {
2117
+ }
2118
+ }
2119
+ };
2120
+
2121
+ // src/encryption.ts
2122
+ function getSubtleCrypto() {
2123
+ if (typeof globalThis.crypto?.subtle !== "undefined") {
2124
+ return globalThis.crypto.subtle;
2125
+ }
2126
+ try {
2127
+ const { webcrypto } = require("crypto");
2128
+ return webcrypto.subtle;
2129
+ } catch {
2130
+ throw new Error("No SubtleCrypto available. Requires browser or Node.js 16+.");
2131
+ }
2132
+ }
2133
+ function getRandomValues(arr) {
2134
+ if (typeof globalThis.crypto?.getRandomValues !== "undefined") {
2135
+ return globalThis.crypto.getRandomValues(arr);
2136
+ }
2137
+ try {
2138
+ const { webcrypto } = require("crypto");
2139
+ return webcrypto.getRandomValues(arr);
2140
+ } catch {
2141
+ throw new Error("No crypto.getRandomValues available.");
2142
+ }
2143
+ }
2144
+ var subtle = () => getSubtleCrypto();
2145
+ var PBKDF2_ITERATIONS = 1e5;
2146
+ var SALT_LENGTH = 16;
2147
+ var IV_LENGTH = 12;
2148
+ var KEY_LENGTH = 256;
2149
+ var E2EEncryption = class {
2150
+ constructor() {
2151
+ this.masterKey = null;
2152
+ this.keyPair = null;
2153
+ this.sessionKeys = /* @__PURE__ */ new Map();
2154
+ // conversationId → AES key
2155
+ this.salt = null;
2156
+ }
2157
+ /**
2158
+ * Initialize encryption with user passphrase.
2159
+ * Derives a master key via PBKDF2 and generates an ECDH key pair.
2160
+ */
2161
+ async init(passphrase) {
2162
+ this.salt = getRandomValues(new Uint8Array(SALT_LENGTH));
2163
+ const passphraseKey = await subtle().importKey(
2164
+ "raw",
2165
+ new TextEncoder().encode(passphrase),
2166
+ "PBKDF2",
2167
+ false,
2168
+ ["deriveKey"]
2169
+ );
2170
+ this.masterKey = await subtle().deriveKey(
2171
+ {
2172
+ name: "PBKDF2",
2173
+ salt: this.salt,
2174
+ iterations: PBKDF2_ITERATIONS,
2175
+ hash: "SHA-256"
2176
+ },
2177
+ passphraseKey,
2178
+ { name: "AES-GCM", length: KEY_LENGTH },
2179
+ false,
2180
+ ["encrypt", "decrypt"]
2181
+ );
2182
+ this.keyPair = await subtle().generateKey(
2183
+ { name: "ECDH", namedCurve: "P-256" },
2184
+ true,
2185
+ ["deriveKey"]
2186
+ );
2187
+ }
2188
+ /**
2189
+ * Export public key for sharing with conversation peers.
2190
+ */
2191
+ async exportPublicKey() {
2192
+ if (!this.keyPair) throw new Error("E2E not initialized. Call init() first.");
2193
+ return subtle().exportKey("jwk", this.keyPair.publicKey);
2194
+ }
2195
+ /**
2196
+ * Derive a shared session key for a conversation using ECDH.
2197
+ * Call this with each peer's public key.
2198
+ */
2199
+ async deriveSessionKey(conversationId, peerPublicKey) {
2200
+ if (!this.keyPair) throw new Error("E2E not initialized. Call init() first.");
2201
+ const importedPeerKey = await subtle().importKey(
2202
+ "jwk",
2203
+ peerPublicKey,
2204
+ { name: "ECDH", namedCurve: "P-256" },
2205
+ false,
2206
+ []
2207
+ );
2208
+ const sessionKey = await subtle().deriveKey(
2209
+ { name: "ECDH", public: importedPeerKey },
2210
+ this.keyPair.privateKey,
2211
+ { name: "AES-GCM", length: KEY_LENGTH },
2212
+ false,
2213
+ ["encrypt", "decrypt"]
2214
+ );
2215
+ this.sessionKeys.set(conversationId, sessionKey);
2216
+ }
2217
+ /**
2218
+ * Set a pre-shared session key for a conversation.
2219
+ * Useful when the key is exchanged out-of-band or derived from a group key.
2220
+ */
2221
+ async setSessionKey(conversationId, rawKey) {
2222
+ const key = await subtle().importKey(
2223
+ "raw",
2224
+ rawKey,
2225
+ { name: "AES-GCM", length: KEY_LENGTH },
2226
+ false,
2227
+ ["encrypt", "decrypt"]
2228
+ );
2229
+ this.sessionKeys.set(conversationId, key);
2230
+ }
2231
+ /**
2232
+ * Generate a random session key for a conversation.
2233
+ * Returns the raw key bytes for sharing with peers.
2234
+ */
2235
+ async generateSessionKey(conversationId) {
2236
+ const key = await subtle().generateKey(
2237
+ { name: "AES-GCM", length: KEY_LENGTH },
2238
+ true,
2239
+ ["encrypt", "decrypt"]
2240
+ );
2241
+ this.sessionKeys.set(conversationId, key);
2242
+ return subtle().exportKey("raw", key);
2243
+ }
2244
+ /**
2245
+ * Encrypt plaintext for a conversation.
2246
+ * Returns base64-encoded ciphertext with prepended IV.
2247
+ */
2248
+ async encrypt(conversationId, plaintext) {
2249
+ const key = this.sessionKeys.get(conversationId);
2250
+ if (!key) throw new Error(`No session key for conversation ${conversationId}. Call deriveSessionKey() first.`);
2251
+ const iv = getRandomValues(new Uint8Array(IV_LENGTH));
2252
+ const encoded = new TextEncoder().encode(plaintext);
2253
+ const ciphertext = await subtle().encrypt(
2254
+ { name: "AES-GCM", iv },
2255
+ key,
2256
+ encoded
2257
+ );
2258
+ const combined = new Uint8Array(iv.length + ciphertext.byteLength);
2259
+ combined.set(iv, 0);
2260
+ combined.set(new Uint8Array(ciphertext), iv.length);
2261
+ return arrayBufferToBase64(combined.buffer);
2262
+ }
2263
+ /**
2264
+ * Decrypt ciphertext from a conversation.
2265
+ * Expects base64-encoded data with prepended IV.
2266
+ */
2267
+ async decrypt(conversationId, ciphertext) {
2268
+ const key = this.sessionKeys.get(conversationId);
2269
+ if (!key) throw new Error(`No session key for conversation ${conversationId}. Call deriveSessionKey() first.`);
2270
+ const combined = base64ToArrayBuffer(ciphertext);
2271
+ const iv = combined.slice(0, IV_LENGTH);
2272
+ const data = combined.slice(IV_LENGTH);
2273
+ const decrypted = await subtle().decrypt(
2274
+ { name: "AES-GCM", iv: new Uint8Array(iv) },
2275
+ key,
2276
+ data
2277
+ );
2278
+ return new TextDecoder().decode(decrypted);
2279
+ }
2280
+ /**
2281
+ * Check if a session key exists for a conversation.
2282
+ */
2283
+ hasSessionKey(conversationId) {
2284
+ return this.sessionKeys.has(conversationId);
2285
+ }
2286
+ /**
2287
+ * Remove session key for a conversation.
2288
+ */
2289
+ removeSessionKey(conversationId) {
2290
+ this.sessionKeys.delete(conversationId);
2291
+ }
2292
+ /**
2293
+ * Clear all keys and reset state.
2294
+ */
2295
+ destroy() {
2296
+ this.masterKey = null;
2297
+ this.keyPair = null;
2298
+ this.sessionKeys.clear();
2299
+ this.salt = null;
2300
+ }
2301
+ };
2302
+ function arrayBufferToBase64(buffer) {
2303
+ if (typeof btoa !== "undefined") {
2304
+ const bytes = new Uint8Array(buffer);
2305
+ let binary = "";
2306
+ for (let i = 0; i < bytes.byteLength; i++) {
2307
+ binary += String.fromCharCode(bytes[i]);
2308
+ }
2309
+ return btoa(binary);
2310
+ }
2311
+ return Buffer.from(buffer).toString("base64");
2312
+ }
2313
+ function base64ToArrayBuffer(base64) {
2314
+ if (typeof atob !== "undefined") {
2315
+ const binary = atob(base64);
2316
+ const bytes = new Uint8Array(binary.length);
2317
+ for (let i = 0; i < binary.length; i++) {
2318
+ bytes[i] = binary.charCodeAt(i);
2319
+ }
2320
+ return bytes.buffer;
2321
+ }
2322
+ const buf = Buffer.from(base64, "base64");
2323
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
2324
+ }
2325
+
475
2326
  // src/index.ts
476
2327
  var AccountClient = class {
477
2328
  constructor(_r) {
@@ -506,8 +2357,8 @@ var DirectClient = class {
506
2357
  /** Get direct message history with a user */
507
2358
  async getMessages(userId, options) {
508
2359
  const query = {};
509
- if (options?.limit) query.limit = String(options.limit);
510
- if (options?.offset) query.offset = String(options.offset);
2360
+ if (options?.limit != null) query.limit = String(options.limit);
2361
+ if (options?.offset != null) query.offset = String(options.offset);
511
2362
  return this._r("GET", `/api/im/direct/${userId}/messages`, void 0, query);
512
2363
  }
513
2364
  };
@@ -539,8 +2390,8 @@ var GroupsClient = class {
539
2390
  /** Get group message history */
540
2391
  async getMessages(groupId, options) {
541
2392
  const query = {};
542
- if (options?.limit) query.limit = String(options.limit);
543
- if (options?.offset) query.offset = String(options.offset);
2393
+ if (options?.limit != null) query.limit = String(options.limit);
2394
+ if (options?.offset != null) query.offset = String(options.offset);
544
2395
  return this._r("GET", `/api/im/groups/${groupId}/messages`, void 0, query);
545
2396
  }
546
2397
  /** Add a member to a group (owner/admin only) */
@@ -592,8 +2443,8 @@ var MessagesClient = class {
592
2443
  /** Get message history for a conversation */
593
2444
  async getHistory(conversationId, options) {
594
2445
  const query = {};
595
- if (options?.limit) query.limit = String(options.limit);
596
- if (options?.offset) query.offset = String(options.offset);
2446
+ if (options?.limit != null) query.limit = String(options.limit);
2447
+ if (options?.offset != null) query.offset = String(options.offset);
597
2448
  return this._r("GET", `/api/im/messages/${conversationId}`, void 0, query);
598
2449
  }
599
2450
  /** Edit a message */
@@ -653,8 +2504,8 @@ var CreditsClient = class {
653
2504
  /** Get credit transaction history */
654
2505
  async transactions(options) {
655
2506
  const query = {};
656
- if (options?.limit) query.limit = String(options.limit);
657
- if (options?.offset) query.offset = String(options.offset);
2507
+ if (options?.limit != null) query.limit = String(options.limit);
2508
+ if (options?.offset != null) query.offset = String(options.offset);
658
2509
  return this._r("GET", "/api/im/credits/transactions", void 0, query);
659
2510
  }
660
2511
  };
@@ -685,6 +2536,211 @@ var WorkspaceClient = class {
685
2536
  return this._r("GET", "/api/im/workspace/mentions/autocomplete", void 0, q);
686
2537
  }
687
2538
  };
2539
+ function guessMimeType(fileName) {
2540
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
2541
+ const map = {
2542
+ png: "image/png",
2543
+ jpg: "image/jpeg",
2544
+ jpeg: "image/jpeg",
2545
+ gif: "image/gif",
2546
+ webp: "image/webp",
2547
+ svg: "image/svg+xml",
2548
+ ico: "image/x-icon",
2549
+ bmp: "image/bmp",
2550
+ pdf: "application/pdf",
2551
+ doc: "application/msword",
2552
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2553
+ xls: "application/vnd.ms-excel",
2554
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2555
+ ppt: "application/vnd.ms-powerpoint",
2556
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2557
+ txt: "text/plain",
2558
+ csv: "text/csv",
2559
+ html: "text/html",
2560
+ css: "text/css",
2561
+ js: "text/javascript",
2562
+ json: "application/json",
2563
+ xml: "application/xml",
2564
+ md: "text/markdown",
2565
+ yaml: "text/yaml",
2566
+ yml: "text/yaml",
2567
+ zip: "application/zip",
2568
+ gz: "application/gzip",
2569
+ tar: "application/x-tar",
2570
+ mp3: "audio/mpeg",
2571
+ wav: "audio/wav",
2572
+ mp4: "video/mp4",
2573
+ webm: "video/webm"
2574
+ };
2575
+ return map[ext] || "application/octet-stream";
2576
+ }
2577
+ var FilesClient = class {
2578
+ constructor(_r, _baseUrl, _fetchFn, _getAuthHeaders) {
2579
+ this._r = _r;
2580
+ this._baseUrl = _baseUrl;
2581
+ this._fetchFn = _fetchFn;
2582
+ this._getAuthHeaders = _getAuthHeaders;
2583
+ }
2584
+ /** Get a presigned upload URL */
2585
+ async presign(options) {
2586
+ return this._r("POST", "/api/im/files/presign", options);
2587
+ }
2588
+ /** Confirm an uploaded file (triggers validation + CDN activation) */
2589
+ async confirm(uploadId) {
2590
+ return this._r("POST", "/api/im/files/confirm", { uploadId });
2591
+ }
2592
+ /** Get storage quota */
2593
+ async quota() {
2594
+ return this._r("GET", "/api/im/files/quota");
2595
+ }
2596
+ /** Delete a file */
2597
+ async delete(uploadId) {
2598
+ return this._r("DELETE", `/api/im/files/${uploadId}`);
2599
+ }
2600
+ /** List allowed MIME types */
2601
+ async types() {
2602
+ return this._r("GET", "/api/im/files/types");
2603
+ }
2604
+ /** Initialize a multipart upload (for files > 10 MB) */
2605
+ async initMultipart(opts) {
2606
+ return this._r("POST", "/api/im/files/upload/init", opts);
2607
+ }
2608
+ /** Complete a multipart upload */
2609
+ async completeMultipart(uploadId, parts) {
2610
+ return this._r("POST", "/api/im/files/upload/complete", { uploadId, parts });
2611
+ }
2612
+ // --------------------------------------------------------------------------
2613
+ // High-level convenience methods
2614
+ // --------------------------------------------------------------------------
2615
+ /**
2616
+ * Upload a file (full lifecycle: presign → upload → confirm).
2617
+ *
2618
+ * @param input - File, Blob, Buffer, Uint8Array, or file path (Node.js string)
2619
+ * @param opts - Optional fileName, mimeType, onProgress
2620
+ * @returns Confirmed upload result with CDN URL
2621
+ */
2622
+ async upload(input, opts) {
2623
+ let bytes;
2624
+ let fileName;
2625
+ if (typeof input === "string") {
2626
+ const fs = await import("fs");
2627
+ const path = await import("path");
2628
+ const buf = await fs.promises.readFile(input);
2629
+ bytes = new Uint8Array(buf);
2630
+ fileName = opts?.fileName || path.basename(input);
2631
+ } else if (typeof Blob !== "undefined" && input instanceof Blob) {
2632
+ const ab = await input.arrayBuffer();
2633
+ bytes = new Uint8Array(ab);
2634
+ fileName = opts?.fileName || (input instanceof File ? input.name : "");
2635
+ if (!fileName) throw new Error("fileName is required when uploading Blob without name");
2636
+ } else if (input instanceof Uint8Array) {
2637
+ bytes = input;
2638
+ fileName = opts?.fileName || "";
2639
+ if (!fileName) throw new Error("fileName is required when uploading Buffer or Uint8Array");
2640
+ } else {
2641
+ throw new Error("Unsupported input type");
2642
+ }
2643
+ const fileSize = bytes.byteLength;
2644
+ const mimeType = opts?.mimeType || guessMimeType(fileName);
2645
+ if (fileSize > 50 * 1024 * 1024) {
2646
+ throw new Error("File exceeds maximum size of 50 MB");
2647
+ }
2648
+ if (fileSize <= 10 * 1024 * 1024) {
2649
+ return this._uploadSimple(bytes, fileName, fileSize, mimeType, opts?.onProgress);
2650
+ }
2651
+ return this._uploadMultipart(bytes, fileName, fileSize, mimeType, opts?.onProgress);
2652
+ }
2653
+ /**
2654
+ * Upload a file and send it as a message in one call.
2655
+ *
2656
+ * @param conversationId - Target conversation
2657
+ * @param input - File input (same as upload())
2658
+ * @param opts - Upload options + optional message content/parentId
2659
+ */
2660
+ async sendFile(conversationId, input, opts) {
2661
+ const uploaded = await this.upload(input, opts);
2662
+ const msgRes = await this._r("POST", `/api/im/messages/${conversationId}`, {
2663
+ content: opts?.content || uploaded.fileName,
2664
+ type: "file",
2665
+ metadata: {
2666
+ uploadId: uploaded.uploadId,
2667
+ fileUrl: uploaded.cdnUrl,
2668
+ fileName: uploaded.fileName,
2669
+ fileSize: uploaded.fileSize,
2670
+ mimeType: uploaded.mimeType
2671
+ },
2672
+ parentId: opts?.parentId
2673
+ });
2674
+ if (!msgRes.ok) {
2675
+ throw new Error(msgRes.error?.message || "Failed to send file message");
2676
+ }
2677
+ return { upload: uploaded, message: msgRes.data };
2678
+ }
2679
+ // --------------------------------------------------------------------------
2680
+ // Private upload helpers
2681
+ // --------------------------------------------------------------------------
2682
+ async _uploadSimple(bytes, fileName, fileSize, mimeType, onProgress) {
2683
+ const presignRes = await this.presign({ fileName, fileSize, mimeType });
2684
+ if (!presignRes.ok || !presignRes.data) {
2685
+ throw new Error(presignRes.error?.message || "Presign failed");
2686
+ }
2687
+ const { uploadId, url, fields } = presignRes.data;
2688
+ const formData = new FormData();
2689
+ const isS3 = url.startsWith("http");
2690
+ const uploadUrl = isS3 ? url : `${this._baseUrl}${url}`;
2691
+ if (isS3) {
2692
+ for (const [k, v] of Object.entries(fields)) formData.append(k, v);
2693
+ }
2694
+ const ab = new ArrayBuffer(bytes.byteLength);
2695
+ new Uint8Array(ab).set(bytes);
2696
+ formData.append("file", new Blob([ab], { type: mimeType }), fileName);
2697
+ const headers = {};
2698
+ if (!isS3) Object.assign(headers, this._getAuthHeaders());
2699
+ const resp = await this._fetchFn(uploadUrl, { method: "POST", body: formData, headers });
2700
+ if (!resp.ok) {
2701
+ const text = await resp.text();
2702
+ throw new Error(`Upload failed (${resp.status}): ${text}`);
2703
+ }
2704
+ onProgress?.(fileSize, fileSize);
2705
+ const confirmRes = await this.confirm(uploadId);
2706
+ if (!confirmRes.ok || !confirmRes.data) {
2707
+ throw new Error(confirmRes.error?.message || "Confirm failed");
2708
+ }
2709
+ return confirmRes.data;
2710
+ }
2711
+ async _uploadMultipart(bytes, fileName, fileSize, mimeType, onProgress) {
2712
+ const initRes = await this.initMultipart({ fileName, fileSize, mimeType });
2713
+ if (!initRes.ok || !initRes.data) {
2714
+ throw new Error(initRes.error?.message || "Multipart init failed");
2715
+ }
2716
+ const { uploadId, parts: partUrls } = initRes.data;
2717
+ const CHUNK_SIZE = 5 * 1024 * 1024;
2718
+ const completedParts = [];
2719
+ let uploaded = 0;
2720
+ for (const part of partUrls) {
2721
+ const start = (part.partNumber - 1) * CHUNK_SIZE;
2722
+ const end = Math.min(start + CHUNK_SIZE, fileSize);
2723
+ const chunk = bytes.slice(start, end);
2724
+ const isS3 = part.url.startsWith("http");
2725
+ const partUrl = isS3 ? part.url : `${this._baseUrl}${part.url}`;
2726
+ const headers = { "Content-Type": mimeType };
2727
+ if (!isS3) Object.assign(headers, this._getAuthHeaders());
2728
+ const resp = await this._fetchFn(partUrl, { method: "PUT", body: chunk, headers });
2729
+ if (!resp.ok) {
2730
+ throw new Error(`Part ${part.partNumber} upload failed (${resp.status})`);
2731
+ }
2732
+ const etag = resp.headers.get("ETag") || `"part-${part.partNumber}"`;
2733
+ completedParts.push({ partNumber: part.partNumber, etag });
2734
+ uploaded += chunk.byteLength;
2735
+ onProgress?.(uploaded, fileSize);
2736
+ }
2737
+ const completeRes = await this.completeMultipart(uploadId, completedParts);
2738
+ if (!completeRes.ok || !completeRes.data) {
2739
+ throw new Error(completeRes.error?.message || "Multipart complete failed");
2740
+ }
2741
+ return completeRes.data;
2742
+ }
2743
+ };
688
2744
  var IMRealtimeClient = class {
689
2745
  constructor(_wsBase) {
690
2746
  this._wsBase = _wsBase;
@@ -708,7 +2764,7 @@ var IMRealtimeClient = class {
708
2764
  }
709
2765
  };
710
2766
  var IMClient = class {
711
- constructor(request, wsBase) {
2767
+ constructor(request, wsBase, fetchFn, getAuthHeaders, offlineManager) {
712
2768
  this.account = new AccountClient(request);
713
2769
  this.direct = new DirectClient(request);
714
2770
  this.groups = new GroupsClient(request);
@@ -718,7 +2774,9 @@ var IMClient = class {
718
2774
  this.bindings = new BindingsClient(request);
719
2775
  this.credits = new CreditsClient(request);
720
2776
  this.workspace = new WorkspaceClient(request);
2777
+ this.files = new FilesClient(request, wsBase, fetchFn, getAuthHeaders);
721
2778
  this.realtime = new IMRealtimeClient(wsBase);
2779
+ this.offline = offlineManager ?? null;
722
2780
  }
723
2781
  /** IM health check */
724
2782
  async health() {
@@ -727,6 +2785,7 @@ var IMClient = class {
727
2785
  };
728
2786
  var PrismerClient = class {
729
2787
  constructor(config = {}) {
2788
+ this._offlineManager = null;
730
2789
  if (config.apiKey && !config.apiKey.startsWith("sk-prismer-") && !config.apiKey.startsWith("eyJ")) {
731
2790
  console.warn('Warning: API key should start with "sk-prismer-" (or "eyJ" for IM JWT)');
732
2791
  }
@@ -736,11 +2795,32 @@ var PrismerClient = class {
736
2795
  this.timeout = config.timeout || 3e4;
737
2796
  this.fetchFn = config.fetch || fetch;
738
2797
  this.imAgent = config.imAgent;
2798
+ if (config.offline) {
2799
+ this._offlineManager = new OfflineManager(
2800
+ config.offline.storage,
2801
+ (m, p, b, q) => this._request(m, p, b, q),
2802
+ config.offline
2803
+ );
2804
+ this._offlineManager.init().catch(
2805
+ (err) => console.warn("[PrismerSDK] Offline storage init failed:", err)
2806
+ );
2807
+ }
2808
+ const imRequest = this._offlineManager ? (m, p, b, q) => this._offlineManager.dispatch(m, p, b, q) : (m, p, b, q) => this._request(m, p, b, q);
739
2809
  this.im = new IMClient(
740
- (method, path, body, query) => this._request(method, path, body, query),
741
- this.baseUrl
2810
+ imRequest,
2811
+ this.baseUrl,
2812
+ this.fetchFn,
2813
+ () => this._getAuthHeaders(),
2814
+ this._offlineManager
742
2815
  );
743
2816
  }
2817
+ /** Build auth headers for raw HTTP requests (used by file upload) */
2818
+ _getAuthHeaders() {
2819
+ const headers = {};
2820
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
2821
+ if (this.imAgent) headers["X-IM-Agent"] = this.imAgent;
2822
+ return headers;
2823
+ }
744
2824
  /**
745
2825
  * Set or update the auth token (API key or IM JWT).
746
2826
  * Useful after anonymous registration to set the returned JWT.
@@ -748,10 +2828,16 @@ var PrismerClient = class {
748
2828
  setToken(token) {
749
2829
  this.apiKey = token;
750
2830
  }
2831
+ /** Cleanup resources (offline manager, timers). Call when disposing the client. */
2832
+ async destroy() {
2833
+ if (this._offlineManager) {
2834
+ await this._offlineManager.destroy();
2835
+ }
2836
+ }
751
2837
  // --------------------------------------------------------------------------
752
2838
  // Internal request helper
753
2839
  // --------------------------------------------------------------------------
754
- async _request(method, path, body, query) {
2840
+ async _request(method, path, body, query, _isRetry) {
755
2841
  const controller = new AbortController();
756
2842
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
757
2843
  try {
@@ -773,6 +2859,16 @@ var PrismerClient = class {
773
2859
  }
774
2860
  const response = await this.fetchFn(url, init);
775
2861
  const data = await response.json();
2862
+ if (response.status === 401 && this.apiKey.startsWith("eyJ") && !_isRetry && !path.includes("/token/refresh")) {
2863
+ try {
2864
+ const refreshRes = await this._request("POST", "/api/im/token/refresh", void 0, void 0, true);
2865
+ if (refreshRes?.ok && refreshRes?.data?.token) {
2866
+ this.apiKey = refreshRes.data.token;
2867
+ return this._request(method, path, body, query, true);
2868
+ }
2869
+ } catch {
2870
+ }
2871
+ }
776
2872
  if (!response.ok) {
777
2873
  const err = data.error || { code: "HTTP_ERROR", message: `Request failed with status ${response.status}` };
778
2874
  return { ...data, success: false, ok: false, error: err };
@@ -853,19 +2949,27 @@ function createClient(config) {
853
2949
  // Annotate the CommonJS export names for ESM import in node:
854
2950
  0 && (module.exports = {
855
2951
  AccountClient,
2952
+ AttachmentQueue,
856
2953
  BindingsClient,
857
2954
  ContactsClient,
858
2955
  ConversationsClient,
859
2956
  CreditsClient,
860
2957
  DirectClient,
2958
+ E2EEncryption,
861
2959
  ENVIRONMENTS,
2960
+ FilesClient,
862
2961
  GroupsClient,
863
2962
  IMClient,
864
2963
  IMRealtimeClient,
2964
+ IndexedDBStorage,
2965
+ MemoryStorage,
865
2966
  MessagesClient,
2967
+ OfflineManager,
866
2968
  PrismerClient,
867
2969
  RealtimeSSEClient,
868
2970
  RealtimeWSClient,
2971
+ SQLiteStorage,
2972
+ TabCoordinator,
869
2973
  WorkspaceClient,
870
2974
  createClient
871
2975
  });