@debros/network-ts-sdk 0.1.5 → 0.3.1

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
@@ -27,32 +27,63 @@ var SDKError = class _SDKError extends Error {
27
27
  var HttpClient = class {
28
28
  constructor(config) {
29
29
  this.baseURL = config.baseURL.replace(/\/$/, "");
30
- this.timeout = config.timeout ?? 3e4;
30
+ this.timeout = config.timeout ?? 6e4;
31
31
  this.maxRetries = config.maxRetries ?? 3;
32
32
  this.retryDelayMs = config.retryDelayMs ?? 1e3;
33
33
  this.fetch = config.fetch ?? globalThis.fetch;
34
34
  }
35
35
  setApiKey(apiKey) {
36
36
  this.apiKey = apiKey;
37
- this.jwt = void 0;
38
37
  }
39
38
  setJwt(jwt) {
40
39
  this.jwt = jwt;
41
- this.apiKey = void 0;
40
+ if (typeof console !== "undefined") {
41
+ console.log(
42
+ "[HttpClient] JWT set:",
43
+ !!jwt,
44
+ "API key still present:",
45
+ !!this.apiKey
46
+ );
47
+ }
42
48
  }
43
- getAuthHeaders() {
49
+ getAuthHeaders(path) {
44
50
  const headers = {};
45
- if (this.jwt) {
46
- headers["Authorization"] = `Bearer ${this.jwt}`;
47
- } else if (this.apiKey) {
48
- headers["X-API-Key"] = this.apiKey;
51
+ const isDbOperation = path.includes("/v1/rqlite/");
52
+ const isPubSubOperation = path.includes("/v1/pubsub/");
53
+ const isProxyOperation = path.includes("/v1/proxy/");
54
+ const isCacheOperation = path.includes("/v1/cache/");
55
+ const isAuthOperation = path.includes("/v1/auth/");
56
+ if (isDbOperation || isPubSubOperation || isProxyOperation || isCacheOperation) {
57
+ if (this.apiKey) {
58
+ headers["X-API-Key"] = this.apiKey;
59
+ } else if (this.jwt) {
60
+ headers["Authorization"] = `Bearer ${this.jwt}`;
61
+ }
62
+ } else if (isAuthOperation) {
63
+ if (this.apiKey) {
64
+ headers["X-API-Key"] = this.apiKey;
65
+ }
66
+ if (this.jwt) {
67
+ headers["Authorization"] = `Bearer ${this.jwt}`;
68
+ }
69
+ } else {
70
+ if (this.jwt) {
71
+ headers["Authorization"] = `Bearer ${this.jwt}`;
72
+ }
73
+ if (this.apiKey) {
74
+ headers["X-API-Key"] = this.apiKey;
75
+ }
49
76
  }
50
77
  return headers;
51
78
  }
52
79
  getAuthToken() {
53
80
  return this.jwt || this.apiKey;
54
81
  }
82
+ getApiKey() {
83
+ return this.apiKey;
84
+ }
55
85
  async request(method, path, options = {}) {
86
+ const startTime = performance.now();
56
87
  const url = new URL(this.baseURL + path);
57
88
  if (options.query) {
58
89
  Object.entries(options.query).forEach(([key, value]) => {
@@ -61,20 +92,70 @@ var HttpClient = class {
61
92
  }
62
93
  const headers = {
63
94
  "Content-Type": "application/json",
64
- ...this.getAuthHeaders(),
95
+ ...this.getAuthHeaders(path),
65
96
  ...options.headers
66
97
  };
98
+ const controller = new AbortController();
99
+ const requestTimeout = options.timeout ?? this.timeout;
100
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
67
101
  const fetchOptions = {
68
102
  method,
69
103
  headers,
70
- signal: AbortSignal.timeout(this.timeout)
104
+ signal: controller.signal
71
105
  };
72
106
  if (options.body !== void 0) {
73
107
  fetchOptions.body = JSON.stringify(options.body);
74
108
  }
75
- return this.requestWithRetry(url.toString(), fetchOptions);
109
+ try {
110
+ const result = await this.requestWithRetry(
111
+ url.toString(),
112
+ fetchOptions,
113
+ 0,
114
+ startTime
115
+ );
116
+ const duration = performance.now() - startTime;
117
+ if (typeof console !== "undefined") {
118
+ console.log(
119
+ `[HttpClient] ${method} ${path} completed in ${duration.toFixed(2)}ms`
120
+ );
121
+ }
122
+ return result;
123
+ } catch (error) {
124
+ const duration = performance.now() - startTime;
125
+ if (typeof console !== "undefined") {
126
+ const isCacheGetNotFound = path === "/v1/cache/get" && error instanceof SDKError && (error.httpStatus === 404 || error.httpStatus === 500 && error.message?.toLowerCase().includes("key not found"));
127
+ const isBlockedUsersNotFound = path === "/v1/rqlite/find-one" && error instanceof SDKError && error.httpStatus === 404 && options.body && (() => {
128
+ try {
129
+ const body = typeof options.body === "string" ? JSON.parse(options.body) : options.body;
130
+ return body.table === "blocked_users";
131
+ } catch {
132
+ return false;
133
+ }
134
+ })();
135
+ const isConversationParticipantNotFound = path === "/v1/rqlite/find-one" && error instanceof SDKError && error.httpStatus === 404 && options.body && (() => {
136
+ try {
137
+ const body = typeof options.body === "string" ? JSON.parse(options.body) : options.body;
138
+ return body.table === "conversation_participants";
139
+ } catch {
140
+ return false;
141
+ }
142
+ })();
143
+ if (isCacheGetNotFound || isBlockedUsersNotFound || isConversationParticipantNotFound) {
144
+ } else {
145
+ console.error(
146
+ `[HttpClient] ${method} ${path} failed after ${duration.toFixed(
147
+ 2
148
+ )}ms:`,
149
+ error
150
+ );
151
+ }
152
+ }
153
+ throw error;
154
+ } finally {
155
+ clearTimeout(timeoutId);
156
+ }
76
157
  }
77
- async requestWithRetry(url, options, attempt = 0) {
158
+ async requestWithRetry(url, options, attempt = 0, startTime) {
78
159
  try {
79
160
  const response = await this.fetch(url, options);
80
161
  if (!response.ok) {
@@ -96,7 +177,7 @@ var HttpClient = class {
96
177
  await new Promise(
97
178
  (resolve) => setTimeout(resolve, this.retryDelayMs * (attempt + 1))
98
179
  );
99
- return this.requestWithRetry(url, options, attempt + 1);
180
+ return this.requestWithRetry(url, options, attempt + 1, startTime);
100
181
  }
101
182
  throw error;
102
183
  }
@@ -113,6 +194,90 @@ var HttpClient = class {
113
194
  async delete(path, options) {
114
195
  return this.request("DELETE", path, options);
115
196
  }
197
+ /**
198
+ * Upload a file using multipart/form-data
199
+ * This is a special method for file uploads that bypasses JSON serialization
200
+ */
201
+ async uploadFile(path, formData, options) {
202
+ const startTime = performance.now();
203
+ const url = new URL(this.baseURL + path);
204
+ const headers = {
205
+ ...this.getAuthHeaders(path)
206
+ // Don't set Content-Type - browser will set it with boundary
207
+ };
208
+ const controller = new AbortController();
209
+ const requestTimeout = options?.timeout ?? this.timeout * 5;
210
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
211
+ const fetchOptions = {
212
+ method: "POST",
213
+ headers,
214
+ body: formData,
215
+ signal: controller.signal
216
+ };
217
+ try {
218
+ const result = await this.requestWithRetry(
219
+ url.toString(),
220
+ fetchOptions,
221
+ 0,
222
+ startTime
223
+ );
224
+ const duration = performance.now() - startTime;
225
+ if (typeof console !== "undefined") {
226
+ console.log(
227
+ `[HttpClient] POST ${path} (upload) completed in ${duration.toFixed(
228
+ 2
229
+ )}ms`
230
+ );
231
+ }
232
+ return result;
233
+ } catch (error) {
234
+ const duration = performance.now() - startTime;
235
+ if (typeof console !== "undefined") {
236
+ console.error(
237
+ `[HttpClient] POST ${path} (upload) failed after ${duration.toFixed(
238
+ 2
239
+ )}ms:`,
240
+ error
241
+ );
242
+ }
243
+ throw error;
244
+ } finally {
245
+ clearTimeout(timeoutId);
246
+ }
247
+ }
248
+ /**
249
+ * Get a binary response (returns Response object for streaming)
250
+ */
251
+ async getBinary(path) {
252
+ const url = new URL(this.baseURL + path);
253
+ const headers = {
254
+ ...this.getAuthHeaders(path)
255
+ };
256
+ const controller = new AbortController();
257
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout * 5);
258
+ const fetchOptions = {
259
+ method: "GET",
260
+ headers,
261
+ signal: controller.signal
262
+ };
263
+ try {
264
+ const response = await this.fetch(url.toString(), fetchOptions);
265
+ if (!response.ok) {
266
+ clearTimeout(timeoutId);
267
+ const error = await response.json().catch(() => ({
268
+ error: response.statusText
269
+ }));
270
+ throw SDKError.fromResponse(response.status, error);
271
+ }
272
+ return response;
273
+ } catch (error) {
274
+ clearTimeout(timeoutId);
275
+ if (error instanceof SDKError) {
276
+ throw error;
277
+ }
278
+ throw error;
279
+ }
280
+ }
116
281
  getToken() {
117
282
  return this.getAuthToken();
118
283
  }
@@ -178,13 +343,11 @@ var AuthClient = class {
178
343
  }
179
344
  setApiKey(apiKey) {
180
345
  this.currentApiKey = apiKey;
181
- this.currentJwt = void 0;
182
346
  this.httpClient.setApiKey(apiKey);
183
347
  this.storage.set("apiKey", apiKey);
184
348
  }
185
349
  setJwt(jwt) {
186
350
  this.currentJwt = jwt;
187
- this.currentApiKey = void 0;
188
351
  this.httpClient.setJwt(jwt);
189
352
  this.storage.set("jwt", jwt);
190
353
  }
@@ -207,12 +370,50 @@ var AuthClient = class {
207
370
  this.setJwt(token);
208
371
  return token;
209
372
  }
373
+ /**
374
+ * Logout user and clear JWT, but preserve API key
375
+ * Use this for user logout in apps where API key is app-level credential
376
+ */
377
+ async logoutUser() {
378
+ if (this.currentJwt) {
379
+ try {
380
+ await this.httpClient.post("/v1/auth/logout", { all: true });
381
+ } catch (error) {
382
+ console.warn(
383
+ "Server-side logout failed, continuing with local cleanup:",
384
+ error
385
+ );
386
+ }
387
+ }
388
+ this.currentJwt = void 0;
389
+ this.httpClient.setJwt(void 0);
390
+ await this.storage.set("jwt", "");
391
+ if (!this.currentApiKey) {
392
+ const storedApiKey = await this.storage.get("apiKey");
393
+ if (storedApiKey) {
394
+ this.currentApiKey = storedApiKey;
395
+ }
396
+ }
397
+ if (this.currentApiKey) {
398
+ this.httpClient.setApiKey(this.currentApiKey);
399
+ console.log("[Auth] API key restored after user logout");
400
+ } else {
401
+ console.warn("[Auth] No API key available after logout");
402
+ }
403
+ }
404
+ /**
405
+ * Full logout - clears both JWT and API key
406
+ * Use this to completely reset authentication state
407
+ */
210
408
  async logout() {
211
409
  if (this.currentJwt) {
212
410
  try {
213
411
  await this.httpClient.post("/v1/auth/logout", { all: true });
214
412
  } catch (error) {
215
- console.warn("Server-side logout failed, continuing with local cleanup:", error);
413
+ console.warn(
414
+ "Server-side logout failed, continuing with local cleanup:",
415
+ error
416
+ );
216
417
  }
217
418
  }
218
419
  this.currentApiKey = void 0;
@@ -228,6 +429,51 @@ var AuthClient = class {
228
429
  this.httpClient.setJwt(void 0);
229
430
  await this.storage.clear();
230
431
  }
432
+ /**
433
+ * Request a challenge nonce for wallet authentication
434
+ */
435
+ async challenge(params) {
436
+ const response = await this.httpClient.post("/v1/auth/challenge", {
437
+ wallet: params.wallet,
438
+ purpose: params.purpose || "authentication",
439
+ namespace: params.namespace || "default"
440
+ });
441
+ return response;
442
+ }
443
+ /**
444
+ * Verify wallet signature and get JWT token
445
+ */
446
+ async verify(params) {
447
+ const response = await this.httpClient.post("/v1/auth/verify", {
448
+ wallet: params.wallet,
449
+ nonce: params.nonce,
450
+ signature: params.signature,
451
+ namespace: params.namespace || "default",
452
+ chain_type: params.chain_type || "ETH"
453
+ });
454
+ this.setJwt(response.access_token);
455
+ if (response.api_key) {
456
+ this.setApiKey(response.api_key);
457
+ }
458
+ if (response.refresh_token) {
459
+ await this.storage.set("refreshToken", response.refresh_token);
460
+ }
461
+ return response;
462
+ }
463
+ /**
464
+ * Get API key for wallet (creates namespace ownership)
465
+ */
466
+ async getApiKey(params) {
467
+ const response = await this.httpClient.post("/v1/auth/api-key", {
468
+ wallet: params.wallet,
469
+ nonce: params.nonce,
470
+ signature: params.signature,
471
+ namespace: params.namespace || "default",
472
+ chain_type: params.chain_type || "ETH"
473
+ });
474
+ this.setApiKey(response.api_key);
475
+ return response;
476
+ }
231
477
  };
232
478
 
233
479
  // src/db/qb.ts
@@ -397,7 +643,9 @@ var Repository = class {
397
643
  buildInsertSql(entity) {
398
644
  const columns = Object.keys(entity).filter((k) => entity[k] !== void 0);
399
645
  const placeholders = columns.map(() => "?").join(", ");
400
- return `INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
646
+ return `INSERT INTO ${this.tableName} (${columns.join(
647
+ ", "
648
+ )}) VALUES (${placeholders})`;
401
649
  }
402
650
  buildInsertArgs(entity) {
403
651
  return Object.entries(entity).filter(([, v]) => v !== void 0).map(([, v]) => v);
@@ -506,33 +754,35 @@ var DBClient = class {
506
754
  import WebSocket from "isomorphic-ws";
507
755
  var WSClient = class {
508
756
  constructor(config) {
509
- this.reconnectAttempts = 0;
510
757
  this.messageHandlers = /* @__PURE__ */ new Set();
511
758
  this.errorHandlers = /* @__PURE__ */ new Set();
512
759
  this.closeHandlers = /* @__PURE__ */ new Set();
513
- this.isManuallyClosed = false;
760
+ this.openHandlers = /* @__PURE__ */ new Set();
761
+ this.isClosed = false;
514
762
  this.url = config.wsURL;
515
763
  this.timeout = config.timeout ?? 3e4;
516
- this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5;
517
- this.reconnectDelayMs = config.reconnectDelayMs ?? 1e3;
518
- this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? 3e4;
519
- this.authMode = config.authMode ?? "header";
520
764
  this.authToken = config.authToken;
521
765
  this.WebSocketClass = config.WebSocket ?? WebSocket;
522
766
  }
767
+ /**
768
+ * Connect to WebSocket server
769
+ */
523
770
  connect() {
524
771
  return new Promise((resolve, reject) => {
525
772
  try {
526
773
  const wsUrl = this.buildWSUrl();
527
774
  this.ws = new this.WebSocketClass(wsUrl);
775
+ this.isClosed = false;
528
776
  const timeout = setTimeout(() => {
529
777
  this.ws?.close();
530
- reject(new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT"));
778
+ reject(
779
+ new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT")
780
+ );
531
781
  }, this.timeout);
532
782
  this.ws.addEventListener("open", () => {
533
783
  clearTimeout(timeout);
534
- this.reconnectAttempts = 0;
535
- this.startHeartbeat();
784
+ console.log("[WSClient] Connected to", this.url);
785
+ this.openHandlers.forEach((handler) => handler());
536
786
  resolve();
537
787
  });
538
788
  this.ws.addEventListener("message", (event) => {
@@ -540,29 +790,24 @@ var WSClient = class {
540
790
  this.messageHandlers.forEach((handler) => handler(msgEvent.data));
541
791
  });
542
792
  this.ws.addEventListener("error", (event) => {
793
+ console.error("[WSClient] WebSocket error:", event);
543
794
  clearTimeout(timeout);
544
- const error = new SDKError(
545
- "WebSocket error",
546
- 500,
547
- "WS_ERROR",
548
- event
549
- );
795
+ const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
550
796
  this.errorHandlers.forEach((handler) => handler(error));
551
797
  });
552
798
  this.ws.addEventListener("close", () => {
553
799
  clearTimeout(timeout);
554
- this.stopHeartbeat();
555
- if (!this.isManuallyClosed) {
556
- this.attemptReconnect();
557
- } else {
558
- this.closeHandlers.forEach((handler) => handler());
559
- }
800
+ console.log("[WSClient] Connection closed");
801
+ this.closeHandlers.forEach((handler) => handler());
560
802
  });
561
803
  } catch (error) {
562
804
  reject(error);
563
805
  }
564
806
  });
565
807
  }
808
+ /**
809
+ * Build WebSocket URL with auth token
810
+ */
566
811
  buildWSUrl() {
567
812
  let url = this.url;
568
813
  if (this.authToken) {
@@ -572,85 +817,152 @@ var WSClient = class {
572
817
  }
573
818
  return url;
574
819
  }
575
- startHeartbeat() {
576
- this.heartbeatInterval = setInterval(() => {
577
- if (this.ws?.readyState === WebSocket.OPEN) {
578
- this.ws.send(JSON.stringify({ type: "ping" }));
579
- }
580
- }, this.heartbeatIntervalMs);
581
- }
582
- stopHeartbeat() {
583
- if (this.heartbeatInterval) {
584
- clearInterval(this.heartbeatInterval);
585
- this.heartbeatInterval = void 0;
586
- }
587
- }
588
- attemptReconnect() {
589
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
590
- this.reconnectAttempts++;
591
- const delayMs = this.reconnectDelayMs * this.reconnectAttempts;
592
- setTimeout(() => {
593
- this.connect().catch((error) => {
594
- this.errorHandlers.forEach((handler) => handler(error));
595
- });
596
- }, delayMs);
597
- } else {
598
- this.closeHandlers.forEach((handler) => handler());
599
- }
600
- }
820
+ /**
821
+ * Register message handler
822
+ */
601
823
  onMessage(handler) {
602
824
  this.messageHandlers.add(handler);
603
825
  return () => this.messageHandlers.delete(handler);
604
826
  }
827
+ /**
828
+ * Unregister message handler
829
+ */
830
+ offMessage(handler) {
831
+ this.messageHandlers.delete(handler);
832
+ }
833
+ /**
834
+ * Register error handler
835
+ */
605
836
  onError(handler) {
606
837
  this.errorHandlers.add(handler);
607
838
  return () => this.errorHandlers.delete(handler);
608
839
  }
840
+ /**
841
+ * Unregister error handler
842
+ */
843
+ offError(handler) {
844
+ this.errorHandlers.delete(handler);
845
+ }
846
+ /**
847
+ * Register close handler
848
+ */
609
849
  onClose(handler) {
610
850
  this.closeHandlers.add(handler);
611
851
  return () => this.closeHandlers.delete(handler);
612
852
  }
853
+ /**
854
+ * Unregister close handler
855
+ */
856
+ offClose(handler) {
857
+ this.closeHandlers.delete(handler);
858
+ }
859
+ /**
860
+ * Register open handler
861
+ */
862
+ onOpen(handler) {
863
+ this.openHandlers.add(handler);
864
+ return () => this.openHandlers.delete(handler);
865
+ }
866
+ /**
867
+ * Send data through WebSocket
868
+ */
613
869
  send(data) {
614
870
  if (this.ws?.readyState !== WebSocket.OPEN) {
615
- throw new SDKError(
616
- "WebSocket is not connected",
617
- 500,
618
- "WS_NOT_CONNECTED"
619
- );
871
+ throw new SDKError("WebSocket is not connected", 500, "WS_NOT_CONNECTED");
620
872
  }
621
873
  this.ws.send(data);
622
874
  }
875
+ /**
876
+ * Close WebSocket connection
877
+ */
623
878
  close() {
624
- this.isManuallyClosed = true;
625
- this.stopHeartbeat();
879
+ if (this.isClosed) {
880
+ return;
881
+ }
882
+ this.isClosed = true;
626
883
  this.ws?.close();
627
884
  }
885
+ /**
886
+ * Check if WebSocket is connected
887
+ */
628
888
  isConnected() {
629
- return this.ws?.readyState === WebSocket.OPEN;
889
+ return !this.isClosed && this.ws?.readyState === WebSocket.OPEN;
630
890
  }
891
+ /**
892
+ * Update auth token
893
+ */
631
894
  setAuthToken(token) {
632
895
  this.authToken = token;
633
896
  }
634
897
  };
635
898
 
636
899
  // src/pubsub/client.ts
900
+ function base64Encode(str) {
901
+ if (typeof Buffer !== "undefined") {
902
+ return Buffer.from(str).toString("base64");
903
+ } else if (typeof btoa !== "undefined") {
904
+ return btoa(
905
+ encodeURIComponent(str).replace(
906
+ /%([0-9A-F]{2})/g,
907
+ (match, p1) => String.fromCharCode(parseInt(p1, 16))
908
+ )
909
+ );
910
+ }
911
+ throw new Error("No base64 encoding method available");
912
+ }
913
+ function base64EncodeBytes(bytes) {
914
+ if (typeof Buffer !== "undefined") {
915
+ return Buffer.from(bytes).toString("base64");
916
+ } else if (typeof btoa !== "undefined") {
917
+ let binary = "";
918
+ for (let i = 0; i < bytes.length; i++) {
919
+ binary += String.fromCharCode(bytes[i]);
920
+ }
921
+ return btoa(binary);
922
+ }
923
+ throw new Error("No base64 encoding method available");
924
+ }
925
+ function base64Decode(b64) {
926
+ if (typeof Buffer !== "undefined") {
927
+ return Buffer.from(b64, "base64").toString("utf-8");
928
+ } else if (typeof atob !== "undefined") {
929
+ const binary = atob(b64);
930
+ const bytes = new Uint8Array(binary.length);
931
+ for (let i = 0; i < binary.length; i++) {
932
+ bytes[i] = binary.charCodeAt(i);
933
+ }
934
+ return new TextDecoder().decode(bytes);
935
+ }
936
+ throw new Error("No base64 decoding method available");
937
+ }
637
938
  var PubSubClient = class {
638
939
  constructor(httpClient, wsConfig = {}) {
639
940
  this.httpClient = httpClient;
640
941
  this.wsConfig = wsConfig;
641
942
  }
642
943
  /**
643
- * Publish a message to a topic.
944
+ * Publish a message to a topic via HTTP
644
945
  */
645
946
  async publish(topic, data) {
646
- const dataBase64 = typeof data === "string" ? Buffer.from(data).toString("base64") : Buffer.from(data).toString("base64");
647
- await this.httpClient.post("/v1/pubsub/publish", {
648
- topic,
649
- data_base64: dataBase64
650
- });
947
+ let dataBase64;
948
+ if (typeof data === "string") {
949
+ dataBase64 = base64Encode(data);
950
+ } else {
951
+ dataBase64 = base64EncodeBytes(data);
952
+ }
953
+ await this.httpClient.post(
954
+ "/v1/pubsub/publish",
955
+ {
956
+ topic,
957
+ data_base64: dataBase64
958
+ },
959
+ {
960
+ timeout: 3e4
961
+ }
962
+ );
651
963
  }
652
964
  /**
653
- * List active topics in the current namespace.
965
+ * List active topics in the current namespace
654
966
  */
655
967
  async topics() {
656
968
  const response = await this.httpClient.get(
@@ -659,18 +971,20 @@ var PubSubClient = class {
659
971
  return response.topics || [];
660
972
  }
661
973
  /**
662
- * Subscribe to a topic via WebSocket.
663
- * Returns a subscription object with event handlers.
974
+ * Subscribe to a topic via WebSocket
975
+ * Creates one WebSocket connection per topic
664
976
  */
665
977
  async subscribe(topic, handlers = {}) {
666
- const wsUrl = new URL(this.wsConfig.wsURL || "ws://localhost:6001");
978
+ const wsUrl = new URL(this.wsConfig.wsURL || "ws://127.0.0.1:6001");
667
979
  wsUrl.pathname = "/v1/pubsub/ws";
668
980
  wsUrl.searchParams.set("topic", topic);
981
+ const authToken = this.httpClient.getApiKey() ?? this.httpClient.getToken();
669
982
  const wsClient = new WSClient({
670
983
  ...this.wsConfig,
671
984
  wsURL: wsUrl.toString(),
672
- authToken: this.httpClient.getToken()
985
+ authToken
673
986
  });
987
+ await wsClient.connect();
674
988
  const subscription = new Subscription(wsClient, topic);
675
989
  if (handlers.onMessage) {
676
990
  subscription.onMessage(handlers.onMessage);
@@ -681,7 +995,6 @@ var PubSubClient = class {
681
995
  if (handlers.onClose) {
682
996
  subscription.onClose(handlers.onClose);
683
997
  }
684
- await wsClient.connect();
685
998
  return subscription;
686
999
  }
687
1000
  };
@@ -690,46 +1003,105 @@ var Subscription = class {
690
1003
  this.messageHandlers = /* @__PURE__ */ new Set();
691
1004
  this.errorHandlers = /* @__PURE__ */ new Set();
692
1005
  this.closeHandlers = /* @__PURE__ */ new Set();
1006
+ this.isClosed = false;
1007
+ this.wsMessageHandler = null;
1008
+ this.wsErrorHandler = null;
1009
+ this.wsCloseHandler = null;
693
1010
  this.wsClient = wsClient;
694
1011
  this.topic = topic;
695
- this.wsClient.onMessage((data) => {
1012
+ this.wsMessageHandler = (data) => {
696
1013
  try {
1014
+ const envelope = JSON.parse(data);
1015
+ if (!envelope || typeof envelope !== "object") {
1016
+ throw new Error("Invalid envelope: not an object");
1017
+ }
1018
+ if (!envelope.data || typeof envelope.data !== "string") {
1019
+ throw new Error("Invalid envelope: missing or invalid data field");
1020
+ }
1021
+ if (!envelope.topic || typeof envelope.topic !== "string") {
1022
+ throw new Error("Invalid envelope: missing or invalid topic field");
1023
+ }
1024
+ if (typeof envelope.timestamp !== "number") {
1025
+ throw new Error(
1026
+ "Invalid envelope: missing or invalid timestamp field"
1027
+ );
1028
+ }
1029
+ const messageData = base64Decode(envelope.data);
697
1030
  const message = {
698
- topic: this.topic,
699
- data,
700
- timestamp: Date.now()
1031
+ topic: envelope.topic,
1032
+ data: messageData,
1033
+ timestamp: envelope.timestamp
701
1034
  };
1035
+ console.log("[Subscription] Received message on topic:", this.topic);
702
1036
  this.messageHandlers.forEach((handler) => handler(message));
703
1037
  } catch (error) {
1038
+ console.error("[Subscription] Error processing message:", error);
704
1039
  this.errorHandlers.forEach(
705
1040
  (handler) => handler(error instanceof Error ? error : new Error(String(error)))
706
1041
  );
707
1042
  }
708
- });
709
- this.wsClient.onError((error) => {
1043
+ };
1044
+ this.wsClient.onMessage(this.wsMessageHandler);
1045
+ this.wsErrorHandler = (error) => {
710
1046
  this.errorHandlers.forEach((handler) => handler(error));
711
- });
712
- this.wsClient.onClose(() => {
1047
+ };
1048
+ this.wsClient.onError(this.wsErrorHandler);
1049
+ this.wsCloseHandler = () => {
713
1050
  this.closeHandlers.forEach((handler) => handler());
714
- });
1051
+ };
1052
+ this.wsClient.onClose(this.wsCloseHandler);
715
1053
  }
1054
+ /**
1055
+ * Register message handler
1056
+ */
716
1057
  onMessage(handler) {
717
1058
  this.messageHandlers.add(handler);
718
1059
  return () => this.messageHandlers.delete(handler);
719
1060
  }
1061
+ /**
1062
+ * Register error handler
1063
+ */
720
1064
  onError(handler) {
721
1065
  this.errorHandlers.add(handler);
722
1066
  return () => this.errorHandlers.delete(handler);
723
1067
  }
1068
+ /**
1069
+ * Register close handler
1070
+ */
724
1071
  onClose(handler) {
725
1072
  this.closeHandlers.add(handler);
726
1073
  return () => this.closeHandlers.delete(handler);
727
1074
  }
1075
+ /**
1076
+ * Close subscription and underlying WebSocket
1077
+ */
728
1078
  close() {
1079
+ if (this.isClosed) {
1080
+ return;
1081
+ }
1082
+ this.isClosed = true;
1083
+ if (this.wsMessageHandler) {
1084
+ this.wsClient.offMessage(this.wsMessageHandler);
1085
+ this.wsMessageHandler = null;
1086
+ }
1087
+ if (this.wsErrorHandler) {
1088
+ this.wsClient.offError(this.wsErrorHandler);
1089
+ this.wsErrorHandler = null;
1090
+ }
1091
+ if (this.wsCloseHandler) {
1092
+ this.wsClient.offClose(this.wsCloseHandler);
1093
+ this.wsCloseHandler = null;
1094
+ }
1095
+ this.messageHandlers.clear();
1096
+ this.errorHandlers.clear();
1097
+ this.closeHandlers.clear();
729
1098
  this.wsClient.close();
730
1099
  }
1100
+ /**
1101
+ * Check if subscription is active
1102
+ */
731
1103
  isConnected() {
732
- return this.wsClient.isConnected();
1104
+ return !this.isClosed && this.wsClient.isConnected();
733
1105
  }
734
1106
  };
735
1107
 
@@ -753,7 +1125,9 @@ var NetworkClient = class {
753
1125
  * Get network status.
754
1126
  */
755
1127
  async status() {
756
- const response = await this.httpClient.get("/v1/status");
1128
+ const response = await this.httpClient.get(
1129
+ "/v1/network/status"
1130
+ );
757
1131
  return response;
758
1132
  }
759
1133
  /**
@@ -777,6 +1151,325 @@ var NetworkClient = class {
777
1151
  async disconnect(peerId) {
778
1152
  await this.httpClient.post("/v1/network/disconnect", { peer_id: peerId });
779
1153
  }
1154
+ /**
1155
+ * Proxy an HTTP request through the Anyone network.
1156
+ * Requires authentication (API key or JWT).
1157
+ *
1158
+ * @param request - The proxy request configuration
1159
+ * @returns The proxied response
1160
+ * @throws {SDKError} If the Anyone proxy is not available or the request fails
1161
+ *
1162
+ * @example
1163
+ * ```ts
1164
+ * const response = await client.network.proxyAnon({
1165
+ * url: 'https://api.example.com/data',
1166
+ * method: 'GET',
1167
+ * headers: {
1168
+ * 'Accept': 'application/json'
1169
+ * }
1170
+ * });
1171
+ *
1172
+ * console.log(response.status_code); // 200
1173
+ * console.log(response.body); // Response data
1174
+ * ```
1175
+ */
1176
+ async proxyAnon(request) {
1177
+ const response = await this.httpClient.post(
1178
+ "/v1/proxy/anon",
1179
+ request
1180
+ );
1181
+ if (response.error) {
1182
+ throw new Error(`Proxy request failed: ${response.error}`);
1183
+ }
1184
+ return response;
1185
+ }
1186
+ };
1187
+
1188
+ // src/cache/client.ts
1189
+ var CacheClient = class {
1190
+ constructor(httpClient) {
1191
+ this.httpClient = httpClient;
1192
+ }
1193
+ /**
1194
+ * Check cache service health
1195
+ */
1196
+ async health() {
1197
+ return this.httpClient.get("/v1/cache/health");
1198
+ }
1199
+ /**
1200
+ * Get a value from cache
1201
+ * Returns null if the key is not found (cache miss/expired), which is normal behavior
1202
+ */
1203
+ async get(dmap, key) {
1204
+ try {
1205
+ return await this.httpClient.post("/v1/cache/get", {
1206
+ dmap,
1207
+ key
1208
+ });
1209
+ } catch (error) {
1210
+ if (error instanceof SDKError && (error.httpStatus === 404 || error.httpStatus === 500 && error.message?.toLowerCase().includes("key not found"))) {
1211
+ return null;
1212
+ }
1213
+ throw error;
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Put a value into cache
1218
+ */
1219
+ async put(dmap, key, value, ttl) {
1220
+ return this.httpClient.post("/v1/cache/put", {
1221
+ dmap,
1222
+ key,
1223
+ value,
1224
+ ttl
1225
+ });
1226
+ }
1227
+ /**
1228
+ * Delete a value from cache
1229
+ */
1230
+ async delete(dmap, key) {
1231
+ return this.httpClient.post("/v1/cache/delete", {
1232
+ dmap,
1233
+ key
1234
+ });
1235
+ }
1236
+ /**
1237
+ * Get multiple values from cache in a single request
1238
+ * Returns a map of key -> value (or null if not found)
1239
+ * Gracefully handles 404 errors (endpoint not implemented) by returning empty results
1240
+ */
1241
+ async multiGet(dmap, keys) {
1242
+ try {
1243
+ if (keys.length === 0) {
1244
+ return /* @__PURE__ */ new Map();
1245
+ }
1246
+ const response = await this.httpClient.post(
1247
+ "/v1/cache/mget",
1248
+ {
1249
+ dmap,
1250
+ keys
1251
+ }
1252
+ );
1253
+ const resultMap = /* @__PURE__ */ new Map();
1254
+ keys.forEach((key) => {
1255
+ resultMap.set(key, null);
1256
+ });
1257
+ if (response.results) {
1258
+ response.results.forEach(({ key, value }) => {
1259
+ resultMap.set(key, value);
1260
+ });
1261
+ }
1262
+ return resultMap;
1263
+ } catch (error) {
1264
+ if (error instanceof SDKError && error.httpStatus === 404) {
1265
+ const resultMap2 = /* @__PURE__ */ new Map();
1266
+ keys.forEach((key) => {
1267
+ resultMap2.set(key, null);
1268
+ });
1269
+ return resultMap2;
1270
+ }
1271
+ const resultMap = /* @__PURE__ */ new Map();
1272
+ keys.forEach((key) => {
1273
+ resultMap.set(key, null);
1274
+ });
1275
+ console.error(`[CacheClient] Error in multiGet for ${dmap}:`, error);
1276
+ return resultMap;
1277
+ }
1278
+ }
1279
+ /**
1280
+ * Scan keys in a distributed map, optionally matching a regex pattern
1281
+ */
1282
+ async scan(dmap, match) {
1283
+ return this.httpClient.post("/v1/cache/scan", {
1284
+ dmap,
1285
+ match
1286
+ });
1287
+ }
1288
+ };
1289
+
1290
+ // src/storage/client.ts
1291
+ var StorageClient = class {
1292
+ constructor(httpClient) {
1293
+ this.httpClient = httpClient;
1294
+ }
1295
+ /**
1296
+ * Upload content to IPFS and optionally pin it.
1297
+ * Supports both File objects (browser) and Buffer/ReadableStream (Node.js).
1298
+ *
1299
+ * @param file - File to upload (File, Blob, or Buffer)
1300
+ * @param name - Optional filename
1301
+ * @param options - Optional upload options
1302
+ * @param options.pin - Whether to pin the content (default: true). Pinning happens asynchronously on the backend.
1303
+ * @returns Upload result with CID
1304
+ *
1305
+ * @example
1306
+ * ```ts
1307
+ * // Browser
1308
+ * const fileInput = document.querySelector('input[type="file"]');
1309
+ * const file = fileInput.files[0];
1310
+ * const result = await client.storage.upload(file, file.name);
1311
+ * console.log(result.cid);
1312
+ *
1313
+ * // Node.js
1314
+ * const fs = require('fs');
1315
+ * const fileBuffer = fs.readFileSync('image.jpg');
1316
+ * const result = await client.storage.upload(fileBuffer, 'image.jpg', { pin: true });
1317
+ * ```
1318
+ */
1319
+ async upload(file, name, options) {
1320
+ const formData = new FormData();
1321
+ if (file instanceof File) {
1322
+ formData.append("file", file);
1323
+ } else if (file instanceof Blob) {
1324
+ formData.append("file", file, name);
1325
+ } else if (file instanceof ArrayBuffer) {
1326
+ const blob = new Blob([file]);
1327
+ formData.append("file", blob, name);
1328
+ } else if (file instanceof Uint8Array) {
1329
+ const buffer = file.buffer.slice(
1330
+ file.byteOffset,
1331
+ file.byteOffset + file.byteLength
1332
+ );
1333
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
1334
+ formData.append("file", blob, name);
1335
+ } else if (file instanceof ReadableStream) {
1336
+ const chunks = [];
1337
+ const reader = file.getReader();
1338
+ while (true) {
1339
+ const { done, value } = await reader.read();
1340
+ if (done) break;
1341
+ const buffer = value.buffer.slice(
1342
+ value.byteOffset,
1343
+ value.byteOffset + value.byteLength
1344
+ );
1345
+ chunks.push(buffer);
1346
+ }
1347
+ const blob = new Blob(chunks);
1348
+ formData.append("file", blob, name);
1349
+ } else {
1350
+ throw new Error(
1351
+ "Unsupported file type. Use File, Blob, ArrayBuffer, Uint8Array, or ReadableStream."
1352
+ );
1353
+ }
1354
+ const shouldPin = options?.pin !== false;
1355
+ formData.append("pin", shouldPin ? "true" : "false");
1356
+ return this.httpClient.uploadFile(
1357
+ "/v1/storage/upload",
1358
+ formData,
1359
+ { timeout: 3e5 }
1360
+ // 5 minute timeout for large files
1361
+ );
1362
+ }
1363
+ /**
1364
+ * Pin an existing CID
1365
+ *
1366
+ * @param cid - Content ID to pin
1367
+ * @param name - Optional name for the pin
1368
+ * @returns Pin result
1369
+ */
1370
+ async pin(cid, name) {
1371
+ return this.httpClient.post("/v1/storage/pin", {
1372
+ cid,
1373
+ name
1374
+ });
1375
+ }
1376
+ /**
1377
+ * Get the pin status for a CID
1378
+ *
1379
+ * @param cid - Content ID to check
1380
+ * @returns Pin status information
1381
+ */
1382
+ async status(cid) {
1383
+ return this.httpClient.get(`/v1/storage/status/${cid}`);
1384
+ }
1385
+ /**
1386
+ * Retrieve content from IPFS by CID
1387
+ *
1388
+ * @param cid - Content ID to retrieve
1389
+ * @returns ReadableStream of the content
1390
+ *
1391
+ * @example
1392
+ * ```ts
1393
+ * const stream = await client.storage.get(cid);
1394
+ * const reader = stream.getReader();
1395
+ * while (true) {
1396
+ * const { done, value } = await reader.read();
1397
+ * if (done) break;
1398
+ * // Process chunk
1399
+ * }
1400
+ * ```
1401
+ */
1402
+ async get(cid) {
1403
+ const maxAttempts = 8;
1404
+ let lastError = null;
1405
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1406
+ try {
1407
+ const response = await this.httpClient.getBinary(
1408
+ `/v1/storage/get/${cid}`
1409
+ );
1410
+ if (!response.body) {
1411
+ throw new Error("Response body is null");
1412
+ }
1413
+ return response.body;
1414
+ } catch (error) {
1415
+ lastError = error;
1416
+ const isNotFound = error?.httpStatus === 404 || error?.message?.includes("not found") || error?.message?.includes("404");
1417
+ if (!isNotFound || attempt === maxAttempts) {
1418
+ throw error;
1419
+ }
1420
+ const backoffMs = attempt * 2500;
1421
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
1422
+ }
1423
+ }
1424
+ throw lastError || new Error("Failed to retrieve content");
1425
+ }
1426
+ /**
1427
+ * Retrieve content from IPFS by CID and return the full Response object
1428
+ * Useful when you need access to response headers (e.g., content-length)
1429
+ *
1430
+ * @param cid - Content ID to retrieve
1431
+ * @returns Response object with body stream and headers
1432
+ *
1433
+ * @example
1434
+ * ```ts
1435
+ * const response = await client.storage.getBinary(cid);
1436
+ * const contentLength = response.headers.get('content-length');
1437
+ * const reader = response.body.getReader();
1438
+ * // ... read stream
1439
+ * ```
1440
+ */
1441
+ async getBinary(cid) {
1442
+ const maxAttempts = 8;
1443
+ let lastError = null;
1444
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1445
+ try {
1446
+ const response = await this.httpClient.getBinary(
1447
+ `/v1/storage/get/${cid}`
1448
+ );
1449
+ if (!response) {
1450
+ throw new Error("Response is null");
1451
+ }
1452
+ return response;
1453
+ } catch (error) {
1454
+ lastError = error;
1455
+ const isNotFound = error?.httpStatus === 404 || error?.message?.includes("not found") || error?.message?.includes("404");
1456
+ if (!isNotFound || attempt === maxAttempts) {
1457
+ throw error;
1458
+ }
1459
+ const backoffMs = attempt * 2500;
1460
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
1461
+ }
1462
+ }
1463
+ throw lastError || new Error("Failed to retrieve content");
1464
+ }
1465
+ /**
1466
+ * Unpin a CID
1467
+ *
1468
+ * @param cid - Content ID to unpin
1469
+ */
1470
+ async unpin(cid) {
1471
+ await this.httpClient.delete(`/v1/storage/unpin/${cid}`);
1472
+ }
780
1473
  };
781
1474
 
782
1475
  // src/index.ts
@@ -801,15 +1494,20 @@ function createClient(config) {
801
1494
  wsURL
802
1495
  });
803
1496
  const network = new NetworkClient(httpClient);
1497
+ const cache = new CacheClient(httpClient);
1498
+ const storage = new StorageClient(httpClient);
804
1499
  return {
805
1500
  auth,
806
1501
  db,
807
1502
  pubsub,
808
- network
1503
+ network,
1504
+ cache,
1505
+ storage
809
1506
  };
810
1507
  }
811
1508
  export {
812
1509
  AuthClient,
1510
+ CacheClient,
813
1511
  DBClient,
814
1512
  HttpClient,
815
1513
  LocalStorageAdapter,
@@ -819,6 +1517,7 @@ export {
819
1517
  QueryBuilder,
820
1518
  Repository,
821
1519
  SDKError,
1520
+ StorageClient,
822
1521
  Subscription,
823
1522
  WSClient,
824
1523
  createClient