@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,174 @@
1
+ import { MantarayNode, NULL_ADDRESS } from "@ethersphere/bee-js"
2
+ import type {
3
+ Bee,
4
+ Stamper,
5
+ UploadOptions,
6
+ BeeRequestOptions,
7
+ } from "@ethersphere/bee-js"
8
+ import {
9
+ makeEncryptedContentAddressedChunk,
10
+ makeContentAddressedChunk,
11
+ } from "../chunk"
12
+ import { uint8ArrayToHex } from "../utils/hex"
13
+ import { uploadSingleChunk } from "./upload-data"
14
+ import { uploadSingleEncryptedChunk } from "./upload-encrypted-data"
15
+
16
+ /**
17
+ * Options for creating a feed manifest
18
+ */
19
+ export interface CreateFeedManifestOptions {
20
+ /**
21
+ * Whether to encrypt the manifest.
22
+ * Default: true (encrypted)
23
+ */
24
+ encrypt?: boolean
25
+ /**
26
+ * Feed type: "Sequence" for sequential feeds, "Epoch" for epoch feeds.
27
+ * Default: "Sequence"
28
+ */
29
+ feedType?: "Sequence" | "Epoch"
30
+ }
31
+
32
+ /**
33
+ * Result of creating a feed manifest
34
+ */
35
+ export interface CreateFeedManifestResult {
36
+ /**
37
+ * Reference to the feed manifest.
38
+ * - Encrypted: 128 hex chars (64 bytes = address + encryption key)
39
+ * - Unencrypted: 64 hex chars (32 bytes = address)
40
+ */
41
+ reference: string
42
+ /**
43
+ * Tag UID if a tag was created during upload
44
+ */
45
+ tagUid?: number
46
+ }
47
+
48
+ /**
49
+ * Create a feed manifest directly using chunk upload
50
+ *
51
+ * Instead of using bee.createFeedManifest() which calls the /feeds endpoint,
52
+ * this function builds the manifest locally as a MantarayNode and uploads it
53
+ * via the encrypted chunk endpoint (or plain chunk endpoint if encrypt=false).
54
+ *
55
+ * Feed manifests have a single "/" path with metadata:
56
+ * - swarm-feed-owner: Owner's ethereum address (40 hex chars, no 0x)
57
+ * - swarm-feed-topic: Topic hash (64 hex chars)
58
+ * - swarm-feed-type: "Sequence" for sequential feeds
59
+ *
60
+ * IMPORTANT: This function implements client-side "saveRecursively" logic:
61
+ * 1. Upload the "/" child node first and get its address
62
+ * 2. Set the child's selfAddress to the uploaded address
63
+ * 3. Then upload the root node (which references the child's address)
64
+ *
65
+ * Without this, Bee's /bzz/ endpoint returns 404 because the "/" child chunk
66
+ * doesn't exist (only its calculated hash was stored in the root manifest).
67
+ *
68
+ * @param bee - Bee client instance
69
+ * @param stamper - Stamper for client-side signing
70
+ * @param topic - Topic hex string (64 chars)
71
+ * @param owner - Owner hex string (40 chars, no 0x prefix)
72
+ * @param options - Options for creating the manifest (encrypt, etc.)
73
+ * @param uploadOptions - Upload options (tag, deferred, etc.)
74
+ * @param requestOptions - Bee request options
75
+ * @returns Reference to the feed manifest
76
+ */
77
+ export async function createFeedManifestDirect(
78
+ bee: Bee,
79
+ stamper: Stamper,
80
+ topic: string,
81
+ owner: string,
82
+ options?: CreateFeedManifestOptions,
83
+ uploadOptions?: UploadOptions,
84
+ requestOptions?: BeeRequestOptions,
85
+ ): Promise<CreateFeedManifestResult> {
86
+ // Normalize owner (remove 0x prefix if present)
87
+ const normalizedOwner = owner.startsWith("0x") ? owner.slice(2) : owner
88
+
89
+ // DEBUG: Log what Bee will use for feed lookup
90
+
91
+ // 1. Create tag for upload if not provided
92
+ let tag = uploadOptions?.tag
93
+ if (!tag) {
94
+ const tagResponse = await bee.createTag()
95
+ tag = tagResponse.uid
96
+ }
97
+ const uploadOptionsWithTag = { ...uploadOptions, tag }
98
+
99
+ // 2. Create root MantarayNode with "/" fork containing feed metadata
100
+ const rootNode = new MantarayNode()
101
+ rootNode.addFork("/", NULL_ADDRESS, {
102
+ "swarm-feed-owner": normalizedOwner,
103
+ "swarm-feed-topic": topic,
104
+ "swarm-feed-type": options?.feedType ?? "Sequence",
105
+ })
106
+
107
+ // 3. Get the "/" child node (addFork created it, we need to access it)
108
+ // 47 is ASCII code for '/'
109
+ const slashFork = rootNode.forks.get(47)
110
+ if (!slashFork) {
111
+ throw new Error("[FeedManifest] Failed to create '/' fork")
112
+ }
113
+ const slashNode = slashFork.node
114
+
115
+ // 4. Marshal and upload the "/" child node FIRST (saveRecursively pattern)
116
+ // This is critical: Bee's /bzz/ needs this chunk to exist
117
+ const slashNodeData = await slashNode.marshal()
118
+ const slashChunk = makeContentAddressedChunk(slashNodeData)
119
+
120
+ await uploadSingleChunk(
121
+ bee,
122
+ stamper,
123
+ slashChunk,
124
+ uploadOptionsWithTag,
125
+ requestOptions,
126
+ )
127
+
128
+ // 5. Set the child's selfAddress to the uploaded chunk address
129
+ // This is used when marshaling the root node
130
+ slashNode.selfAddress = slashChunk.address.toUint8Array()
131
+
132
+ // 6. Now marshal and upload the root node
133
+ const rootNodeData = await rootNode.marshal()
134
+
135
+ // 7. Encrypt and upload OR upload directly
136
+ const shouldEncrypt = options?.encrypt !== false
137
+
138
+ if (shouldEncrypt) {
139
+ // Encrypted upload for root
140
+ const encryptedChunk = makeEncryptedContentAddressedChunk(rootNodeData)
141
+
142
+ await uploadSingleEncryptedChunk(
143
+ bee,
144
+ stamper,
145
+ encryptedChunk,
146
+ uploadOptionsWithTag,
147
+ requestOptions,
148
+ )
149
+
150
+ // Return 64-byte reference (address + key)
151
+ const ref = new Uint8Array(64)
152
+ ref.set(encryptedChunk.address.toUint8Array(), 0)
153
+ ref.set(encryptedChunk.encryptionKey, 32)
154
+ const reference = uint8ArrayToHex(ref)
155
+
156
+ return { reference, tagUid: tag }
157
+ } else {
158
+ // Unencrypted upload for root
159
+ const rootChunk = makeContentAddressedChunk(rootNodeData)
160
+
161
+ await uploadSingleChunk(
162
+ bee,
163
+ stamper,
164
+ rootChunk,
165
+ uploadOptionsWithTag,
166
+ requestOptions,
167
+ )
168
+
169
+ // Return 32-byte reference
170
+ const reference = rootChunk.address.toHex()
171
+
172
+ return { reference, tagUid: tag }
173
+ }
174
+ }
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Async Epoch Feed Finder (Concurrent)
3
+ *
4
+ * Concurrent implementation for finding feed updates at specific timestamps
5
+ * using epoch-based indexing with parallel chunk fetching.
6
+ */
7
+
8
+ import { Binary } from "cafe-utility"
9
+ import type { Bee, BeeRequestOptions } from "@ethersphere/bee-js"
10
+ import { EthAddress, Reference, Topic } from "@ethersphere/bee-js"
11
+ import { EpochIndex, MAX_LEVEL } from "./epoch"
12
+ import type { EpochFinder, EpochLookupResult } from "./types"
13
+ import { downloadEncryptedSOC } from "../../download-data"
14
+ import { findPreviousLeaf } from "./utils"
15
+
16
+ const EPOCH_LOOKUP_TIMEOUT_MS = 2000
17
+
18
+ /**
19
+ * Async concurrent finder for epoch-based feeds
20
+ *
21
+ * Launches parallel chunk fetches along the epoch tree path
22
+ * to find the feed update valid at a specific timestamp.
23
+ *
24
+ * Implements the EpochFinder interface.
25
+ */
26
+ export class AsyncEpochFinder implements EpochFinder {
27
+ constructor(
28
+ private readonly bee: Bee,
29
+ private readonly topic: Topic,
30
+ private readonly owner: EthAddress,
31
+ private readonly encryptionKey?: Uint8Array,
32
+ ) {}
33
+
34
+ /**
35
+ * Find the feed update valid at time `at`
36
+ * @param at - Target unix timestamp (seconds)
37
+ * @param after - Hint of latest known update timestamp (0 if unknown)
38
+ * @returns 32-byte Swarm reference, or undefined if no update found
39
+ */
40
+ async findAt(
41
+ at: bigint,
42
+ after: bigint = 0n,
43
+ ): Promise<Uint8Array | undefined> {
44
+ // Fast path: exact timestamp updates are written at level-0 and should be
45
+ // retrievable without traversing potentially poisoned ancestors.
46
+ const exactEpoch = new EpochIndex(at, 0)
47
+ try {
48
+ const exact = await this.getEpochChunk(at, exactEpoch)
49
+ if (exact) {
50
+ return exact
51
+ }
52
+ } catch {
53
+ // Ignore and fall back to tree traversal.
54
+ }
55
+
56
+ // Start from top epoch and traverse down
57
+ const traversed = await this.findAtEpoch(
58
+ at,
59
+ new EpochIndex(0n, MAX_LEVEL),
60
+ undefined,
61
+ )
62
+ if (traversed) {
63
+ return traversed
64
+ }
65
+
66
+ // Recovery fallback for poisoned-ancestor histories: only enable bounded
67
+ // leaf back-scan when root epoch exists but is invalid for `at`.
68
+ try {
69
+ const rootProbe = await this.getEpochChunk(
70
+ at,
71
+ new EpochIndex(0n, MAX_LEVEL),
72
+ )
73
+ if (rootProbe === undefined) {
74
+ return this.findPreviousLeaf(at, after)
75
+ }
76
+ } catch {
77
+ // Root missing - no evidence of poisoned ancestors.
78
+ }
79
+
80
+ return undefined
81
+ }
82
+
83
+ /**
84
+ * Recursively find update at epoch, with parallel fetching
85
+ *
86
+ * @param at - Target timestamp
87
+ * @param epoch - Current epoch to check
88
+ * @param currentBest - Best result found so far
89
+ * @returns Reference if found, undefined otherwise
90
+ */
91
+ private async findAtEpoch(
92
+ at: bigint,
93
+ epoch: EpochIndex,
94
+ currentBest: Uint8Array | undefined,
95
+ ): Promise<Uint8Array | undefined> {
96
+ // Try to get chunk at this epoch.
97
+ // getEpochChunk throws when chunk is missing, returns undefined when
98
+ // chunk exists but timestamp is invalid for `at`.
99
+ let chunk: Uint8Array | undefined
100
+ try {
101
+ chunk = await this.getEpochChunk(at, epoch)
102
+ } catch (error) {
103
+ // Chunk missing at this epoch.
104
+ if (epoch.isLeft()) {
105
+ return currentBest
106
+ }
107
+ return this.findAtEpoch(epoch.start - 1n, epoch.left(), currentBest)
108
+ }
109
+
110
+ // If chunk found and valid
111
+ if (chunk) {
112
+ // If at finest resolution, this is our answer
113
+ if (epoch.level === 0) {
114
+ return chunk
115
+ }
116
+
117
+ // Continue to finer resolution
118
+ return this.findAtEpoch(at, epoch.childAt(at), chunk)
119
+ }
120
+
121
+ // Chunk exists but timestamp invalid.
122
+ // Keep descending towards the target epoch first, because finer epochs
123
+ // may still contain a valid update.
124
+ if (epoch.level > 0) {
125
+ const down = await this.findAtEpoch(at, epoch.childAt(at), currentBest)
126
+ if (down) {
127
+ return down
128
+ }
129
+ }
130
+
131
+ if (epoch.isLeft()) {
132
+ // Left child - return best we have so far
133
+ return currentBest
134
+ }
135
+
136
+ // Right child - need to search left sibling branch
137
+ return this.findAtEpoch(epoch.start - 1n, epoch.left(), currentBest)
138
+ }
139
+ /**
140
+ * Fetch chunk for a specific epoch
141
+ *
142
+ * @param at - Target timestamp for validation
143
+ * @param epoch - Epoch to fetch
144
+ * @returns Chunk payload
145
+ * @throws Error if chunk not found
146
+ */
147
+ private async getEpochChunk(
148
+ at: bigint,
149
+ epoch: EpochIndex,
150
+ ): Promise<Uint8Array | undefined> {
151
+ const result = await this.getEpochChunkWithMetadata(at, epoch)
152
+ return result?.reference
153
+ }
154
+
155
+ /**
156
+ * Find the feed update valid at time `at` with full metadata
157
+ * Used by updater to calculate next epoch when no hints provided
158
+ *
159
+ * @param at - Target unix timestamp (seconds)
160
+ * @returns EpochLookupResult with reference, epoch, and timestamp, or undefined if no update found
161
+ */
162
+ async findAtWithMetadata(at: bigint): Promise<EpochLookupResult | undefined> {
163
+ // Fast path: exact timestamp updates are written at level-0
164
+ const exactEpoch = new EpochIndex(at, 0)
165
+ try {
166
+ const exact = await this.getEpochChunkWithMetadata(at, exactEpoch)
167
+ if (exact) {
168
+ return {
169
+ reference: exact.reference,
170
+ epoch: exactEpoch,
171
+ timestamp: exact.timestamp,
172
+ }
173
+ }
174
+ } catch {
175
+ // Ignore and fall back to tree traversal
176
+ }
177
+
178
+ // Start from top epoch and traverse down, tracking found epoch
179
+ return this.findAtEpochWithMetadata(
180
+ at,
181
+ new EpochIndex(0n, MAX_LEVEL),
182
+ undefined,
183
+ )
184
+ }
185
+
186
+ /**
187
+ * Recursively find update at epoch with full metadata tracking
188
+ *
189
+ * @param at - Target timestamp
190
+ * @param epoch - Current epoch to check
191
+ * @param currentBest - Best result found so far
192
+ * @returns EpochLookupResult if found, undefined otherwise
193
+ */
194
+ private async findAtEpochWithMetadata(
195
+ at: bigint,
196
+ epoch: EpochIndex,
197
+ currentBest: EpochLookupResult | undefined,
198
+ ): Promise<EpochLookupResult | undefined> {
199
+ // Try to get chunk at this epoch
200
+ let chunkData: { reference: Uint8Array; timestamp: bigint } | undefined
201
+ try {
202
+ chunkData = await this.getEpochChunkWithMetadata(at, epoch)
203
+ } catch (error) {
204
+ // Chunk missing at this epoch
205
+ if (epoch.isLeft()) {
206
+ return currentBest
207
+ }
208
+ return this.findAtEpochWithMetadata(
209
+ epoch.start - 1n,
210
+ epoch.left(),
211
+ currentBest,
212
+ )
213
+ }
214
+
215
+ // If chunk found and valid
216
+ if (chunkData) {
217
+ const result: EpochLookupResult = {
218
+ reference: chunkData.reference,
219
+ epoch,
220
+ timestamp: chunkData.timestamp,
221
+ }
222
+
223
+ // If at finest resolution, this is our answer
224
+ if (epoch.level === 0) {
225
+ return result
226
+ }
227
+
228
+ // Continue to finer resolution
229
+ return this.findAtEpochWithMetadata(at, epoch.childAt(at), result)
230
+ }
231
+
232
+ // Chunk exists but timestamp invalid
233
+ // Keep descending towards the target epoch first
234
+ if (epoch.level > 0) {
235
+ const down = await this.findAtEpochWithMetadata(
236
+ at,
237
+ epoch.childAt(at),
238
+ currentBest,
239
+ )
240
+ if (down) {
241
+ return down
242
+ }
243
+ }
244
+
245
+ if (epoch.isLeft()) {
246
+ return currentBest
247
+ }
248
+
249
+ // Right child - need to search left sibling branch
250
+ return this.findAtEpochWithMetadata(
251
+ epoch.start - 1n,
252
+ epoch.left(),
253
+ currentBest,
254
+ )
255
+ }
256
+
257
+ /**
258
+ * Fetch chunk for a specific epoch and return full metadata
259
+ *
260
+ * @param at - Target timestamp for validation
261
+ * @param epoch - Epoch to fetch
262
+ * @returns Object with reference and timestamp, or undefined if timestamp > at
263
+ * @throws Error if chunk not found
264
+ */
265
+ private async getEpochChunkWithMetadata(
266
+ at: bigint,
267
+ epoch: EpochIndex,
268
+ ): Promise<{ reference: Uint8Array; timestamp: bigint } | undefined> {
269
+ const requestOptions: BeeRequestOptions = {
270
+ timeout: EPOCH_LOOKUP_TIMEOUT_MS,
271
+ }
272
+ // Calculate epoch identifier: Keccak256(topic || Keccak256(start || level))
273
+ const epochHash = await epoch.marshalBinary()
274
+ const identifier = Binary.keccak256(
275
+ Binary.concatBytes(this.topic.toUint8Array(), epochHash),
276
+ )
277
+
278
+ let payload: Uint8Array
279
+ if (this.encryptionKey) {
280
+ const soc = await downloadEncryptedSOC(
281
+ this.bee,
282
+ this.owner,
283
+ identifier,
284
+ this.encryptionKey,
285
+ requestOptions,
286
+ )
287
+ payload = soc.payload
288
+ } else {
289
+ // Calculate chunk address: Keccak256(identifier || owner)
290
+ const address = new Reference(
291
+ Binary.keccak256(
292
+ Binary.concatBytes(identifier, this.owner.toUint8Array()),
293
+ ),
294
+ )
295
+
296
+ // Download chunk
297
+ const chunkData = await this.bee.downloadChunk(
298
+ address.toHex(),
299
+ undefined,
300
+ requestOptions,
301
+ )
302
+
303
+ // Extract payload from SOC (Single Owner Chunk)
304
+ const IDENTIFIER_SIZE = 32
305
+ const SIGNATURE_SIZE = 65
306
+ const SPAN_SIZE = 8
307
+ const SOC_HEADER_SIZE = IDENTIFIER_SIZE + SIGNATURE_SIZE
308
+
309
+ // Read span to get payload length
310
+ const spanStart = SOC_HEADER_SIZE
311
+ const span = chunkData.slice(spanStart, spanStart + SPAN_SIZE)
312
+ const spanView = new DataView(
313
+ span.buffer,
314
+ span.byteOffset,
315
+ span.byteLength,
316
+ )
317
+ const payloadLength = Number(spanView.getBigUint64(0, true)) // little-endian
318
+
319
+ // Extract full payload (timestamp + reference)
320
+ const payloadStart = spanStart + SPAN_SIZE
321
+ payload = chunkData.slice(payloadStart, payloadStart + payloadLength)
322
+ }
323
+
324
+ // Detect payload format based on length:
325
+ // - 40 bytes: timestamp(8) + reference(32) - from /soc endpoint
326
+ // - 48 bytes: span(8) + timestamp(8) + reference(32) - from /chunks with v1 format
327
+ // - 72 bytes: timestamp(8) + encrypted_reference(64) - from /soc endpoint
328
+ // - 80 bytes: span(8) + timestamp(8) + encrypted_reference(64) - from /chunks with v1 format
329
+ const VALID_PAYLOAD_LENGTHS = [40, 48, 72, 80]
330
+ if (!VALID_PAYLOAD_LENGTHS.includes(payload.length)) {
331
+ console.warn(
332
+ `Unexpected feed payload length: ${payload.length}. Expected 40, 48, 72, or 80 bytes.`,
333
+ )
334
+ return undefined
335
+ }
336
+
337
+ const TIMESTAMP_SIZE = 8
338
+ const hasSpanPrefix = payload.length === 48 || payload.length === 80
339
+
340
+ const timestampOffset = hasSpanPrefix ? 8 : 0
341
+ const timestampBytes = payload.slice(
342
+ timestampOffset,
343
+ timestampOffset + TIMESTAMP_SIZE,
344
+ )
345
+ const timestampView = new DataView(
346
+ timestampBytes.buffer,
347
+ timestampBytes.byteOffset,
348
+ timestampBytes.byteLength,
349
+ )
350
+ const timestamp = timestampView.getBigUint64(0, false) // big-endian
351
+
352
+ // Validate timestamp - update must be at or before target time
353
+ if (timestamp > at) {
354
+ return undefined
355
+ }
356
+
357
+ // Return reference and timestamp (skip timestamp and optional span prefix)
358
+ return {
359
+ reference: payload.slice(timestampOffset + TIMESTAMP_SIZE),
360
+ timestamp,
361
+ }
362
+ }
363
+
364
+ private findPreviousLeaf(
365
+ at: bigint,
366
+ after: bigint,
367
+ ): Promise<Uint8Array | undefined> {
368
+ return findPreviousLeaf(at, after, (targetAt, epoch) =>
369
+ this.getEpochChunk(targetAt, epoch),
370
+ )
371
+ }
372
+ }