@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,274 @@
1
+ /**
2
+ * Test utilities for epoch feeds
3
+ *
4
+ * Provides mock storage and helpers for testing epoch feed operations
5
+ */
6
+
7
+ import { Binary } from "cafe-utility"
8
+ import { PrivateKey, Topic, EthAddress } from "@ethersphere/bee-js"
9
+
10
+ /**
11
+ * In-memory chunk storage for testing
12
+ */
13
+ export class MockChunkStore {
14
+ private chunks = new Map<string, Uint8Array>()
15
+
16
+ async put(address: string, data: Uint8Array): Promise<void> {
17
+ this.chunks.set(address.toLowerCase(), data)
18
+ }
19
+
20
+ async get(address: string): Promise<Uint8Array> {
21
+ const data = this.chunks.get(address.toLowerCase())
22
+ if (!data) {
23
+ throw new Error(`Chunk not found: ${address}`)
24
+ }
25
+ return data
26
+ }
27
+
28
+ has(address: string): boolean {
29
+ return this.chunks.has(address.toLowerCase())
30
+ }
31
+
32
+ clear(): void {
33
+ this.chunks.clear()
34
+ }
35
+
36
+ size(): number {
37
+ return this.chunks.size
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Mock Bee instance for testing
43
+ */
44
+ export class MockBee {
45
+ public readonly url = "http://localhost:1633"
46
+ private store: MockChunkStore
47
+ private tagCounter = 0
48
+
49
+ constructor(store?: MockChunkStore) {
50
+ this.store = store || new MockChunkStore()
51
+ }
52
+
53
+ async createTag(): Promise<{
54
+ uid: number
55
+ split: number
56
+ seen: number
57
+ stored: number
58
+ sent: number
59
+ synced: number
60
+ address: string
61
+ startedAt: string
62
+ }> {
63
+ this.tagCounter++
64
+ return {
65
+ uid: this.tagCounter,
66
+ split: 0,
67
+ seen: 0,
68
+ stored: 0,
69
+ sent: 0,
70
+ synced: 0,
71
+ address: "",
72
+ startedAt: new Date().toISOString(),
73
+ }
74
+ }
75
+
76
+ async downloadChunk(reference: string): Promise<Uint8Array> {
77
+ return this.store.get(reference)
78
+ }
79
+
80
+ async uploadChunk(
81
+ data: Uint8Array,
82
+ _postageBatchId: string,
83
+ ): Promise<{ reference: string }> {
84
+ const address = Binary.keccak256(data)
85
+ const reference = Binary.uint8ArrayToHex(address)
86
+ await this.store.put(reference, data)
87
+ return { reference }
88
+ }
89
+
90
+ getStore(): MockChunkStore {
91
+ return this.store
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Create test signer with a deterministic private key
97
+ */
98
+ export function createTestSigner(): PrivateKey {
99
+ // Use a fixed private key for deterministic tests
100
+ const privateKeyHex =
101
+ "634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd"
102
+ return new PrivateKey(privateKeyHex)
103
+ }
104
+
105
+ /**
106
+ * Create test topic
107
+ */
108
+ export function createTestTopic(name: string = "testtopic"): Topic {
109
+ const encoder = new TextEncoder()
110
+ const hash = Binary.keccak256(encoder.encode(name))
111
+ return new Topic(hash)
112
+ }
113
+
114
+ /**
115
+ * Create test owner address
116
+ */
117
+ export function createTestOwner(): EthAddress {
118
+ return createTestSigner().publicKey().address()
119
+ }
120
+
121
+ /**
122
+ * Create test reference (32 bytes)
123
+ */
124
+ export function createTestReference(value: number | bigint): Uint8Array {
125
+ const ref = new Uint8Array(32)
126
+ const view = new DataView(ref.buffer)
127
+ view.setBigUint64(0, BigInt(value), false) // big-endian
128
+ return ref
129
+ }
130
+
131
+ /**
132
+ * Create test payload with timestamp
133
+ */
134
+ export function createTestPayload(at: bigint): Uint8Array {
135
+ const payload = new Uint8Array(8)
136
+ const view = new DataView(payload.buffer)
137
+ view.setBigUint64(0, at, false) // big-endian
138
+ return payload
139
+ }
140
+
141
+ const BATCH_ID_LENGTH = 32
142
+ const INDEX_LENGTH = 8
143
+ const TIMESTAMP_LENGTH = 8
144
+ const SIGNATURE_LENGTH = 65
145
+ const ISSUER_LENGTH = 20
146
+ const SOC_IDENTIFIER_LENGTH = 32
147
+
148
+ /**
149
+ * Create a mock Stamper for testing
150
+ *
151
+ * Returns an object with a stamp() method that produces a valid-looking
152
+ * EnvelopeWithBatchId without performing real cryptographic operations.
153
+ */
154
+ export function createMockStamper() {
155
+ return {
156
+ stamp(_chunk: any) {
157
+ return {
158
+ batchId: {
159
+ toUint8Array: () => new Uint8Array(BATCH_ID_LENGTH),
160
+ },
161
+ index: new Uint8Array(INDEX_LENGTH),
162
+ timestamp: new Uint8Array(TIMESTAMP_LENGTH),
163
+ signature: new Uint8Array(SIGNATURE_LENGTH),
164
+ issuer: new Uint8Array(ISSUER_LENGTH),
165
+ }
166
+ },
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Mock fetch for SOC and chunk uploads
172
+ *
173
+ * Intercepts fetch calls to /soc/ and /chunks endpoints, storing data
174
+ * in the provided MockChunkStore.
175
+ *
176
+ * @param store - Mock chunk store for persisting uploaded data
177
+ * @param owner - Owner address for computing SOC addresses on /chunks uploads
178
+ */
179
+ export function mockFetch(store?: MockChunkStore, owner?: EthAddress): void {
180
+ const originalFetch = global.fetch
181
+
182
+ global.fetch = async (url: string | URL | Request, init?: RequestInit) => {
183
+ // Parse URL
184
+ const urlStr = typeof url === "string" ? url : url.toString()
185
+
186
+ // Check if it's a SOC upload
187
+ if (urlStr.includes("/soc/")) {
188
+ // Extract body data and SOC info from URL
189
+ const body = init?.body as Uint8Array
190
+ if (!body) {
191
+ throw new Error("Missing body in SOC upload")
192
+ }
193
+
194
+ // Parse URL: /soc/{owner}/{id}?sig={signature}
195
+ const parts = urlStr.split("/soc/")[1].split("/")
196
+ const socOwner = parts[0]
197
+ const idAndSig = parts[1].split("?sig=")
198
+ const identifier = idAndSig[0]
199
+
200
+ // Calculate SOC address: Keccak256(identifier || owner)
201
+ const identifierBytes = Binary.hexToUint8Array(identifier)
202
+ const ownerBytes = Binary.hexToUint8Array(socOwner)
203
+ const socAddress = Binary.keccak256(
204
+ Binary.concatBytes(identifierBytes, ownerBytes),
205
+ )
206
+ const reference = Binary.uint8ArrayToHex(socAddress)
207
+
208
+ // Build full SOC chunk data (identifier + signature + span + payload)
209
+ // For mock purposes, we need to reconstruct the full SOC
210
+ const signatureHex = idAndSig[1]
211
+ const signatureBytes = Binary.hexToUint8Array(signatureHex)
212
+ const fullSOCData = Binary.concatBytes(
213
+ identifierBytes,
214
+ signatureBytes,
215
+ body, // This is span + payload
216
+ )
217
+
218
+ // Store in mock store if provided
219
+ if (store) {
220
+ await store.put(reference, fullSOCData)
221
+ }
222
+
223
+ return new Response(JSON.stringify({ reference }), {
224
+ status: 200,
225
+ headers: { "Content-Type": "application/json" },
226
+ })
227
+ }
228
+
229
+ // Check if it's a chunk upload (used by uploadChunkWithFetch)
230
+ if (urlStr.includes("/chunks") && init?.method === "POST") {
231
+ const body = init?.body as Uint8Array
232
+ if (!body) {
233
+ throw new Error("Missing body in chunk upload")
234
+ }
235
+
236
+ // SOC data starts with identifier (32 bytes)
237
+ // Compute SOC address: Keccak256(identifier + owner)
238
+ if (store && owner) {
239
+ const identifier = body.slice(0, SOC_IDENTIFIER_LENGTH)
240
+ const socAddress = Binary.keccak256(
241
+ Binary.concatBytes(identifier, owner.toUint8Array()),
242
+ )
243
+ const reference = Binary.uint8ArrayToHex(socAddress)
244
+ await store.put(reference, body)
245
+ return new Response(JSON.stringify({ reference }), {
246
+ status: 200,
247
+ headers: { "Content-Type": "application/json" },
248
+ })
249
+ }
250
+
251
+ // Fallback: store by content hash
252
+ const address = Binary.keccak256(body)
253
+ const reference = Binary.uint8ArrayToHex(address)
254
+ if (store) {
255
+ await store.put(reference, body)
256
+ }
257
+ return new Response(JSON.stringify({ reference }), {
258
+ status: 200,
259
+ headers: { "Content-Type": "application/json" },
260
+ })
261
+ }
262
+
263
+ // Fall back to original fetch for other URLs
264
+ return originalFetch(url, init)
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Restore original fetch
270
+ */
271
+ export function restoreFetch(): void {
272
+ // This is a simplified version - in production you'd want to properly restore
273
+ // For now, tests should use the mocked version throughout
274
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Types for Epoch-Based Feeds
3
+ */
4
+
5
+ import type {
6
+ Bee,
7
+ EthAddress,
8
+ Topic,
9
+ PrivateKey,
10
+ Stamper,
11
+ } from "@ethersphere/bee-js"
12
+ import type { EpochIndex } from "./epoch"
13
+
14
+ /**
15
+ * Options for creating an epoch feed reader
16
+ */
17
+ export interface EpochFeedOptions {
18
+ /** Bee instance for chunk operations */
19
+ bee: Bee
20
+
21
+ /** Feed topic (32 bytes) */
22
+ topic: Topic
23
+
24
+ /** Feed owner address */
25
+ owner: EthAddress
26
+
27
+ /** Optional encryption key for encrypted feed updates */
28
+ encryptionKey?: Uint8Array
29
+ }
30
+
31
+ /**
32
+ * Options for creating an epoch feed writer
33
+ */
34
+ export interface EpochFeedWriterOptions extends EpochFeedOptions {
35
+ /** Private key for signing chunks */
36
+ signer: PrivateKey
37
+ }
38
+
39
+ /**
40
+ * Result from an epoch feed lookup
41
+ */
42
+ export interface EpochLookupResult {
43
+ /** Swarm reference (32 bytes) */
44
+ reference: Uint8Array
45
+
46
+ /** Epoch where the update was found */
47
+ epoch: EpochIndex
48
+
49
+ /** Timestamp of the update */
50
+ timestamp: bigint
51
+ }
52
+
53
+ /**
54
+ * Hints for calculating the next epoch in a stateless manner.
55
+ * Callers should store these after each update and pass them to subsequent updates.
56
+ */
57
+ export interface EpochUpdateHints {
58
+ /** Previous epoch (for calculating next) */
59
+ lastEpoch?: { start: bigint; level: number }
60
+ /** Timestamp of last update */
61
+ lastTimestamp?: bigint
62
+ }
63
+
64
+ /**
65
+ * Result from an epoch feed update, including epoch info for subsequent updates.
66
+ */
67
+ export interface EpochUpdateResult {
68
+ /** SOC address of the uploaded chunk */
69
+ socAddress: Uint8Array
70
+ /** Epoch used for this update (for caller to store as hint) */
71
+ epoch: { start: bigint; level: number }
72
+ /** Timestamp used (for caller to store as hint) */
73
+ timestamp: bigint
74
+ }
75
+
76
+ /**
77
+ * Interface for epoch feed finders (readers)
78
+ *
79
+ * Implementations: SyncEpochFinder, AsyncEpochFinder
80
+ */
81
+ export interface EpochFinder {
82
+ /**
83
+ * Find the feed update valid at time `at`
84
+ *
85
+ * @param at - Target unix timestamp (seconds)
86
+ * @param after - Hint of latest known update timestamp (0 if unknown)
87
+ * @returns 32 or 64-byte Swarm reference, or undefined if no update found
88
+ */
89
+ findAt(at: bigint, after?: bigint): Promise<Uint8Array | undefined>
90
+ }
91
+
92
+ /**
93
+ * Interface for epoch feed updaters (writers)
94
+ *
95
+ * Implementation: BasicEpochUpdater
96
+ *
97
+ * Implements Bee-compatible stateless epoch calculation.
98
+ * Each update uses hints from the previous update to calculate the next epoch,
99
+ * creating a proper epoch tree that Bee's finder can traverse.
100
+ */
101
+ export interface EpochUpdater {
102
+ /**
103
+ * Update feed with a reference at given timestamp
104
+ *
105
+ * Calculates the appropriate epoch based on hints:
106
+ * - First update (no hints): uses root epoch (level 32, start 0)
107
+ * - Subsequent updates: calculates next epoch using LCA-based algorithm
108
+ *
109
+ * @param at - Unix timestamp for this update (seconds)
110
+ * @param reference - 32 or 64-byte Swarm reference to store
111
+ * @param stamper - Stamper object for stamping
112
+ * @param encryptionKey - Optional encryption key for the update
113
+ * @param hints - Optional hints from previous update for calculating epoch
114
+ * @returns Update result with SOC address and epoch info for next update
115
+ */
116
+ update(
117
+ at: bigint,
118
+ reference: Uint8Array,
119
+ stamper: Stamper,
120
+ encryptionKey?: Uint8Array,
121
+ hints?: EpochUpdateHints,
122
+ ): Promise<EpochUpdateResult>
123
+
124
+ /**
125
+ * Get the owner address (derived from signer)
126
+ */
127
+ getOwner(): EthAddress
128
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Basic Epoch Feed Updater
3
+ *
4
+ * Handles writing updates to epoch-based feeds by calculating the next
5
+ * epoch and uploading chunks.
6
+ */
7
+
8
+ import { Binary } from "cafe-utility"
9
+ import type { Bee, Stamper } from "@ethersphere/bee-js"
10
+ import { EthAddress, Topic, PrivateKey, Identifier } from "@ethersphere/bee-js"
11
+ import { EpochIndex, MAX_LEVEL } from "./epoch"
12
+ import {
13
+ uploadEncryptedSOC,
14
+ uploadSOCViaSocEndpoint,
15
+ } from "../../upload-encrypted-data"
16
+ import type { EpochUpdater, EpochUpdateHints, EpochUpdateResult } from "./types"
17
+ import { AsyncEpochFinder } from "./async-finder"
18
+
19
+ /**
20
+ * Basic updater for epoch-based feeds
21
+ *
22
+ * Implements Bee-compatible stateless epoch calculation.
23
+ * Each update uses hints from the previous update to calculate the next epoch,
24
+ * creating a proper epoch tree that Bee's finder can traverse.
25
+ *
26
+ * - First update (no hints): uses root epoch (level 32, start 0)
27
+ * - Subsequent updates: calculates next epoch using the standard algorithm:
28
+ * - If new timestamp within previous epoch's range: dive to child
29
+ * - Else: jump to LCA(newTimestamp, prevTimestamp) and dive to child
30
+ *
31
+ * Implements the EpochUpdater interface.
32
+ */
33
+ export class BasicEpochUpdater implements EpochUpdater {
34
+ constructor(
35
+ private readonly bee: Bee,
36
+ private readonly topic: Topic,
37
+ private readonly signer: PrivateKey,
38
+ ) {}
39
+
40
+ /**
41
+ * Update feed with a reference at given timestamp
42
+ *
43
+ * @param at - Unix timestamp for this update (seconds)
44
+ * @param reference - 32 or 64-byte Swarm reference to store
45
+ * @param stamper - Stamper object for stamping
46
+ * @param encryptionKey - Optional encryption key for the update
47
+ * @param hints - Optional hints from previous update for calculating epoch
48
+ * @returns Update result with SOC address and epoch info for next update
49
+ */
50
+ async update(
51
+ at: bigint,
52
+ reference: Uint8Array,
53
+ stamper: Stamper,
54
+ encryptionKey?: Uint8Array,
55
+ hints?: EpochUpdateHints,
56
+ ): Promise<EpochUpdateResult> {
57
+ if (reference.length !== 32 && reference.length !== 64) {
58
+ throw new Error(
59
+ `Reference must be 32 or 64 bytes, got ${reference.length}`,
60
+ )
61
+ }
62
+
63
+ // Calculate epoch - auto-lookup if no hints provided
64
+ const epoch = await this.calculateEpoch(at, hints, encryptionKey)
65
+
66
+ // Upload the chunk with timestamp + reference
67
+ const socAddress = await this.uploadEpochChunk(
68
+ epoch,
69
+ at,
70
+ reference,
71
+ stamper,
72
+ encryptionKey,
73
+ )
74
+
75
+ // Return result with hints for next update
76
+ return {
77
+ socAddress,
78
+ epoch: { start: epoch.start, level: epoch.level },
79
+ timestamp: at,
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Calculate the epoch for this update based on hints or auto-lookup
85
+ *
86
+ * When hints are provided, uses them for fast epoch calculation.
87
+ * When no hints are provided, looks up the current feed state to calculate
88
+ * the next epoch, preventing overwrites of previous updates.
89
+ *
90
+ * @param at - Timestamp for this update
91
+ * @param hints - Optional hints from previous update
92
+ * @param encryptionKey - Optional encryption key for looking up encrypted feeds
93
+ * @returns Epoch to use for this update
94
+ */
95
+ private async calculateEpoch(
96
+ at: bigint,
97
+ hints?: EpochUpdateHints,
98
+ encryptionKey?: Uint8Array,
99
+ ): Promise<EpochIndex> {
100
+ // Fast path: use provided hints
101
+ if (hints?.lastEpoch && hints.lastTimestamp !== undefined) {
102
+ const prevEpoch = new EpochIndex(
103
+ hints.lastEpoch.start,
104
+ hints.lastEpoch.level,
105
+ )
106
+ return prevEpoch.next(hints.lastTimestamp, at)
107
+ }
108
+
109
+ // Slow path: lookup current state
110
+ const owner = this.signer.publicKey().address()
111
+ const finder = new AsyncEpochFinder(
112
+ this.bee,
113
+ this.topic,
114
+ owner,
115
+ encryptionKey,
116
+ )
117
+
118
+ // Use findAtWithMetadata to get both reference AND epoch info
119
+ const current = await finder.findAtWithMetadata(at)
120
+
121
+ if (!current) {
122
+ // First update ever - use root epoch
123
+ return new EpochIndex(0n, MAX_LEVEL)
124
+ }
125
+
126
+ // Calculate next epoch based on found state
127
+ return current.epoch.next(current.timestamp, at)
128
+ }
129
+
130
+ /**
131
+ * Get the owner address (derived from signer)
132
+ */
133
+ getOwner(): EthAddress {
134
+ return this.signer.publicKey().address()
135
+ }
136
+
137
+ /**
138
+ * Upload a chunk for a specific epoch
139
+ *
140
+ * @param epoch - Epoch to upload to
141
+ * @param at - Timestamp of this update
142
+ * @param reference - 32 or 64-byte reference to store
143
+ * @param stamper - Stamper object for stamping
144
+ * @returns SOC chunk address for utilization tracking
145
+ */
146
+ private async uploadEpochChunk(
147
+ epoch: EpochIndex,
148
+ at: bigint,
149
+ reference: Uint8Array,
150
+ stamper: Stamper,
151
+ encryptionKey?: Uint8Array,
152
+ ): Promise<Uint8Array> {
153
+ // Calculate epoch identifier: Keccak256(topic || Keccak256(start || level))
154
+ const epochHash = await epoch.marshalBinary()
155
+ const identifier = new Identifier(
156
+ Binary.keccak256(
157
+ Binary.concatBytes(this.topic.toUint8Array(), epochHash),
158
+ ),
159
+ )
160
+
161
+ // Timestamp: 8-byte big-endian
162
+ const timestamp = new Uint8Array(8)
163
+ const tsView = new DataView(timestamp.buffer)
164
+ tsView.setBigUint64(0, at, false) // big-endian
165
+
166
+ // Payload: timestamp + reference = 40 or 72 bytes
167
+ // The upload function will wrap this in a CAC (adding span) to get 48 or 80 bytes
168
+ // which is the v1 format Bee expects for /bzz/ compatibility
169
+ const payload = Binary.concatBytes(timestamp, reference)
170
+
171
+ const result = encryptionKey
172
+ ? await uploadEncryptedSOC(
173
+ this.bee,
174
+ stamper,
175
+ this.signer,
176
+ identifier,
177
+ payload,
178
+ encryptionKey,
179
+ { deferred: false }, // deferred setting is NOT the cause of /bzz/ timeout
180
+ )
181
+ : await uploadSOCViaSocEndpoint(
182
+ this.bee,
183
+ stamper,
184
+ this.signer,
185
+ identifier,
186
+ payload,
187
+ { deferred: false }, // deferred setting is NOT the cause of /bzz/ timeout
188
+ )
189
+
190
+ return result.socAddress
191
+ }
192
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared utilities for epoch feed finders
3
+ */
4
+
5
+ import { EpochIndex } from "./epoch"
6
+
7
+ const MAX_LEAF_BACKSCAN = 128n
8
+
9
+ /**
10
+ * Type for the chunk fetcher callback used by findPreviousLeaf.
11
+ * This allows both sync and async finders to share the same logic.
12
+ */
13
+ export type EpochChunkFetcher = (
14
+ at: bigint,
15
+ epoch: EpochIndex,
16
+ ) => Promise<Uint8Array | undefined>
17
+
18
+ /**
19
+ * Find a previous leaf chunk by scanning backwards from the target timestamp.
20
+ *
21
+ * This is a recovery fallback for poisoned-ancestor histories where the tree
22
+ * traversal fails but valid leaf nodes exist.
23
+ *
24
+ * @param at - Target unix timestamp (seconds)
25
+ * @param after - Hint of latest known update timestamp (0 if unknown)
26
+ * @param getEpochChunk - Callback function to fetch epoch chunks
27
+ * @returns 32-byte Swarm reference, or undefined if no update found
28
+ */
29
+ export async function findPreviousLeaf(
30
+ at: bigint,
31
+ after: bigint,
32
+ getEpochChunk: EpochChunkFetcher,
33
+ ): Promise<Uint8Array | undefined> {
34
+ if (at === 0n) {
35
+ return undefined
36
+ }
37
+
38
+ const minAt = after > 0n ? after : 0n
39
+ const lowerBound =
40
+ at > MAX_LEAF_BACKSCAN && at - MAX_LEAF_BACKSCAN > minAt
41
+ ? at - MAX_LEAF_BACKSCAN
42
+ : minAt
43
+
44
+ let probe = at - 1n
45
+ while (probe >= lowerBound) {
46
+ try {
47
+ const leaf = await getEpochChunk(at, new EpochIndex(probe, 0))
48
+ if (leaf) {
49
+ return leaf
50
+ }
51
+ } catch {
52
+ // Missing leaf at probe timestamp.
53
+ }
54
+
55
+ if (probe === 0n) {
56
+ break
57
+ }
58
+ probe--
59
+ }
60
+
61
+ return undefined
62
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Feed utilities and implementations
3
+ */
4
+
5
+ export * from "./epochs"
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Async Sequential Feed Finder
3
+ *
4
+ * Currently delegates to the sync finder to match Go behavior
5
+ * while keeping an async interface.
6
+ */
7
+
8
+ import type {
9
+ Bee,
10
+ EthAddress,
11
+ Topic,
12
+ BeeRequestOptions,
13
+ } from "@ethersphere/bee-js"
14
+ import type { SequentialFinder, SequentialLookupResult } from "./types"
15
+ import { SyncSequentialFinder } from "./finder"
16
+
17
+ export class AsyncSequentialFinder implements SequentialFinder {
18
+ private readonly syncFinder: SyncSequentialFinder
19
+
20
+ constructor(bee: Bee, topic: Topic, owner: EthAddress) {
21
+ this.syncFinder = new SyncSequentialFinder(bee, topic, owner)
22
+ }
23
+
24
+ async findAt(
25
+ at: bigint,
26
+ after: bigint = 0n,
27
+ requestOptions?: BeeRequestOptions,
28
+ ): Promise<SequentialLookupResult> {
29
+ return this.syncFinder.findAt(at, after, requestOptions)
30
+ }
31
+ }