@cmdoss/memwal-sdk 0.9.0 → 1.0.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.
Files changed (174) hide show
  1. package/README.md +157 -52
  2. package/dist/client/ClientMemoryManager.d.ts.map +1 -1
  3. package/dist/client/ClientMemoryManager.js +25 -8
  4. package/dist/client/ClientMemoryManager.js.map +1 -1
  5. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  6. package/dist/client/SimplePDWClient.d.ts +2 -1
  7. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  8. package/dist/client/SimplePDWClient.js +23 -6
  9. package/dist/client/SimplePDWClient.js.map +1 -1
  10. package/dist/client/namespaces/MemoryNamespace.d.ts +6 -0
  11. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  12. package/dist/client/namespaces/MemoryNamespace.js +131 -18
  13. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  14. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +3 -1
  15. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  16. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  17. package/dist/config/ConfigurationHelper.js +61 -61
  18. package/dist/config/index.d.ts +1 -0
  19. package/dist/config/index.d.ts.map +1 -1
  20. package/dist/config/index.js +2 -0
  21. package/dist/config/index.js.map +1 -1
  22. package/dist/config/modelDefaults.d.ts +67 -0
  23. package/dist/config/modelDefaults.d.ts.map +1 -0
  24. package/dist/config/modelDefaults.js +91 -0
  25. package/dist/config/modelDefaults.js.map +1 -0
  26. package/dist/graph/GraphService.d.ts.map +1 -1
  27. package/dist/graph/GraphService.js +22 -21
  28. package/dist/graph/GraphService.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/langchain/createPDWRAG.js +30 -30
  34. package/dist/pipeline/MemoryPipeline.d.ts.map +1 -1
  35. package/dist/pipeline/MemoryPipeline.js +2 -1
  36. package/dist/pipeline/MemoryPipeline.js.map +1 -1
  37. package/dist/services/GeminiAIService.d.ts.map +1 -1
  38. package/dist/services/GeminiAIService.js +311 -310
  39. package/dist/services/GeminiAIService.js.map +1 -1
  40. package/dist/services/StorageService.d.ts +4 -1
  41. package/dist/services/StorageService.d.ts.map +1 -1
  42. package/dist/services/StorageService.js.map +1 -1
  43. package/dist/services/storage/QuiltBatchManager.d.ts +7 -0
  44. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  45. package/dist/services/storage/QuiltBatchManager.js +24 -5
  46. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  47. package/dist/services/storage/WalrusStorageManager.d.ts +10 -1
  48. package/dist/services/storage/WalrusStorageManager.d.ts.map +1 -1
  49. package/dist/services/storage/WalrusStorageManager.js +53 -12
  50. package/dist/services/storage/WalrusStorageManager.js.map +1 -1
  51. package/dist/vector/BrowserHnswIndexService.js +2 -2
  52. package/dist/vector/BrowserHnswIndexService.js.map +1 -1
  53. package/dist/vector/NodeHnswService.js +4 -4
  54. package/dist/vector/NodeHnswService.js.map +1 -1
  55. package/dist/vector/createHnswService.d.ts +4 -0
  56. package/dist/vector/createHnswService.d.ts.map +1 -1
  57. package/dist/vector/createHnswService.js +15 -3
  58. package/dist/vector/createHnswService.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/access/PermissionService.ts +635 -635
  61. package/src/aggregation/AggregationService.ts +389 -389
  62. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  63. package/src/ai-sdk/index.ts +65 -65
  64. package/src/ai-sdk/tools.ts +460 -460
  65. package/src/ai-sdk/types.ts +404 -404
  66. package/src/batch/BatchManager.ts +597 -597
  67. package/src/batch/BatchingService.ts +429 -429
  68. package/src/batch/MemoryProcessingCache.ts +492 -492
  69. package/src/batch/index.ts +30 -30
  70. package/src/browser.ts +200 -200
  71. package/src/client/ClientMemoryManager.ts +1004 -987
  72. package/src/client/PersonalDataWallet.ts +345 -345
  73. package/src/client/SimplePDWClient.ts +1387 -1369
  74. package/src/client/factory.ts +154 -154
  75. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  76. package/src/client/namespaces/BatchNamespace.ts +356 -356
  77. package/src/client/namespaces/CacheNamespace.ts +123 -123
  78. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  79. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  80. package/src/client/namespaces/ContextNamespace.ts +297 -297
  81. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  82. package/src/client/namespaces/GraphNamespace.ts +468 -468
  83. package/src/client/namespaces/IndexNamespace.ts +364 -364
  84. package/src/client/namespaces/MemoryNamespace.ts +1704 -1569
  85. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  86. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  87. package/src/client/namespaces/StorageNamespace.ts +458 -458
  88. package/src/client/namespaces/TxNamespace.ts +260 -260
  89. package/src/client/namespaces/WalletNamespace.ts +243 -243
  90. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -607
  91. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  92. package/src/client/namespaces/consolidated/StorageNamespace.ts +1143 -1141
  93. package/src/client/namespaces/consolidated/index.ts +41 -41
  94. package/src/client/signers/KeypairSigner.ts +108 -108
  95. package/src/client/signers/UnifiedSigner.ts +110 -110
  96. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  97. package/src/client/signers/index.ts +26 -26
  98. package/src/config/ConfigurationHelper.ts +412 -412
  99. package/src/config/defaults.ts +56 -56
  100. package/src/config/index.ts +16 -9
  101. package/src/config/modelDefaults.ts +103 -0
  102. package/src/config/validation.ts +70 -70
  103. package/src/core/index.ts +14 -14
  104. package/src/core/interfaces/IService.ts +307 -307
  105. package/src/core/interfaces/index.ts +8 -8
  106. package/src/core/types/capability.ts +297 -297
  107. package/src/core/types/index.ts +874 -874
  108. package/src/core/types/wallet.ts +270 -270
  109. package/src/core/types.ts +9 -9
  110. package/src/core/wallet.ts +222 -222
  111. package/src/embedding/index.ts +19 -19
  112. package/src/embedding/types.ts +357 -357
  113. package/src/errors/index.ts +602 -602
  114. package/src/errors/recovery.ts +461 -461
  115. package/src/errors/validation.ts +567 -567
  116. package/src/generated/pdw/capability.ts +319 -319
  117. package/src/graph/GraphService.ts +888 -887
  118. package/src/graph/KnowledgeGraphManager.ts +728 -728
  119. package/src/graph/index.ts +25 -25
  120. package/src/index.ts +498 -498
  121. package/src/infrastructure/index.ts +22 -22
  122. package/src/infrastructure/seal/EncryptionService.ts +628 -628
  123. package/src/infrastructure/seal/SealService.ts +613 -613
  124. package/src/infrastructure/seal/index.ts +9 -9
  125. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  126. package/src/infrastructure/sui/SuiService.ts +888 -888
  127. package/src/infrastructure/sui/index.ts +9 -9
  128. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  129. package/src/infrastructure/walrus/WalrusStorageService.ts +637 -637
  130. package/src/infrastructure/walrus/index.ts +9 -9
  131. package/src/langchain/createPDWRAG.ts +303 -303
  132. package/src/langchain/index.ts +47 -47
  133. package/src/permissions/ConsentRepository.browser.ts +249 -249
  134. package/src/permissions/ConsentRepository.ts +364 -364
  135. package/src/pipeline/MemoryPipeline.ts +863 -862
  136. package/src/pipeline/PipelineManager.ts +683 -683
  137. package/src/pipeline/index.ts +26 -26
  138. package/src/retrieval/AdvancedSearchService.ts +629 -629
  139. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  140. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -825
  141. package/src/retrieval/index.ts +42 -42
  142. package/src/services/BatchService.ts +352 -352
  143. package/src/services/CapabilityService.ts +464 -464
  144. package/src/services/ClassifierService.ts +465 -465
  145. package/src/services/CrossContextPermissionService.ts +486 -486
  146. package/src/services/EmbeddingService.ts +796 -796
  147. package/src/services/EncryptionService.ts +712 -712
  148. package/src/services/GeminiAIService.ts +754 -753
  149. package/src/services/MemoryIndexService.ts +1009 -1009
  150. package/src/services/MemoryService.ts +369 -369
  151. package/src/services/QueryService.ts +890 -890
  152. package/src/services/StorageService.ts +1185 -1182
  153. package/src/services/TransactionService.ts +838 -838
  154. package/src/services/VectorService.ts +462 -462
  155. package/src/services/ViewService.ts +484 -484
  156. package/src/services/index.ts +25 -25
  157. package/src/services/storage/BlobAttributesManager.ts +333 -333
  158. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  159. package/src/services/storage/MemorySearchManager.ts +387 -387
  160. package/src/services/storage/QuiltBatchManager.ts +1157 -1130
  161. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  162. package/src/services/storage/WalrusStorageManager.ts +333 -287
  163. package/src/services/storage/index.ts +57 -57
  164. package/src/types/index.ts +13 -13
  165. package/src/utils/index.ts +76 -76
  166. package/src/utils/memoryIndexOnChain.ts +507 -507
  167. package/src/vector/BrowserHnswIndexService.ts +758 -758
  168. package/src/vector/HnswWasmService.ts +731 -731
  169. package/src/vector/IHnswService.ts +233 -233
  170. package/src/vector/NodeHnswService.ts +833 -833
  171. package/src/vector/createHnswService.ts +147 -135
  172. package/src/vector/index.ts +56 -56
  173. package/src/wallet/ContextWalletService.ts +656 -656
  174. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,638 +1,638 @@
