@snaha/swarm-id 0.0.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.
Files changed (223) hide show
  1. package/README.md +431 -0
  2. package/dist/chunk/bmt.d.ts +17 -0
  3. package/dist/chunk/bmt.d.ts.map +1 -0
  4. package/dist/chunk/cac.d.ts +18 -0
  5. package/dist/chunk/cac.d.ts.map +1 -0
  6. package/dist/chunk/constants.d.ts +10 -0
  7. package/dist/chunk/constants.d.ts.map +1 -0
  8. package/dist/chunk/encrypted-cac.d.ts +48 -0
  9. package/dist/chunk/encrypted-cac.d.ts.map +1 -0
  10. package/dist/chunk/encryption.d.ts +86 -0
  11. package/dist/chunk/encryption.d.ts.map +1 -0
  12. package/dist/chunk/index.d.ts +6 -0
  13. package/dist/chunk/index.d.ts.map +1 -0
  14. package/dist/index.d.ts +46 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/proxy/act/act.d.ts +78 -0
  17. package/dist/proxy/act/act.d.ts.map +1 -0
  18. package/dist/proxy/act/crypto.d.ts +44 -0
  19. package/dist/proxy/act/crypto.d.ts.map +1 -0
  20. package/dist/proxy/act/grantee-list.d.ts +82 -0
  21. package/dist/proxy/act/grantee-list.d.ts.map +1 -0
  22. package/dist/proxy/act/history.d.ts +183 -0
  23. package/dist/proxy/act/history.d.ts.map +1 -0
  24. package/dist/proxy/act/index.d.ts +104 -0
  25. package/dist/proxy/act/index.d.ts.map +1 -0
  26. package/dist/proxy/chunking-encrypted.d.ts +14 -0
  27. package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
  28. package/dist/proxy/chunking.d.ts +15 -0
  29. package/dist/proxy/chunking.d.ts.map +1 -0
  30. package/dist/proxy/download-data.d.ts +16 -0
  31. package/dist/proxy/download-data.d.ts.map +1 -0
  32. package/dist/proxy/feed-manifest.d.ts +62 -0
  33. package/dist/proxy/feed-manifest.d.ts.map +1 -0
  34. package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
  35. package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
  36. package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
  37. package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
  38. package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
  39. package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
  40. package/dist/proxy/feeds/epochs/index.d.ts +35 -0
  41. package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
  42. package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
  43. package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
  44. package/dist/proxy/feeds/epochs/types.d.ts +109 -0
  45. package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
  46. package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
  47. package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
  48. package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
  49. package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
  50. package/dist/proxy/feeds/index.d.ts +5 -0
  51. package/dist/proxy/feeds/index.d.ts.map +1 -0
  52. package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
  53. package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
  54. package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
  55. package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
  56. package/dist/proxy/feeds/sequence/index.d.ts +23 -0
  57. package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
  58. package/dist/proxy/feeds/sequence/types.d.ts +80 -0
  59. package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
  60. package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
  61. package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
  62. package/dist/proxy/index.d.ts +6 -0
  63. package/dist/proxy/index.d.ts.map +1 -0
  64. package/dist/proxy/manifest-builder.d.ts +183 -0
  65. package/dist/proxy/manifest-builder.d.ts.map +1 -0
  66. package/dist/proxy/mantaray-encrypted.d.ts +27 -0
  67. package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
  68. package/dist/proxy/mantaray.d.ts +26 -0
  69. package/dist/proxy/mantaray.d.ts.map +1 -0
  70. package/dist/proxy/types.d.ts +29 -0
  71. package/dist/proxy/types.d.ts.map +1 -0
  72. package/dist/proxy/upload-data.d.ts +17 -0
  73. package/dist/proxy/upload-data.d.ts.map +1 -0
  74. package/dist/proxy/upload-encrypted-data.d.ts +103 -0
  75. package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
  76. package/dist/schemas.d.ts +240 -0
  77. package/dist/schemas.d.ts.map +1 -0
  78. package/dist/storage/debounced-uploader.d.ts +62 -0
  79. package/dist/storage/debounced-uploader.d.ts.map +1 -0
  80. package/dist/storage/utilization-store.d.ts +108 -0
  81. package/dist/storage/utilization-store.d.ts.map +1 -0
  82. package/dist/swarm-id-auth.d.ts +74 -0
  83. package/dist/swarm-id-auth.d.ts.map +1 -0
  84. package/dist/swarm-id-auth.js +2 -0
  85. package/dist/swarm-id-auth.js.map +1 -0
  86. package/dist/swarm-id-client.d.ts +878 -0
  87. package/dist/swarm-id-client.d.ts.map +1 -0
  88. package/dist/swarm-id-client.js +2 -0
  89. package/dist/swarm-id-client.js.map +1 -0
  90. package/dist/swarm-id-proxy.d.ts +236 -0
  91. package/dist/swarm-id-proxy.d.ts.map +1 -0
  92. package/dist/swarm-id-proxy.js +2 -0
  93. package/dist/swarm-id-proxy.js.map +1 -0
  94. package/dist/swarm-id.esm.js +2 -0
  95. package/dist/swarm-id.esm.js.map +1 -0
  96. package/dist/swarm-id.umd.js +2 -0
  97. package/dist/swarm-id.umd.js.map +1 -0
  98. package/dist/sync/index.d.ts +9 -0
  99. package/dist/sync/index.d.ts.map +1 -0
  100. package/dist/sync/key-derivation.d.ts +25 -0
  101. package/dist/sync/key-derivation.d.ts.map +1 -0
  102. package/dist/sync/restore-account.d.ts +28 -0
  103. package/dist/sync/restore-account.d.ts.map +1 -0
  104. package/dist/sync/serialization.d.ts +16 -0
  105. package/dist/sync/serialization.d.ts.map +1 -0
  106. package/dist/sync/store-interfaces.d.ts +53 -0
  107. package/dist/sync/store-interfaces.d.ts.map +1 -0
  108. package/dist/sync/sync-account.d.ts +44 -0
  109. package/dist/sync/sync-account.d.ts.map +1 -0
  110. package/dist/sync/types.d.ts +13 -0
  111. package/dist/sync/types.d.ts.map +1 -0
  112. package/dist/test-fixtures.d.ts +17 -0
  113. package/dist/test-fixtures.d.ts.map +1 -0
  114. package/dist/types-BD_VkNn0.js +2 -0
  115. package/dist/types-BD_VkNn0.js.map +1 -0
  116. package/dist/types-lJCaT-50.js +2 -0
  117. package/dist/types-lJCaT-50.js.map +1 -0
  118. package/dist/types.d.ts +2157 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/utils/account-payload.d.ts +94 -0
  121. package/dist/utils/account-payload.d.ts.map +1 -0
  122. package/dist/utils/account-state-snapshot.d.ts +38 -0
  123. package/dist/utils/account-state-snapshot.d.ts.map +1 -0
  124. package/dist/utils/backup-encryption.d.ts +127 -0
  125. package/dist/utils/backup-encryption.d.ts.map +1 -0
  126. package/dist/utils/batch-utilization.d.ts +432 -0
  127. package/dist/utils/batch-utilization.d.ts.map +1 -0
  128. package/dist/utils/constants.d.ts +11 -0
  129. package/dist/utils/constants.d.ts.map +1 -0
  130. package/dist/utils/hex.d.ts +17 -0
  131. package/dist/utils/hex.d.ts.map +1 -0
  132. package/dist/utils/key-derivation.d.ts +92 -0
  133. package/dist/utils/key-derivation.d.ts.map +1 -0
  134. package/dist/utils/storage-managers.d.ts +65 -0
  135. package/dist/utils/storage-managers.d.ts.map +1 -0
  136. package/dist/utils/swarm-id-export.d.ts +24 -0
  137. package/dist/utils/swarm-id-export.d.ts.map +1 -0
  138. package/dist/utils/ttl.d.ts +49 -0
  139. package/dist/utils/ttl.d.ts.map +1 -0
  140. package/dist/utils/url.d.ts +41 -0
  141. package/dist/utils/url.d.ts.map +1 -0
  142. package/dist/utils/versioned-storage.d.ts +131 -0
  143. package/dist/utils/versioned-storage.d.ts.map +1 -0
  144. package/package.json +78 -0
  145. package/src/chunk/bmt.test.ts +217 -0
  146. package/src/chunk/bmt.ts +57 -0
  147. package/src/chunk/cac.test.ts +214 -0
  148. package/src/chunk/cac.ts +65 -0
  149. package/src/chunk/constants.ts +18 -0
  150. package/src/chunk/encrypted-cac.test.ts +385 -0
  151. package/src/chunk/encrypted-cac.ts +131 -0
  152. package/src/chunk/encryption.test.ts +352 -0
  153. package/src/chunk/encryption.ts +300 -0
  154. package/src/chunk/index.ts +47 -0
  155. package/src/index.ts +430 -0
  156. package/src/proxy/act/act.test.ts +278 -0
  157. package/src/proxy/act/act.ts +158 -0
  158. package/src/proxy/act/bee-compat.test.ts +948 -0
  159. package/src/proxy/act/crypto.test.ts +436 -0
  160. package/src/proxy/act/crypto.ts +376 -0
  161. package/src/proxy/act/grantee-list.test.ts +393 -0
  162. package/src/proxy/act/grantee-list.ts +239 -0
  163. package/src/proxy/act/history.test.ts +360 -0
  164. package/src/proxy/act/history.ts +413 -0
  165. package/src/proxy/act/index.test.ts +748 -0
  166. package/src/proxy/act/index.ts +853 -0
  167. package/src/proxy/chunking-encrypted.ts +95 -0
  168. package/src/proxy/chunking.ts +65 -0
  169. package/src/proxy/download-data.ts +448 -0
  170. package/src/proxy/feed-manifest.ts +174 -0
  171. package/src/proxy/feeds/epochs/async-finder.ts +372 -0
  172. package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
  173. package/src/proxy/feeds/epochs/epoch.ts +181 -0
  174. package/src/proxy/feeds/epochs/finder.ts +282 -0
  175. package/src/proxy/feeds/epochs/index.ts +73 -0
  176. package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
  177. package/src/proxy/feeds/epochs/test-utils.ts +274 -0
  178. package/src/proxy/feeds/epochs/types.ts +128 -0
  179. package/src/proxy/feeds/epochs/updater.ts +192 -0
  180. package/src/proxy/feeds/epochs/utils.ts +62 -0
  181. package/src/proxy/feeds/index.ts +5 -0
  182. package/src/proxy/feeds/sequence/async-finder.ts +31 -0
  183. package/src/proxy/feeds/sequence/finder.ts +73 -0
  184. package/src/proxy/feeds/sequence/index.ts +54 -0
  185. package/src/proxy/feeds/sequence/integration.test.ts +966 -0
  186. package/src/proxy/feeds/sequence/types.ts +103 -0
  187. package/src/proxy/feeds/sequence/updater.ts +71 -0
  188. package/src/proxy/index.ts +5 -0
  189. package/src/proxy/manifest-builder.test.ts +427 -0
  190. package/src/proxy/manifest-builder.ts +679 -0
  191. package/src/proxy/mantaray-encrypted.ts +78 -0
  192. package/src/proxy/mantaray.ts +104 -0
  193. package/src/proxy/types.ts +32 -0
  194. package/src/proxy/upload-data.ts +189 -0
  195. package/src/proxy/upload-encrypted-data.ts +658 -0
  196. package/src/schemas.ts +299 -0
  197. package/src/storage/debounced-uploader.ts +192 -0
  198. package/src/storage/utilization-store.ts +397 -0
  199. package/src/swarm-id-client.test.ts +99 -0
  200. package/src/swarm-id-client.ts +3095 -0
  201. package/src/swarm-id-proxy.ts +3891 -0
  202. package/src/sync/index.ts +28 -0
  203. package/src/sync/restore-account.ts +90 -0
  204. package/src/sync/serialization.ts +39 -0
  205. package/src/sync/store-interfaces.ts +62 -0
  206. package/src/sync/sync-account.test.ts +302 -0
  207. package/src/sync/sync-account.ts +396 -0
  208. package/src/sync/types.ts +11 -0
  209. package/src/test-fixtures.ts +109 -0
  210. package/src/types.ts +1651 -0
  211. package/src/utils/account-state-snapshot.test.ts +595 -0
  212. package/src/utils/account-state-snapshot.ts +94 -0
  213. package/src/utils/backup-encryption.test.ts +442 -0
  214. package/src/utils/backup-encryption.ts +352 -0
  215. package/src/utils/batch-utilization.ts +1309 -0
  216. package/src/utils/constants.ts +20 -0
  217. package/src/utils/hex.ts +27 -0
  218. package/src/utils/key-derivation.ts +197 -0
  219. package/src/utils/storage-managers.ts +365 -0
  220. package/src/utils/ttl.ts +129 -0
  221. package/src/utils/url.test.ts +136 -0
  222. package/src/utils/url.ts +71 -0
  223. package/src/utils/versioned-storage.ts +323 -0
@@ -0,0 +1,397 @@
1
+ /**
2
+ * IndexedDB Cache for Batch Utilization Chunks
3
+ *
4
+ * Provides fast local caching of 4KB utilization chunks to avoid
5
+ * repeated Swarm downloads. Each chunk is 4096 bytes containing
6
+ * 1024 bucket counters (uint32).
7
+ */
8
+
9
+ import { Binary } from "cafe-utility"
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Cache entry for a single utilization chunk
17
+ */
18
+ export interface ChunkCacheEntry {
19
+ /** Batch ID (hex string) */
20
+ batchId: string
21
+
22
+ /** Chunk index (0-63) */
23
+ chunkIndex: number
24
+
25
+ /** Serialized chunk data (4KB) */
26
+ data: Uint8Array
27
+
28
+ /** Content hash for change detection */
29
+ contentHash: string
30
+
31
+ /** SOC reference if uploaded to Swarm */
32
+ socReference?: string
33
+
34
+ /** Last access timestamp (for eviction) */
35
+ lastAccess: number
36
+ }
37
+
38
+ /**
39
+ * Metadata for a batch's utilization state
40
+ */
41
+ export interface BatchMetadata {
42
+ batchId: string
43
+ lastSync: number
44
+ chunkCount: number
45
+ }
46
+
47
+ // ============================================================================
48
+ // IndexedDB Cache Manager
49
+ // ============================================================================
50
+
51
+ const DB_NAME = "swarm-utilization-store"
52
+ const DB_VERSION = 1
53
+ const CHUNKS_STORE = "chunks"
54
+ const METADATA_STORE = "metadata"
55
+
56
+ /**
57
+ * Manages IndexedDB cache for batch utilization chunks
58
+ */
59
+ export class UtilizationStoreDB {
60
+ private db: IDBDatabase | undefined
61
+
62
+ /**
63
+ * Open the IndexedDB database
64
+ */
65
+ async open(): Promise<void> {
66
+ if (this.db) return
67
+
68
+ return new Promise((resolve, reject) => {
69
+ const request = indexedDB.open(DB_NAME, DB_VERSION)
70
+
71
+ request.onerror = () => {
72
+ reject(new Error(`Failed to open IndexedDB: ${request.error}`))
73
+ }
74
+
75
+ request.onsuccess = () => {
76
+ this.db = request.result
77
+ resolve()
78
+ }
79
+
80
+ request.onupgradeneeded = (event) => {
81
+ const db = (event.target as IDBOpenDBRequest).result
82
+
83
+ // Create chunks store with compound key
84
+ if (!db.objectStoreNames.contains(CHUNKS_STORE)) {
85
+ const chunksStore = db.createObjectStore(CHUNKS_STORE, {
86
+ keyPath: ["batchId", "chunkIndex"],
87
+ })
88
+
89
+ // Index for querying by batchId
90
+ chunksStore.createIndex("batchId", "batchId", { unique: false })
91
+
92
+ // Index for eviction by lastAccess
93
+ chunksStore.createIndex("lastAccess", "lastAccess", {
94
+ unique: false,
95
+ })
96
+ }
97
+
98
+ // Create metadata store
99
+ if (!db.objectStoreNames.contains(METADATA_STORE)) {
100
+ db.createObjectStore(METADATA_STORE, { keyPath: "batchId" })
101
+ }
102
+ }
103
+ })
104
+ }
105
+
106
+ /**
107
+ * Get a chunk from cache
108
+ * @param batchId - Batch ID (hex string)
109
+ * @param chunkIndex - Chunk index (0-63)
110
+ * @returns Cache entry or undefined if not found
111
+ */
112
+ async getChunk(
113
+ batchId: string,
114
+ chunkIndex: number,
115
+ ): Promise<ChunkCacheEntry | undefined> {
116
+ await this.open()
117
+
118
+ return new Promise((resolve, reject) => {
119
+ const transaction = this.db!.transaction([CHUNKS_STORE], "readonly")
120
+ const store = transaction.objectStore(CHUNKS_STORE)
121
+ const request = store.get([batchId, chunkIndex])
122
+
123
+ request.onsuccess = () => {
124
+ const entry = request.result as ChunkCacheEntry | undefined
125
+
126
+ if (entry) {
127
+ // Update lastAccess asynchronously (don't wait)
128
+ this.touchChunk(batchId, chunkIndex).catch((err) => {
129
+ console.warn("[UtilizationStore] Failed to update lastAccess:", err)
130
+ })
131
+ }
132
+
133
+ resolve(entry)
134
+ }
135
+
136
+ request.onerror = () => {
137
+ reject(new Error(`Failed to get chunk: ${request.error}`))
138
+ }
139
+ })
140
+ }
141
+
142
+ /**
143
+ * Store a chunk in cache
144
+ * @param entry - Cache entry to store
145
+ */
146
+ async putChunk(entry: ChunkCacheEntry): Promise<void> {
147
+ await this.open()
148
+
149
+ return new Promise((resolve, reject) => {
150
+ const transaction = this.db!.transaction([CHUNKS_STORE], "readwrite")
151
+ const store = transaction.objectStore(CHUNKS_STORE)
152
+
153
+ // Update lastAccess before storing
154
+ const entryWithAccess = {
155
+ ...entry,
156
+ lastAccess: Date.now(),
157
+ }
158
+
159
+ const request = store.put(entryWithAccess)
160
+
161
+ request.onsuccess = () => resolve()
162
+ request.onerror = () => {
163
+ reject(new Error(`Failed to put chunk: ${request.error}`))
164
+ }
165
+ })
166
+ }
167
+
168
+ /**
169
+ * Get all chunks for a batch
170
+ * @param batchId - Batch ID (hex string)
171
+ * @returns Array of cache entries (sorted by chunkIndex)
172
+ */
173
+ async getAllChunks(batchId: string): Promise<ChunkCacheEntry[]> {
174
+ await this.open()
175
+
176
+ return new Promise((resolve, reject) => {
177
+ const transaction = this.db!.transaction([CHUNKS_STORE], "readonly")
178
+ const store = transaction.objectStore(CHUNKS_STORE)
179
+ const index = store.index("batchId")
180
+ const request = index.getAll(batchId)
181
+
182
+ request.onsuccess = () => {
183
+ const entries = request.result as ChunkCacheEntry[]
184
+ // Sort by chunkIndex for predictable order
185
+ entries.sort((a, b) => a.chunkIndex - b.chunkIndex)
186
+ resolve(entries)
187
+ }
188
+
189
+ request.onerror = () => {
190
+ reject(new Error(`Failed to get all chunks: ${request.error}`))
191
+ }
192
+ })
193
+ }
194
+
195
+ /**
196
+ * Clear all chunks for a batch
197
+ * @param batchId - Batch ID (hex string)
198
+ */
199
+ async clearBatch(batchId: string): Promise<void> {
200
+ await this.open()
201
+
202
+ return new Promise((resolve, reject) => {
203
+ const transaction = this.db!.transaction(
204
+ [CHUNKS_STORE, METADATA_STORE],
205
+ "readwrite",
206
+ )
207
+ const chunksStore = transaction.objectStore(CHUNKS_STORE)
208
+ const metadataStore = transaction.objectStore(METADATA_STORE)
209
+
210
+ // Delete all chunks for this batch
211
+ const chunksIndex = chunksStore.index("batchId")
212
+ const chunksRequest = chunksIndex.openCursor(batchId)
213
+
214
+ chunksRequest.onsuccess = () => {
215
+ const cursor = chunksRequest.result
216
+ if (cursor) {
217
+ cursor.delete()
218
+ cursor.continue()
219
+ }
220
+ }
221
+
222
+ // Delete metadata
223
+ metadataStore.delete(batchId)
224
+
225
+ transaction.oncomplete = () => resolve()
226
+ transaction.onerror = () => {
227
+ reject(new Error(`Failed to clear batch: ${transaction.error}`))
228
+ }
229
+ })
230
+ }
231
+
232
+ /**
233
+ * Update lastAccess timestamp for a chunk
234
+ * @param batchId - Batch ID (hex string)
235
+ * @param chunkIndex - Chunk index (0-63)
236
+ */
237
+ private async touchChunk(batchId: string, chunkIndex: number): Promise<void> {
238
+ await this.open()
239
+
240
+ return new Promise((resolve, reject) => {
241
+ const transaction = this.db!.transaction([CHUNKS_STORE], "readwrite")
242
+ const store = transaction.objectStore(CHUNKS_STORE)
243
+ const request = store.get([batchId, chunkIndex])
244
+
245
+ request.onsuccess = () => {
246
+ const entry = request.result as ChunkCacheEntry | undefined
247
+ if (entry) {
248
+ entry.lastAccess = Date.now()
249
+ store.put(entry)
250
+ }
251
+ }
252
+
253
+ transaction.oncomplete = () => resolve()
254
+ transaction.onerror = () => {
255
+ reject(new Error(`Failed to touch chunk: ${transaction.error}`))
256
+ }
257
+ })
258
+ }
259
+
260
+ /**
261
+ * Get batch metadata
262
+ * @param batchId - Batch ID (hex string)
263
+ * @returns Metadata or undefined if not found
264
+ */
265
+ async getMetadata(batchId: string): Promise<BatchMetadata | undefined> {
266
+ await this.open()
267
+
268
+ return new Promise((resolve, reject) => {
269
+ const transaction = this.db!.transaction([METADATA_STORE], "readonly")
270
+ const store = transaction.objectStore(METADATA_STORE)
271
+ const request = store.get(batchId)
272
+
273
+ request.onsuccess = () =>
274
+ resolve(request.result as BatchMetadata | undefined)
275
+ request.onerror = () => {
276
+ reject(new Error(`Failed to get metadata: ${request.error}`))
277
+ }
278
+ })
279
+ }
280
+
281
+ /**
282
+ * Update batch metadata
283
+ * @param metadata - Metadata to store
284
+ */
285
+ async putMetadata(metadata: BatchMetadata): Promise<void> {
286
+ await this.open()
287
+
288
+ return new Promise((resolve, reject) => {
289
+ const transaction = this.db!.transaction([METADATA_STORE], "readwrite")
290
+ const store = transaction.objectStore(METADATA_STORE)
291
+ const request = store.put(metadata)
292
+
293
+ request.onsuccess = () => resolve()
294
+ request.onerror = () => {
295
+ reject(new Error(`Failed to put metadata: ${request.error}`))
296
+ }
297
+ })
298
+ }
299
+
300
+ /**
301
+ * Close the database connection
302
+ */
303
+ close(): void {
304
+ if (this.db) {
305
+ this.db.close()
306
+ this.db = undefined
307
+ }
308
+ }
309
+ }
310
+
311
+ // ============================================================================
312
+ // Cache Eviction
313
+ // ============================================================================
314
+
315
+ /**
316
+ * Policy for cache eviction
317
+ */
318
+ export interface CacheEvictionPolicy {
319
+ /** Maximum age in milliseconds (default: 7 days) */
320
+ maxAge?: number
321
+
322
+ /** Maximum number of chunks to keep (default: 640 = 10 batches) */
323
+ maxChunks?: number
324
+ }
325
+
326
+ /**
327
+ * Evict old cache entries based on policy
328
+ * @param cache - Cache database instance
329
+ * @param policy - Eviction policy
330
+ */
331
+ export async function evictOldEntries(
332
+ cache: UtilizationStoreDB,
333
+ policy: CacheEvictionPolicy = {},
334
+ ): Promise<void> {
335
+ const maxAge = policy.maxAge ?? 7 * 24 * 60 * 60 * 1000 // 7 days
336
+ const maxChunks = policy.maxChunks ?? 640 // 10 batches × 64 chunks
337
+
338
+ await cache.open()
339
+
340
+ const db = (cache as never)["db"] as IDBDatabase
341
+
342
+ return new Promise((resolve, reject) => {
343
+ const transaction = db.transaction([CHUNKS_STORE], "readwrite")
344
+ const store = transaction.objectStore(CHUNKS_STORE)
345
+ const index = store.index("lastAccess")
346
+
347
+ const oldestAllowed = Date.now() - maxAge
348
+ const entries: Array<{ key: IDBValidKey; lastAccess: number }> = []
349
+
350
+ // Collect all entries with their lastAccess times
351
+ const request = index.openCursor()
352
+
353
+ request.onsuccess = () => {
354
+ const cursor = request.result
355
+ if (cursor) {
356
+ const entry = cursor.value as ChunkCacheEntry
357
+ entries.push({
358
+ key: cursor.primaryKey,
359
+ lastAccess: entry.lastAccess,
360
+ })
361
+ cursor.continue()
362
+ } else {
363
+ // All entries collected, now evict
364
+ // 1. Delete entries older than maxAge
365
+ for (const entry of entries) {
366
+ if (entry.lastAccess < oldestAllowed) {
367
+ store.delete(entry.key)
368
+ }
369
+ }
370
+
371
+ // 2. If still over maxChunks, delete oldest entries
372
+ if (entries.length > maxChunks) {
373
+ entries.sort((a, b) => a.lastAccess - b.lastAccess)
374
+ const toDelete = entries.slice(0, entries.length - maxChunks)
375
+ for (const entry of toDelete) {
376
+ store.delete(entry.key)
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ transaction.oncomplete = () => resolve()
383
+ transaction.onerror = () => {
384
+ reject(new Error(`Failed to evict entries: ${transaction.error}`))
385
+ }
386
+ })
387
+ }
388
+
389
+ /**
390
+ * Calculate content hash for chunk data
391
+ * @param data - Chunk data (4KB)
392
+ * @returns Hex string hash
393
+ */
394
+ export function calculateContentHash(data: Uint8Array): string {
395
+ const hash = Binary.keccak256(data)
396
+ return Binary.uint8ArrayToHex(hash)
397
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { SwarmIdClient } from "./swarm-id-client"
3
+
4
+ describe("SwarmIdClient connect()", () => {
5
+ let client: SwarmIdClient
6
+
7
+ beforeEach(() => {
8
+ // Mock window object and its properties
9
+ const mockWindow = {
10
+ addEventListener: vi.fn(),
11
+ removeEventListener: vi.fn(),
12
+ parent: { postMessage: vi.fn() },
13
+ location: { origin: "https://localhost" },
14
+ open: vi.fn(),
15
+ }
16
+
17
+ vi.stubGlobal("window", mockWindow)
18
+ vi.stubGlobal("document", {
19
+ createElement: vi.fn().mockReturnValue({
20
+ style: {},
21
+ onload: null,
22
+ onerror: null,
23
+ src: "",
24
+ contentWindow: { postMessage: vi.fn() },
25
+ }),
26
+ body: {
27
+ appendChild: vi.fn(),
28
+ removeChild: vi.fn(),
29
+ },
30
+ })
31
+
32
+ client = new SwarmIdClient({
33
+ iframeOrigin: "https://swarm-id.example.com",
34
+ metadata: {
35
+ name: "Test App",
36
+ description: "A test application",
37
+ },
38
+ })
39
+ })
40
+
41
+ it("should send connect message to proxy", async () => {
42
+ vi.spyOn(client, "ensureReady").mockImplementation(() => {})
43
+ const sendRequestSpy = vi
44
+ .spyOn(client as never, "sendRequest")
45
+ .mockResolvedValue({
46
+ type: "connectResponse",
47
+ requestId: "test",
48
+ success: true,
49
+ })
50
+
51
+ await client.connect()
52
+
53
+ expect(sendRequestSpy).toHaveBeenCalledWith(
54
+ expect.objectContaining({
55
+ type: "connect",
56
+ agent: undefined,
57
+ }),
58
+ )
59
+ })
60
+
61
+ it("should send agent flag to proxy when agent option is true", async () => {
62
+ vi.spyOn(client, "ensureReady").mockImplementation(() => {})
63
+ const sendRequestSpy = vi
64
+ .spyOn(client as never, "sendRequest")
65
+ .mockResolvedValue({
66
+ type: "connectResponse",
67
+ requestId: "test",
68
+ success: true,
69
+ })
70
+
71
+ await client.connect({ agent: true })
72
+
73
+ expect(sendRequestSpy).toHaveBeenCalledWith(
74
+ expect.objectContaining({
75
+ type: "connect",
76
+ agent: true,
77
+ }),
78
+ )
79
+ })
80
+
81
+ it("should throw when popup fails to open", async () => {
82
+ vi.spyOn(client, "ensureReady").mockImplementation(() => {})
83
+ vi.spyOn(client as never, "sendRequest").mockResolvedValue({
84
+ type: "connectResponse",
85
+ requestId: "test",
86
+ success: false,
87
+ })
88
+
89
+ await expect(client.connect()).rejects.toThrow(
90
+ "Failed to open authentication popup",
91
+ )
92
+ })
93
+
94
+ it("should throw error if client is not initialized", async () => {
95
+ await expect(client.connect()).rejects.toThrow(
96
+ "SwarmIdClient not initialized. Call initialize() first.",
97
+ )
98
+ })
99
+ })