1
- /**
2
- * WalrusStorageService - Production Decentralized Storage
3
- *
4
- * Walrus client integration with SEAL encryption,
5
- * content verification, and standardized tagging per https://docs.wal.app/
6
- *
7
- * Uses @mysten/walrus SDK for writeBlob/readBlob operations when signer is available.
8
- * Falls back to REST API for read-only operations without signer.
9
- */
10
-
11
- import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
12
- import { walrus } from '@mysten/walrus';
13
- import type { Signer } from '@mysten/sui/cryptography';
14
- import type { SealService } from '../seal/SealService';
15
-
16
- export interface WalrusConfig {
17
- network?: 'testnet' | 'mainnet';
18
- adminAddress?: string;
19
- storageEpochs?: number;
20
- /** Publisher URL for direct blob uploads (server-side) */
21
- publisherHost?: string;
22
- /** Upload Relay URL for browser/mobile uploads (fewer connections, faster) */
23
- uploadRelayHost?: string;
24
- /** Aggregator URL for blob retrieval */
25
- aggregatorHost?: string;
26
- /** Use upload relay instead of publisher (default: true for browser, false for server) */
27
- useUploadRelay?: boolean;
28
- retryAttempts?: number;
29
- timeoutMs?: number;
30
- sealService?: SealService;
31
- signer?: Signer; // Required for writeBlob operations
32
- }
33
-
34
- export interface MemoryMetadata {
35
- contentType: string;
36
- contentSize: number;
37
- contentHash: string;
38
- category: string;
39
- topic: string;
40
- importance: number; // 1-10 scale
41
- embeddingBlobId?: string;
42
- embeddingDimension: number;
43
- createdTimestamp: number;
44
- updatedTimestamp?: number;
45
- customMetadata?: Record<string, string>;
46
- isEncrypted?: boolean;
47
- encryptionType?: string;
48
- }
49
-
50
- export interface WalrusUploadResult {
51
- blobId: string;
52
- metadata: MemoryMetadata;
53
- embeddingBlobId?: string;
54
- isEncrypted: boolean;
55
- backupKey?: string;
56
- storageEpochs: number;
57
- uploadTimeMs: number;
58
- }
59
-
60
- export interface WalrusRetrievalResult {
61
- content: string;
62
- metadata: MemoryMetadata;
63
- isDecrypted: boolean;
64
- retrievalTimeMs: number;
65
- }
66
-
67
- export interface BlobInfo {
68
- blobId: string;
69
- contentType: string;
70
- contentLength: number;
71
- contentHash: string;
72
- metadata: Record<string, string>;
73
- tags: string[];
74
- }
75
-
76
- class WalrusError extends Error {
77
- public readonly cause?: unknown;
78
-
79
- constructor(message: string, cause?: unknown) {
80
- super(message);
81
- this.name = 'WalrusError';
82
- this.cause = cause;
83
- }
84
- }
85
-
86
- export interface WalrusStats {
87
- totalUploads: number;
88
- totalRetrievals: number;
89
- successfulUploads: number;
90
- failedUploads: number;
91
- cacheHitRate: number;
92
- averageUploadTime: number;
93
- averageRetrievalTime: number;
94
- localFallbackCount: number;
95
- totalStorageUsed: number;
96
- }
97
-
98
- interface CachedBlob {
99
- content: Uint8Array;
100
- contentType: string;
101
- timestamp: Date;
102
- metadata: Record<string, string>;
103
- }
104
-
105
- /**
106
- * Production-ready Walrus storage service using @mysten/walrus SDK
107
- */
108
- export class WalrusStorageService {
109
- private readonly config: Omit<Required<WalrusConfig>, 'sealService' | 'signer'> & {
110
- sealService?: SealService;
111
- signer?: Signer;
112
- };
113
- private readonly cache = new Map<string, CachedBlob>();
114
- private stats: WalrusStats = {
115
- totalUploads: 0,
116
- totalRetrievals: 0,
117
- successfulUploads: 0,
118
- failedUploads: 0,
119
- cacheHitRate: 0,
120
- averageUploadTime: 0,
121
- averageRetrievalTime: 0,
122
- localFallbackCount: 0,
123
- totalStorageUsed: 0
124
- };
125
-
126
- private readonly CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
127
- private sealService?: SealService;
128
- private walrusClient: ReturnType<typeof this.createWalrusClient> | null = null;
129
-
130
- constructor(config: Partial<WalrusConfig> = {}) {
131
- const network = config.network || 'testnet';
132
- // Detect browser environment for default useUploadRelay
133
- const isBrowser = typeof window !== 'undefined';
134
-
135
- this.config = {
136
- network,
137
- adminAddress: config.adminAddress || '',
138
- storageEpochs: config.storageEpochs || 12,
139
- // Publisher: direct uploads (server-side)
140
- publisherHost: config.publisherHost || 'https://publisher.walrus-testnet.walrus.space',
141
- // Upload Relay: optimized for browser/mobile (fewer connections)
142
- uploadRelayHost: config.uploadRelayHost || 'https://upload-relay.testnet.walrus.space',
143
- // Aggregator: blob retrieval
144
- aggregatorHost: config.aggregatorHost || 'https://aggregator.walrus-testnet.walrus.space',
145
- // Use upload relay by default in browser, publisher on server
146
- useUploadRelay: config.useUploadRelay ?? isBrowser,
147
- retryAttempts: config.retryAttempts || 3,
148
- timeoutMs: config.timeoutMs || 60000,
149
- sealService: config.sealService,
150
- signer: config.signer
151
- };
152
-
153
- this.sealService = config.sealService;
154
-
155
- // Initialize Walrus SDK client
156
- this.walrusClient = this.createWalrusClient(network);
157
- }
158
-
159
- /**
160
- * Create Walrus client using @mysten/walrus SDK
161
- * Uses $extend to add walrus capabilities to SuiClient
162
- */
163
- private createWalrusClient(network: 'testnet' | 'mainnet') {
164
- const suiClient = new SuiClient({
165
- url: getFullnodeUrl(network)
166
- });
167
-
168
- // Extend SuiClient with Walrus capabilities using $extend
169
- return suiClient.$extend(walrus({
170
- network,
171
- }));
172
- }
173
-
174
- // ==================== PUBLIC API ====================
175
-
176
- /**
177
- * Store memory with encryption and metadata
178
- */
179
- async storeMemory(
180
- content: string,
181
- category: string,
182
- options: {
183
- topic?: string;
184
- importance?: number;
185
- customMetadata?: Record<string, string>;
186
- contextId?: string;
187
- appId?: string;
188
- encrypt?: boolean;
189
- userAddress?: string;
190
- } = {}
191
- ): Promise<WalrusUploadResult> {
192
- const startTime = Date.now();
193
- this.stats.totalUploads++;
194
-
195
- try {
196
- const {
197
- topic = `Memory about ${category}`,
198
- importance = 5,
199
- customMetadata = {},
200
- contextId,
201
- appId,
202
- encrypt = false,
203
- userAddress
204
- } = options;
205
-
206
- let processedContent = content;
207
- let backupKey: string | undefined;
208
- let isEncrypted = false;
209
-
210
- // Use SEAL encryption if requested and available
211
- if (encrypt && this.sealService && userAddress) {
212
- const sessionConfig = {
213
- address: userAddress,
214
- packageId: this.config.adminAddress, // Use configured package ID
215
- ttlMin: 60 // 1 hour session
216
- };
217
- const sessionKey = await this.sealService.createSession(sessionConfig);
218
- const encryptResult = await this.sealService.encryptData({
219
- data: new TextEncoder().encode(content),
220
- id: userAddress,
221
- threshold: 2 // Default threshold
222
- });
223
- processedContent = JSON.stringify(encryptResult);
224
- backupKey = 'session-key-reference'; // Store session reference
225
- isEncrypted = true;
226
- }
227
-
228
- const metadata = this.createMetadataWithEmbedding(
229
- processedContent,
230
- category,
231
- topic,
232
- importance,
233
- {
234
- ...customMetadata,
235
- ...(contextId && { 'context-id': contextId }),
236
- ...(appId && { 'app-id': appId }),
237
- ...(userAddress && { owner: userAddress }),
238
- encrypted: isEncrypted.toString(),
239
- ...(isEncrypted && { 'encryption-type': 'seal' })
240
- }
241
- );
242
-
243
- // Upload to Walrus with official client
244
- const blobId = await this.uploadToWalrus(processedContent, metadata);
245
-
246
- // Use Walrus blob_id as content hash (already content-addressed via blake2b256)
247
- metadata.contentHash = blobId;
248
-
249
- const uploadTimeMs = Date.now() - startTime;
250
- this.stats.successfulUploads++;
251
- this.updateAverageUploadTime(uploadTimeMs);
252
-
253
- return {
254
- blobId,
255
- metadata,
256
- isEncrypted,
257
- backupKey,
258
- storageEpochs: this.config.storageEpochs,
259
- uploadTimeMs
260
- };
261
-
262
- } catch (error) {
263
- this.stats.failedUploads++;
264
- throw new WalrusError(
265
- `Failed to store memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
266
- error
267
- );
268
- }
269
- }
270
-
271
- /**
272
- * Retrieve memory with decryption
273
- */
274
- async retrieveMemory(
275
- blobId: string,
276
- options: {
277
- userAddress?: string;
278
- sessionKey?: any; // SEAL session key
279
- txBytes?: Uint8Array;
280
- } = {}
281
- ): Promise<WalrusRetrievalResult> {
282
- const startTime = Date.now();
283
- this.stats.totalRetrievals++;
284
-
285
- try {
286
- // Check cache first
287
- const cached = this.cache.get(blobId);
288
- if (cached && this.isCacheValid(cached.timestamp)) {
289
- this.stats.cacheHitRate = (this.stats.cacheHitRate * (this.stats.totalRetrievals - 1) + 1) / this.stats.totalRetrievals;
290
-
291
- const content = new TextDecoder().decode(cached.content);
292
- return {
293
- content,
294
- metadata: cached.metadata as unknown as MemoryMetadata,
295
- isDecrypted: false,
296
- retrievalTimeMs: Date.now() - startTime
297
- };
298
- }
299
-
300
- // Retrieve from Walrus
301
- const { content, metadata } = await this.retrieveFromWalrus(blobId);
302
-
303
- let processedContent = content;
304
- let isDecrypted = false;
305
-
306
- // Decrypt if needed
307
- if (metadata.isEncrypted && metadata.encryptionType === 'seal' && this.sealService) {
308
- const { userAddress, sessionKey, txBytes } = options;
309
- if (userAddress && sessionKey && txBytes) {
310
- try {
311
- const encryptedData = JSON.parse(content);
312
- const decryptedBytes = await this.sealService.decryptData({
313
- encryptedObject: new Uint8Array(encryptedData),
314
- sessionKey,
315
- txBytes
316
- });
317
- processedContent = new TextDecoder().decode(decryptedBytes);
318
- isDecrypted = true;
319
- } catch (decryptError) {
320
- console.warn('SEAL decryption failed, returning encrypted content:', decryptError);
321
- }
322
- }
323
- }
324
-
325
- const retrievalTimeMs = Date.now() - startTime;
326
- this.updateAverageRetrievalTime(retrievalTimeMs);
327
-
328
- return {
329
- content: processedContent,
330
- metadata,
331
- isDecrypted,
332
- retrievalTimeMs
333
- };
334
-
335
- } catch (error) {
336
- throw new WalrusError(
337
- `Failed to retrieve memory ${blobId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
338
- error
339
- );
340
- }
341
- }
342
-
343
- /**
344
- * Get service statistics
345
- */
346
- getStats(): WalrusStats {
347
- return { ...this.stats };
348
- }
349
-
350
- /**
351
- * Clear cache
352
- */
353
- clearCache(): void {
354
- this.cache.clear();
355
- }
356
-
357
- /**
358
- * Retrieve content by blobId with optional decryption
359
- */
360
- async retrieveContent(blobId: string, decryptionKey?: string | Uint8Array): Promise<{
361
- content: string;
362
- metadata: MemoryMetadata;
363
- retrievalTimeMs: number;
364
- isFromCache: boolean;
365
- }> {
366
- const startTime = Date.now();
367
-
368
- try {
369
- const result = await this.retrieveFromWalrus(blobId);
370
-
371
- return {
372
- content: result.content,
373
- metadata: result.metadata,
374
- retrievalTimeMs: Date.now() - startTime,
375
- isFromCache: false // TODO: implement cache checking
376
- };
377
- } catch (error) {
378
- throw new Error(`Failed to retrieve content: ${error}`);
379
- }
380
- }
381
-
382
- /**
383
- * List blobs for a specific user
384
- */
385
- async listUserBlobs(userId: string, options: {
386
- category?: string;
387
- limit?: number;
388
- offset?: number;
389
- sortBy?: 'date' | 'size' | 'importance';
390
- filters?: Record<string, any>;
391
- } = {}): Promise<{ blobs: BlobInfo[]; totalCount: number }> {
392
- // TODO: Implement actual Walrus listing when API is available
393
- // For now, return empty result as this would require backend indexing
394
- console.warn('listUserBlobs not yet implemented - requires Walrus indexing service');
395
- return {
396
- blobs: [],
397
- totalCount: 0
398
- };
399
- }
400
-
401
- /**
402
- * Delete a blob by ID
403
- */
404
- async deleteBlob(blobId: string): Promise<boolean> {
405
- // TODO: Implement actual Walrus deletion when API is available
406
- // Walrus typically doesn't support deletion, but this could mark as deleted
407
- console.warn('deleteBlob not yet implemented - Walrus typically immutable');
408
- return false;
409
- }
410
-
411
- /**
412
- * Check Walrus service availability
413
- */
414
- async checkWalrusAvailability(): Promise<boolean> {
415
- // TODO: Implement proper availability check with official client
416
- console.warn('checkWalrusAvailability not yet implemented - assuming available for testing');
417
- return true; // Return true to allow tests to run, even though implementation is placeholder
418
- }
419
-
420
- /**
421
- * Get cache information
422
- */
423
- getCacheInfo(): {
424
- size: number;
425
- maxSize: number;
426
- hitRate: number;
427
- entries: number;
428
- } {
429
- return {
430
- size: this.cache.size, // Approximate
431
- maxSize: 100, // Default cache size
432
- hitRate: this.stats.cacheHitRate,
433
- entries: this.cache.size
434
- };
435
- }
436
-
437
- /**
438
- * Upload content with metadata
439
- */
440
- async uploadContentWithMetadata(
441
- content: string,
442
- userId: string,
443
- options: {
444
- category?: string;
445
- topic?: string;
446
- importance?: number;
447
- additionalTags?: Record<string, string>;
448
- enableEncryption?: boolean;
449
- }
450
- ): Promise<{
451
- blobId: string;
452
- metadata: MemoryMetadata;
453
- uploadTimeMs: number;
454
- isEncrypted: boolean;
455
- }> {
456
- const startTime = Date.now();
457
-
458
- try {
459
- // Upload to Walrus first to get blob_id (which serves as content hash)
460
- const tempMetadata: MemoryMetadata = {
461
- contentType: 'application/json',
462
- contentSize: content.length,
463
- contentHash: '', // Will be set to blobId below
464
- category: options.category || 'general',
465
- topic: options.topic || 'misc',
466
- importance: options.importance || 5,
467
- embeddingDimension: 0,
468
- createdTimestamp: Date.now(),
469
- updatedTimestamp: Date.now(),
470
- customMetadata: options.additionalTags || {},
471
- isEncrypted: options.enableEncryption || false,
472
- encryptionType: options.enableEncryption ? 'seal' : undefined
473
- };
474
-
475
- const blobId = await this.uploadToWalrus(content, tempMetadata);
476
-
477
- // Use Walrus blob_id as content hash (already content-addressed via blake2b256)
478
- const memoryMetadata: MemoryMetadata = {
479
- ...tempMetadata,
480
- contentHash: blobId, // Walrus blob_id serves as content hash
481
- };
482
-
483
- return {
484
- blobId,
485
- metadata: memoryMetadata,
486
- uploadTimeMs: Date.now() - startTime,
487
- isEncrypted: memoryMetadata.isEncrypted || false
488
- };
489
- } catch (error) {
490
- throw new Error(`Failed to upload content: ${error}`);
491
- }
492
- }
493
-
494
- // ==================== PRIVATE METHODS ====================
495
-
496
- private createMetadataWithEmbedding(
497
- content: string,
498
- category: string,
499
- topic: string,
500
- importance: number,
501
- customMetadata: Record<string, string>
502
- ): MemoryMetadata {
503
- const contentBuffer = Buffer.from(content, 'utf-8');
504
- const timestamp = Date.now();
505
-
506
- return {
507
- contentType: 'text/plain',
508
- contentSize: contentBuffer.length,
509
- contentHash: '', // Will be set to blobId after upload
510
- category,
511
- topic: topic || `Memory about ${category}`,
512
- importance: Math.max(1, Math.min(10, importance)),
513
- embeddingDimension: 3072,
514
- createdTimestamp: timestamp,
515
- customMetadata
516
- };
517
- }
518
-
519
- private async uploadToWalrus(content: string, _metadata: MemoryMetadata): Promise<string> {
520
- // Use Walrus SDK writeBlob when signer is available
521
- if (this.walrusClient && this.config.signer) {
522
- try {
523
- const blob = new TextEncoder().encode(content);
524
- const { blobId } = await this.walrusClient.walrus.writeBlob({
525
- blob,
526
- deletable: false,
527
- epochs: this.config.storageEpochs,
528
- signer: this.config.signer
529
- });
530
- return blobId;
531
- } catch (error) {
532
- throw new WalrusError(`Walrus SDK upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
533
- }
534
- }
535
-
536
- // Fallback to REST API based on useUploadRelay config
537
- try {
538
- let url: string;
539
- if (this.config.useUploadRelay) {
540
- // Upload Relay: optimized for browser/mobile (fewer network connections)
541
- // POST to /v1/blobs endpoint
542
- url = `${this.config.uploadRelayHost}/v1/blobs?epochs=${this.config.storageEpochs}`;
543
- } else {
544
- // Publisher: direct upload (server-side)
545
- // PUT to /v1/blobs endpoint
546
- url = `${this.config.publisherHost}/v1/blobs?epochs=${this.config.storageEpochs}`;
547
- }
548
-
549
- const response = await fetch(url, {
550
- method: 'PUT',
551
- headers: {
552
- 'Content-Type': 'application/octet-stream'
553
- },
554
- body: new TextEncoder().encode(content)
555
- });
556
-
557
- if (!response.ok) {
558
- throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
559
- }
560
-
561
- const result = await response.json();
562
- // Handle both newlyCreated and alreadyCertified responses
563
- const newBlob = result.newlyCreated?.blobObject;
564
- const certifiedBlob = result.alreadyCertified?.blobId;
565
- return newBlob?.blobId || certifiedBlob || result.blobId;
566
- } catch (error) {
567
- throw new WalrusError(`Walrus REST upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
568
- }
569
- }
570
-
571
- private async retrieveFromWalrus(blobId: string): Promise<{ content: string; metadata: MemoryMetadata }> {
572
- // Use Walrus SDK readBlob when client is available
573
- if (this.walrusClient) {
574
- try {
575
- const blob = await this.walrusClient.walrus.readBlob({ blobId });
576
- const content = new TextDecoder().decode(blob);
577
-
578
- const metadata: MemoryMetadata = {
579
- contentType: 'text/plain',
580
- contentSize: content.length,
581
- contentHash: blobId,
582
- category: 'unknown',
583
- topic: 'Retrieved memory',
584
- importance: 5,
585
- embeddingDimension: 3072,
586
- createdTimestamp: Date.now()
587
- };
588
-
589
- return { content, metadata };
590
- } catch (error) {
591
- // Fall through to REST API on SDK error
592
- console.warn('Walrus SDK readBlob failed, falling back to REST API:', error);
593
- }
594
- }
595
-
596
- // Fallback to REST API - Aggregator endpoint: GET /v1/blobs/<blob-id>
597
- try {
598
- const response = await fetch(`${this.config.aggregatorHost}/v1/blobs/${blobId}`);
599
-
600
- if (!response.ok) {
601
- throw new Error(`Retrieval failed: ${response.status} ${response.statusText}`);
602
- }
603
-
604
- const content = await response.text();
605
-
606
- const metadata: MemoryMetadata = {
607
- contentType: 'text/plain',
608
- contentSize: content.length,
609
- contentHash: blobId,
610
- category: 'unknown',
611
- topic: 'Retrieved memory',
612
- importance: 5,
613
- embeddingDimension: 3072,
614
- createdTimestamp: Date.now()
615
- };
616
-
617
- return { content, metadata };
618
- } catch (error) {
619
- throw new WalrusError(`Walrus retrieval failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
620
- }
621
- }
622
-
623
- private isCacheValid(timestamp: Date): boolean {
624
- return Date.now() - timestamp.getTime() < this.CACHE_TTL_MS;
625
- }
626
-
627
- private updateAverageUploadTime(newTime: number): void {
628
- const totalUploads = this.stats.successfulUploads;
629
- this.stats.averageUploadTime =
630
- (this.stats.averageUploadTime * (totalUploads - 1) + newTime) / totalUploads;
631
- }
632
-
633
- private updateAverageRetrievalTime(newTime: number): void {
634
- const totalRetrievals = this.stats.totalRetrievals;
635
- this.stats.averageRetrievalTime =
636
- (this.stats.averageRetrievalTime * (totalRetrievals - 1) + newTime) / totalRetrievals;
637
- }
1
+ /**
2
+ * WalrusStorageService - Production Decentralized Storage
3
+ *
4
+ * Walrus client integration with SEAL encryption,
5
+ * content verification, and standardized tagging per https://docs.wal.app/
6
+ *
7
+ * Uses @mysten/walrus SDK for writeBlob/readBlob operations when signer is available.
8
+ * Falls back to REST API for read-only operations without signer.
9
+ */
10
+
11
+ import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
12
+ import { walrus } from '@mysten/walrus';
13
+ import type { Signer } from '@mysten/sui/cryptography';
14
+ import type { SealService } from '../seal/SealService';
15
+
16
+ export interface WalrusConfig {
17
+ network?: 'testnet' | 'mainnet';
18
+ adminAddress?: string;
19
+ storageEpochs?: number;
20
+ /** Publisher URL for direct blob uploads (server-side) */
21
+ publisherHost?: string;
22
+ /** Upload Relay URL for browser/mobile uploads (fewer connections, faster) */
23
+ uploadRelayHost?: string;
24
+ /** Aggregator URL for blob retrieval */
25
+ aggregatorHost?: string;
26
+ /** Use upload relay instead of publisher (default: true for browser, false for server) */
27
+ useUploadRelay?: boolean;
28
+ retryAttempts?: number;
29
+ timeoutMs?: number;
30
+ sealService?: SealService;
31
+ signer?: Signer; // Required for writeBlob operations
32
+ }
33
+
34
+ export interface MemoryMetadata {
35
+ contentType: string;
36
+ contentSize: number;
37
+ contentHash: string;
38
+ category: string;
39
+ topic: string;
40
+ importance: number; // 1-10 scale
41
+ embeddingBlobId?: string;
42
+ embeddingDimension: number;
43
+ createdTimestamp: number;
44
+ updatedTimestamp?: number;
45
+ customMetadata?: Record<string, string>;
46
+ isEncrypted?: boolean;
47
+ encryptionType?: string;
48
+ }
49
+
50
+ export interface WalrusUploadResult {
51
+ blobId: string;
52
+ metadata: MemoryMetadata;
53
+ embeddingBlobId?: string;
54
+ isEncrypted: boolean;
55
+ backupKey?: string;
56
+ storageEpochs: number;
57
+ uploadTimeMs: number;
58
+ }
59
+
60
+ export interface WalrusRetrievalResult {
61
+ content: string;
62
+ metadata: MemoryMetadata;
63
+ isDecrypted: boolean;
64
+ retrievalTimeMs: number;
65
+ }
66
+
67
+ export interface BlobInfo {
68
+ blobId: string;
69
+ contentType: string;
70
+ contentLength: number;
71
+ contentHash: string;
72
+ metadata: Record<string, string>;
73
+ tags: string[];
74
+ }
75
+
76
+ class WalrusError extends Error {
77
+ public readonly cause?: unknown;
78
+
79
+ constructor(message: string, cause?: unknown) {
80
+ super(message);
81
+ this.name = 'WalrusError';
82
+ this.cause = cause;
83
+ }
84
+ }
85
+
86
+ export interface WalrusStats {
87
+ totalUploads: number;
88
+ totalRetrievals: number;
89
+ successfulUploads: number;
90
+ failedUploads: number;
91
+ cacheHitRate: number;
92
+ averageUploadTime: number;
93
+ averageRetrievalTime: number;
94
+ localFallbackCount: number;
95
+ totalStorageUsed: number;
96
+ }
97
+
98
+ interface CachedBlob {
99
+ content: Uint8Array;
100
+ contentType: string;
101
+ timestamp: Date;
102
+ metadata: Record<string, string>;
103
+ }
104
+
105
+ /**
106
+ * Production-ready Walrus storage service using @mysten/walrus SDK
107
+ */
108
+ export class WalrusStorageService {
109
+ private readonly config: Omit<Required<WalrusConfig>, 'sealService' | 'signer'> & {
110
+ sealService?: SealService;
111
+ signer?: Signer;
112
+ };
113
+ private readonly cache = new Map<string, CachedBlob>();
114
+ private stats: WalrusStats = {
115
+ totalUploads: 0,
116
+ totalRetrievals: 0,
117
+ successfulUploads: 0,
118
+ failedUploads: 0,
119
+ cacheHitRate: 0,
120
+ averageUploadTime: 0,
121
+ averageRetrievalTime: 0,
122
+ localFallbackCount: 0,
123
+ totalStorageUsed: 0
124
+ };
125
+
126
+ private readonly CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
127
+ private sealService?: SealService;
128
+ private walrusClient: ReturnType<typeof this.createWalrusClient> | null = null;
129
+
130
+ constructor(config: Partial<WalrusConfig> = {}) {
131
+ const network = config.network || 'testnet';
132
+ // Detect browser environment for default useUploadRelay
133
+ const isBrowser = typeof window !== 'undefined';
134
+
135
+ this.config = {
136
+ network,
137
+ adminAddress: config.adminAddress || '',
138
+ storageEpochs: config.storageEpochs || 12,
139
+ // Publisher: direct uploads (server-side)
140
+ publisherHost: config.publisherHost || 'https://publisher.walrus-testnet.walrus.space',
141
+ // Upload Relay: optimized for browser/mobile (fewer connections)
142
+ uploadRelayHost: config.uploadRelayHost || 'https://upload-relay.testnet.walrus.space',
143
+ // Aggregator: blob retrieval
144
+ aggregatorHost: config.aggregatorHost || 'https://aggregator.walrus-testnet.walrus.space',
145
+ // Use upload relay by default in browser, publisher on server
146
+ useUploadRelay: config.useUploadRelay ?? isBrowser,
147
+ retryAttempts: config.retryAttempts || 3,
148
+ timeoutMs: config.timeoutMs || 60000,
149
+ sealService: config.sealService,
150
+ signer: config.signer
151
+ };
152
+
153
+ this.sealService = config.sealService;
154
+
155
+ // Initialize Walrus SDK client
156
+ this.walrusClient = this.createWalrusClient(network);
157
+ }
158
+
159
+ /**
160
+ * Create Walrus client using @mysten/walrus SDK
161
+ * Uses $extend to add walrus capabilities to SuiClient
162
+ */
163
+ private createWalrusClient(network: 'testnet' | 'mainnet') {
164
+ const suiClient = new SuiClient({
165
+ url: getFullnodeUrl(network)
166
+ });
167
+
168
+ // Extend SuiClient with Walrus capabilities using $extend
169
+ return suiClient.$extend(walrus({
170
+ network,
171
+ }));
172
+ }
173
+
174
+ // ==================== PUBLIC API ====================
175
+
176
+ /**
177
+ * Store memory with encryption and metadata
178
+ */
179
+ async storeMemory(
180
+ content: string,
181
+ category: string,
182
+ options: {
183
+ topic?: string;
184
+ importance?: number;
185
+ customMetadata?: Record<string, string>;
186
+ contextId?: string;
187
+ appId?: string;
188
+ encrypt?: boolean;
189
+ userAddress?: string;
190
+ } = {}
191
+ ): Promise<WalrusUploadResult> {
192
+ const startTime = Date.now();
193
+ this.stats.totalUploads++;
194
+
195
+ try {
196
+ const {
197
+ topic = `Memory about ${category}`,
198
+ importance = 5,
199
+ customMetadata = {},
200
+ contextId,
201
+ appId,
202
+ encrypt = false,
203
+ userAddress
204
+ } = options;
205
+
206
+ let processedContent = content;
207
+ let backupKey: string | undefined;
208
+ let isEncrypted = false;
209
+
210
+ // Use SEAL encryption if requested and available
211
+ if (encrypt && this.sealService && userAddress) {
212
+ const sessionConfig = {
213
+ address: userAddress,
214
+ packageId: this.config.adminAddress, // Use configured package ID
215
+ ttlMin: 60 // 1 hour session
216
+ };
217
+ const sessionKey = await this.sealService.createSession(sessionConfig);
218
+ const encryptResult = await this.sealService.encryptData({
219
+ data: new TextEncoder().encode(content),
220
+ id: userAddress,
221
+ threshold: 2 // Default threshold
222
+ });
223
+ processedContent = JSON.stringify(encryptResult);
224
+ backupKey = 'session-key-reference'; // Store session reference
225
+ isEncrypted = true;
226
+ }
227
+
228
+ const metadata = this.createMetadataWithEmbedding(
229
+ processedContent,
230
+ category,
231
+ topic,
232
+ importance,
233
+ {
234
+ ...customMetadata,
235
+ ...(contextId && { 'context-id': contextId }),
236
+ ...(appId && { 'app-id': appId }),
237
+ ...(userAddress && { owner: userAddress }),
238
+ encrypted: isEncrypted.toString(),
239
+ ...(isEncrypted && { 'encryption-type': 'seal' })
240
+ }
241
+ );
242
+
243
+ // Upload to Walrus with official client
244
+ const blobId = await this.uploadToWalrus(processedContent, metadata);
245
+
246
+ // Use Walrus blob_id as content hash (already content-addressed via blake2b256)
247
+ metadata.contentHash = blobId;
248
+
249
+ const uploadTimeMs = Date.now() - startTime;
250
+ this.stats.successfulUploads++;
251
+ this.updateAverageUploadTime(uploadTimeMs);
252
+
253
+ return {
254
+ blobId,
255
+ metadata,
256
+ isEncrypted,
257
+ backupKey,
258
+ storageEpochs: this.config.storageEpochs,
259
+ uploadTimeMs
260
+ };
261
+
262
+ } catch (error) {
263
+ this.stats.failedUploads++;
264
+ throw new WalrusError(
265
+ `Failed to store memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
266
+ error
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Retrieve memory with decryption
273
+ */
274
+ async retrieveMemory(
275
+ blobId: string,
276
+ options: {
277
+ userAddress?: string;
278
+ sessionKey?: any; // SEAL session key
279
+ txBytes?: Uint8Array;
280
+ } = {}
281
+ ): Promise<WalrusRetrievalResult> {
282
+ const startTime = Date.now();
283
+ this.stats.totalRetrievals++;
284
+
285
+ try {
286
+ // Check cache first
287
+ const cached = this.cache.get(blobId);
288
+ if (cached && this.isCacheValid(cached.timestamp)) {
289
+ this.stats.cacheHitRate = (this.stats.cacheHitRate * (this.stats.totalRetrievals - 1) + 1) / this.stats.totalRetrievals;
290
+
291
+ const content = new TextDecoder().decode(cached.content);
292
+ return {
293
+ content,
294
+ metadata: cached.metadata as unknown as MemoryMetadata,
295
+ isDecrypted: false,
296
+ retrievalTimeMs: Date.now() - startTime
297
+ };
298
+ }
299
+
300
+ // Retrieve from Walrus
301
+ const { content, metadata } = await this.retrieveFromWalrus(blobId);
302
+
303
+ let processedContent = content;
304
+ let isDecrypted = false;
305
+
306
+ // Decrypt if needed
307
+ if (metadata.isEncrypted && metadata.encryptionType === 'seal' && this.sealService) {
308
+ const { userAddress, sessionKey, txBytes } = options;
309
+ if (userAddress && sessionKey && txBytes) {
310
+ try {
311
+ const encryptedData = JSON.parse(content);
312
+ const decryptedBytes = await this.sealService.decryptData({
313
+ encryptedObject: new Uint8Array(encryptedData),
314
+ sessionKey,
315
+ txBytes
316
+ });
317
+ processedContent = new TextDecoder().decode(decryptedBytes);
318
+ isDecrypted = true;
319
+ } catch (decryptError) {
320
+ console.warn('SEAL decryption failed, returning encrypted content:', decryptError);
321
+ }
322
+ }
323
+ }
324
+
325
+ const retrievalTimeMs = Date.now() - startTime;
326
+ this.updateAverageRetrievalTime(retrievalTimeMs);
327
+
328
+ return {
329
+ content: processedContent,
330
+ metadata,
331
+ isDecrypted,
332
+ retrievalTimeMs
333
+ };
334
+
335
+ } catch (error) {
336
+ throw new WalrusError(
337
+ `Failed to retrieve memory ${blobId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
338
+ error
339
+ );
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get service statistics
345
+ */
346
+ getStats(): WalrusStats {
347
+ return { ...this.stats };
348
+ }
349
+
350
+ /**
351
+ * Clear cache
352
+ */
353
+ clearCache(): void {
354
+ this.cache.clear();
355
+ }
356
+
357
+ /**
358
+ * Retrieve content by blobId with optional decryption
359
+ */
360
+ async retrieveContent(blobId: string, decryptionKey?: string | Uint8Array): Promise<{
361
+ content: string;
362
+ metadata: MemoryMetadata;
363
+ retrievalTimeMs: number;
364
+ isFromCache: boolean;
365
+ }> {
366
+ const startTime = Date.now();
367
+
368
+ try {
369
+ const result = await this.retrieveFromWalrus(blobId);
370
+
371
+ return {
372
+ content: result.content,
373
+ metadata: result.metadata,
374
+ retrievalTimeMs: Date.now() - startTime,
375
+ isFromCache: false // TODO: implement cache checking
376
+ };
377
+ } catch (error) {
378
+ throw new Error(`Failed to retrieve content: ${error}`);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * List blobs for a specific user
384
+ */
385
+ async listUserBlobs(userId: string, options: {
386
+ category?: string;
387
+ limit?: number;
388
+ offset?: number;
389
+ sortBy?: 'date' | 'size' | 'importance';
390
+ filters?: Record<string, any>;
391
+ } = {}): Promise<{ blobs: BlobInfo[]; totalCount: number }> {
392
+ // TODO: Implement actual Walrus listing when API is available
393
+ // For now, return empty result as this would require backend indexing
394
+ console.warn('listUserBlobs not yet implemented - requires Walrus indexing service');
395
+ return {
396
+ blobs: [],
397
+ totalCount: 0
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Delete a blob by ID
403
+ */
404
+ async deleteBlob(blobId: string): Promise<boolean> {
405
+ // TODO: Implement actual Walrus deletion when API is available
406
+ // Walrus typically doesn't support deletion, but this could mark as deleted
407
+ console.warn('deleteBlob not yet implemented - Walrus typically immutable');
408
+ return false;
409
+ }
410
+
411
+ /**
412
+ * Check Walrus service availability
413
+ */
414
+ async checkWalrusAvailability(): Promise<boolean> {
415
+ // TODO: Implement proper availability check with official client
416
+ console.warn('checkWalrusAvailability not yet implemented - assuming available for testing');
417
+ return true; // Return true to allow tests to run, even though implementation is placeholder
418
+ }
419
+
420
+ /**
421
+ * Get cache information
422
+ */
423
+ getCacheInfo(): {
424
+ size: number;
425
+ maxSize: number;
426
+ hitRate: number;
427
+ entries: number;
428
+ } {
429
+ return {
430
+ size: this.cache.size, // Approximate
431
+ maxSize: 100, // Default cache size
432
+ hitRate: this.stats.cacheHitRate,
433
+ entries: this.cache.size
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Upload content with metadata
439
+ */
440
+ async uploadContentWithMetadata(
441
+ content: string,
442
+ userId: string,
443
+ options: {
444
+ category?: string;
445
+ topic?: string;
446
+ importance?: number;
447
+ additionalTags?: Record<string, string>;
448
+ enableEncryption?: boolean;
449
+ }
450
+ ): Promise<{
451
+ blobId: string;
452
+ metadata: MemoryMetadata;
453
+ uploadTimeMs: number;
454
+ isEncrypted: boolean;
455
+ }> {
456
+ const startTime = Date.now();
457
+
458
+ try {
459
+ // Upload to Walrus first to get blob_id (which serves as content hash)
460
+ const tempMetadata: MemoryMetadata = {
461
+ contentType: 'application/json',
462
+ contentSize: content.length,
463
+ contentHash: '', // Will be set to blobId below
464
+ category: options.category || 'general',
465
+ topic: options.topic || 'misc',
466
+ importance: options.importance || 5,
467
+ embeddingDimension: 0,
468
+ createdTimestamp: Date.now(),
469
+ updatedTimestamp: Date.now(),
470
+ customMetadata: options.additionalTags || {},
471
+ isEncrypted: options.enableEncryption || false,
472
+ encryptionType: options.enableEncryption ? 'seal' : undefined
473
+ };
474
+
475
+ const blobId = await this.uploadToWalrus(content, tempMetadata);
476
+
477
+ // Use Walrus blob_id as content hash (already content-addressed via blake2b256)
478
+ const memoryMetadata: MemoryMetadata = {
479
+ ...tempMetadata,
480
+ contentHash: blobId, // Walrus blob_id serves as content hash
481
+ };
482
+
483
+ return {
484
+ blobId,
485
+ metadata: memoryMetadata,
486
+ uploadTimeMs: Date.now() - startTime,
487
+ isEncrypted: memoryMetadata.isEncrypted || false
488
+ };
489
+ } catch (error) {
490
+ throw new Error(`Failed to upload content: ${error}`);
491
+ }
492
+ }
493
+
494
+ // ==================== PRIVATE METHODS ====================
495
+
496
+ private createMetadataWithEmbedding(
497
+ content: string,
498
+ category: string,
499
+ topic: string,
500
+ importance: number,
501
+ customMetadata: Record<string, string>
502
+ ): MemoryMetadata {
503
+ const contentBuffer = Buffer.from(content, 'utf-8');
504
+ const timestamp = Date.now();
505
+
506
+ return {
507
+ contentType: 'text/plain',
508
+ contentSize: contentBuffer.length,
509
+ contentHash: '', // Will be set to blobId after upload
510
+ category,
511
+ topic: topic || `Memory about ${category}`,
512
+ importance: Math.max(1, Math.min(10, importance)),
513
+ embeddingDimension: 3072,
514
+ createdTimestamp: timestamp,
515
+ customMetadata
516
+ };
517
+ }
518
+
519
+ private async uploadToWalrus(content: string, _metadata: MemoryMetadata): Promise<string> {
520
+ // Use Walrus SDK writeBlob when signer is available
521
+ if (this.walrusClient && this.config.signer) {
522
+ try {
523
+ const blob = new TextEncoder().encode(content);
524
+ const { blobId } = await this.walrusClient.walrus.writeBlob({
525
+ blob,
526
+ deletable: false,
527
+ epochs: this.config.storageEpochs,
528
+ signer: this.config.signer
529
+ });
530
+ return blobId;
531
+ } catch (error) {
532
+ throw new WalrusError(`Walrus SDK upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
533
+ }
534
+ }
535
+
536
+ // Fallback to REST API based on useUploadRelay config
537
+ try {
538
+ let url: string;
539
+ if (this.config.useUploadRelay) {
540
+ // Upload Relay: optimized for browser/mobile (fewer network connections)
541
+ // POST to /v1/blobs endpoint
542
+ url = `${this.config.uploadRelayHost}/v1/blobs?epochs=${this.config.storageEpochs}`;
543
+ } else {
544
+ // Publisher: direct upload (server-side)
545
+ // PUT to /v1/blobs endpoint
546
+ url = `${this.config.publisherHost}/v1/blobs?epochs=${this.config.storageEpochs}`;
547
+ }
548
+
549
+ const response = await fetch(url, {
550
+ method: 'PUT',
551
+ headers: {
552
+ 'Content-Type': 'application/octet-stream'
553
+ },
554
+ body: new TextEncoder().encode(content)
555
+ });
556
+
557
+ if (!response.ok) {
558
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
559
+ }
560
+
561
+ const result = await response.json();
562
+ // Handle both newlyCreated and alreadyCertified responses
563
+ const newBlob = result.newlyCreated?.blobObject;
564
+ const certifiedBlob = result.alreadyCertified?.blobId;
565
+ return newBlob?.blobId || certifiedBlob || result.blobId;
566
+ } catch (error) {
567
+ throw new WalrusError(`Walrus REST upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
568
+ }
569
+ }
570
+
571
+ private async retrieveFromWalrus(blobId: string): Promise<{ content: string; metadata: MemoryMetadata }> {
572
+ // Use Walrus SDK readBlob when client is available
573
+ if (this.walrusClient) {
574
+ try {
575
+ const blob = await this.walrusClient.walrus.readBlob({ blobId });
576
+ const content = new TextDecoder().decode(blob);
577
+
578
+ const metadata: MemoryMetadata = {
579
+ contentType: 'text/plain',
580
+ contentSize: content.length,
581
+ contentHash: blobId,
582
+ category: 'unknown',
583
+ topic: 'Retrieved memory',
584
+ importance: 5,
585
+ embeddingDimension: 3072,
586
+ createdTimestamp: Date.now()
587
+ };
588
+
589
+ return { content, metadata };
590
+ } catch (error) {
591
+ // Fall through to REST API on SDK error
592
+ console.warn('Walrus SDK readBlob failed, falling back to REST API:', error);
593
+ }
594
+ }
595
+
596
+ // Fallback to REST API - Aggregator endpoint: GET /v1/blobs/<blob-id>
597
+ try {
598
+ const response = await fetch(`${this.config.aggregatorHost}/v1/blobs/${blobId}`);
599
+
600
+ if (!response.ok) {
601
+ throw new Error(`Retrieval failed: ${response.status} ${response.statusText}`);
602
+ }
603
+
604
+ const content = await response.text();
605
+
606
+ const metadata: MemoryMetadata = {
607
+ contentType: 'text/plain',
608
+ contentSize: content.length,
609
+ contentHash: blobId,
610
+ category: 'unknown',
611
+ topic: 'Retrieved memory',
612
+ importance: 5,
613
+ embeddingDimension: 3072,
614
+ createdTimestamp: Date.now()
615
+ };
616
+
617
+ return { content, metadata };
618
+ } catch (error) {
619
+ throw new WalrusError(`Walrus retrieval failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
620
+ }
621
+ }
622
+
623
+ private isCacheValid(timestamp: Date): boolean {
624
+ return Date.now() - timestamp.getTime() < this.CACHE_TTL_MS;
625
+ }
626
+
627
+ private updateAverageUploadTime(newTime: number): void {
628
+ const totalUploads = this.stats.successfulUploads;
629
+ this.stats.averageUploadTime =
630
+ (this.stats.averageUploadTime * (totalUploads - 1) + newTime) / totalUploads;
631
+ }
632
+
633
+ private updateAverageRetrievalTime(newTime: number): void {
634
+ const totalRetrievals = this.stats.totalRetrievals;
635
+ this.stats.averageRetrievalTime =
636
+ (this.stats.averageRetrievalTime * (totalRetrievals - 1) + newTime) / totalRetrievals;
637
+ }
638
638
  }