@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,396 @@
1
+ /**
2
+ * Sync Account
3
+ *
4
+ * Factory function that creates a sync function for syncing account state
5
+ * to Swarm. This integrates store access, key derivation, and utilization
6
+ * tracking.
7
+ */
8
+
9
+ import {
10
+ Bee,
11
+ PrivateKey,
12
+ BatchId,
13
+ EthAddress,
14
+ Topic,
15
+ Reference,
16
+ type Chunk,
17
+ } from "@ethersphere/bee-js"
18
+ import { deriveSecret, hexToUint8Array } from "../utils/key-derivation"
19
+ import {
20
+ updateAfterWrite,
21
+ saveUtilizationState,
22
+ calculateUtilization,
23
+ } from "../utils/batch-utilization"
24
+ import type { UtilizationStoreDB } from "../storage/utilization-store"
25
+ import type { DebouncedUtilizationUploader } from "../storage/debounced-uploader"
26
+ import type {
27
+ AccountsStoreInterface,
28
+ IdentitiesStoreInterface,
29
+ ConnectedAppsStoreInterface,
30
+ PostageStampsStoreInterface,
31
+ } from "./store-interfaces"
32
+ import { BasicEpochUpdater } from "../proxy/feeds/epochs"
33
+ import { uploadEncryptedDataWithSigning } from "../proxy/upload-encrypted-data"
34
+ import { serializeAccountState } from "./serialization"
35
+ import type { SyncResult } from "./types"
36
+ import type { AccountStateSnapshot } from "../utils/account-state-snapshot"
37
+ import type { PostageStamp } from "../schemas"
38
+
39
+ // Timeout for utilization upload in milliseconds
40
+ const UTILIZATION_UPLOAD_TIMEOUT_MS = 30000
41
+
42
+ // Topic prefix for sync feeds
43
+ export const ACCOUNT_SYNC_TOPIC_PREFIX = "swarm-id-backup-v1:account"
44
+
45
+ /**
46
+ * Options for creating a sync account function
47
+ */
48
+ export interface SyncAccountOptions {
49
+ /** Bee client for Swarm operations */
50
+ bee: Bee
51
+
52
+ /** Store providing account data */
53
+ accountsStore: AccountsStoreInterface
54
+
55
+ /** Store providing identity data */
56
+ identitiesStore: IdentitiesStoreInterface
57
+
58
+ /** Store providing connected app data */
59
+ connectedAppsStore: ConnectedAppsStoreInterface
60
+
61
+ /** Store providing postage stamp data */
62
+ postageStampsStore: PostageStampsStoreInterface
63
+
64
+ /** Utilization store for browser-based utilization tracking */
65
+ utilizationStore: UtilizationStoreDB
66
+
67
+ /** Debounced uploader for batch utilization state */
68
+ utilizationUploader: DebouncedUtilizationUploader
69
+ }
70
+
71
+ /**
72
+ * Sync account function type
73
+ */
74
+ export type SyncAccountFunction = (
75
+ accountId: string,
76
+ ) => Promise<SyncResult | undefined>
77
+
78
+ /**
79
+ * Convert chunk addresses to Chunk objects for utilization tracking
80
+ *
81
+ * Creates minimal chunk objects with just the address property
82
+ * needed for bucket calculation. We don't need actual chunk data
83
+ * since we're only tracking which buckets/slots were used.
84
+ */
85
+ function createChunksFromAddresses(addresses: Uint8Array[]): Chunk[] {
86
+ return addresses.map((address) => {
87
+ return {
88
+ address: {
89
+ toUint8Array: () => address,
90
+ toHex: () =>
91
+ Array.from(address)
92
+ .map((b) => b.toString(16).padStart(2, "0"))
93
+ .join(""),
94
+ },
95
+ data: new Uint8Array(0), // Not used for utilization tracking
96
+ } as Chunk
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Create a sync account function with dependency-injected stores
102
+ *
103
+ * @param options - Configuration options including stores and optional utilization tracking
104
+ * @returns Function that syncs an account to Swarm
105
+ */
106
+ export function createSyncAccount(
107
+ options: SyncAccountOptions,
108
+ ): SyncAccountFunction {
109
+ const {
110
+ bee,
111
+ accountsStore,
112
+ identitiesStore,
113
+ connectedAppsStore,
114
+ postageStampsStore,
115
+ utilizationStore,
116
+ utilizationUploader,
117
+ } = options
118
+
119
+ /**
120
+ * Handle utilization tracking after chunk upload
121
+ */
122
+ async function handleUtilizationUpdate(
123
+ accountId: string,
124
+ chunkAddresses: Uint8Array[],
125
+ ): Promise<void> {
126
+ // Get account
127
+ const account = accountsStore.getAccount(new EthAddress(accountId))
128
+ if (!account) {
129
+ console.warn("[SyncCoordinator] Account not found for utilization update")
130
+ return
131
+ }
132
+
133
+ // Resolve default stamp
134
+ const defaultStamp =
135
+ account.defaultPostageStampBatchID ??
136
+ identitiesStore.getIdentitiesByAccount(account.id)[0]
137
+ ?.defaultPostageStampBatchID
138
+
139
+ if (!defaultStamp) {
140
+ console.warn("[SyncCoordinator] No default stamp, skipping utilization")
141
+ return
142
+ }
143
+
144
+ const batchID = new BatchId(defaultStamp)
145
+ const stamp = postageStampsStore.getStamp(batchID)
146
+
147
+ if (!stamp) {
148
+ console.warn("[SyncCoordinator] Stamp not found, skipping utilization")
149
+ return
150
+ }
151
+
152
+ // Convert chunk addresses to Chunks
153
+ const chunks = createChunksFromAddresses(chunkAddresses)
154
+
155
+ // Derive owner address from backup key
156
+ const backupKeyHex = await deriveSecret(
157
+ account.swarmEncryptionKey,
158
+ "backup-key",
159
+ )
160
+ const backupKey = new PrivateKey(backupKeyHex)
161
+ const owner = backupKey.publicKey().address()
162
+
163
+ // Update utilization state
164
+ const { state: utilizationState, tracker } = await updateAfterWrite(
165
+ batchID,
166
+ chunks,
167
+ stamp.depth,
168
+ {
169
+ bee,
170
+ owner,
171
+ encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
172
+ cache: utilizationStore,
173
+ },
174
+ )
175
+
176
+ // Calculate new utilization fraction
177
+ const newUtilization = calculateUtilization(utilizationState, stamp.depth)
178
+
179
+ // Update stamp in store (without triggering sync)
180
+ postageStampsStore.updateStampUtilization(batchID, newUtilization)
181
+
182
+ // Schedule debounced upload of dirty chunks and WAIT for it
183
+ if (tracker.hasDirtyChunks()) {
184
+ // Get stamper for signing chunks (with loaded bucket state)
185
+ const stamper = await postageStampsStore.getStamper(batchID, {
186
+ owner,
187
+ encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
188
+ })
189
+ if (!stamper) {
190
+ console.warn("[SyncCoordinator] Cannot create stamper, skipping upload")
191
+ return
192
+ }
193
+
194
+ const uploadPromise = utilizationUploader.scheduleUpload(
195
+ batchID.toHex(),
196
+ tracker,
197
+ async () => {
198
+ await saveUtilizationState(utilizationState, {
199
+ bee,
200
+ stamper,
201
+ encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
202
+ cache: utilizationStore,
203
+ tracker,
204
+ })
205
+
206
+ // Flush stamper bucket state updates to cache (if supported)
207
+ if (stamper.flush) {
208
+ await stamper.flush()
209
+ }
210
+ },
211
+ )
212
+
213
+ // Add timeout to prevent hanging
214
+ const timeoutPromise = new Promise<void>((_, reject) => {
215
+ setTimeout(
216
+ () =>
217
+ reject(
218
+ new Error(
219
+ `Utilization upload timeout (${UTILIZATION_UPLOAD_TIMEOUT_MS}ms)`,
220
+ ),
221
+ ),
222
+ UTILIZATION_UPLOAD_TIMEOUT_MS,
223
+ )
224
+ })
225
+
226
+ return Promise.race([uploadPromise, timeoutPromise])
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Capture a consistent snapshot of account state for syncing.
232
+ * Must be called before any async operations to ensure consistency.
233
+ *
234
+ * @param accountId - Account ID (hex address)
235
+ * @returns Snapshot and sync context, or undefined if account/stamp not found
236
+ */
237
+ function getAccountStateSnapshot(accountId: string):
238
+ | {
239
+ snapshot: AccountStateSnapshot
240
+ encryptionKey: string
241
+ defaultStamp: PostageStamp
242
+ }
243
+ | undefined {
244
+ // Get account
245
+ const account = accountsStore.getAccount(new EthAddress(accountId))
246
+ if (!account) {
247
+ console.warn("[SyncCoordinator] Account not found", accountId)
248
+ return undefined
249
+ }
250
+
251
+ // Resolve default stamp (account or first identity)
252
+ const defaultStampBatchID =
253
+ account.defaultPostageStampBatchID ??
254
+ identitiesStore.getIdentitiesByAccount(account.id)[0]
255
+ ?.defaultPostageStampBatchID
256
+
257
+ if (!defaultStampBatchID) {
258
+ console.warn("[SyncCoordinator] No default stamp for account", accountId)
259
+ return undefined
260
+ }
261
+
262
+ const defaultStamp = postageStampsStore.getStamp(defaultStampBatchID)
263
+ if (!defaultStamp) {
264
+ console.warn("[SyncCoordinator] Default stamp not found")
265
+ return undefined
266
+ }
267
+
268
+ // Collect account state
269
+ const identities = identitiesStore.getIdentitiesByAccount(account.id)
270
+ const apps = identities.flatMap((identity) =>
271
+ connectedAppsStore.getAppsByIdentityId(identity.id),
272
+ )
273
+ const stamps = postageStampsStore.getStampsByAccount(accountId)
274
+
275
+ const snapshot: AccountStateSnapshot = {
276
+ version: 1,
277
+ timestamp: Date.now(),
278
+ accountId,
279
+ metadata: {
280
+ accountName: account.name,
281
+ defaultPostageStampBatchID: defaultStampBatchID.toHex(),
282
+ createdAt: account.createdAt,
283
+ lastModified: Date.now(),
284
+ },
285
+ identities,
286
+ connectedApps: apps,
287
+ postageStamps: stamps,
288
+ }
289
+
290
+ return {
291
+ snapshot,
292
+ encryptionKey: account.swarmEncryptionKey,
293
+ defaultStamp,
294
+ }
295
+ }
296
+
297
+ return async function syncAccount(
298
+ accountId: string,
299
+ ): Promise<SyncResult | undefined> {
300
+ const startTime = performance.now()
301
+ const timestamp = () => new Date().toISOString()
302
+
303
+ // Capture state snapshot BEFORE any async operations
304
+ const snapshotResult = getAccountStateSnapshot(accountId)
305
+ if (!snapshotResult) {
306
+ return undefined
307
+ }
308
+
309
+ const { snapshot: state, encryptionKey, defaultStamp } = snapshotResult
310
+
311
+ try {
312
+ // 1. Derive account signing key for feed
313
+ const backupKeyHex = await deriveSecret(encryptionKey, "backup-key")
314
+ const accountKey = new PrivateKey(backupKeyHex)
315
+ const owner = accountKey.publicKey().address()
316
+
317
+ // 2. Serialize account state
318
+ const jsonData = serializeAccountState(state)
319
+
320
+ // 3. Get stamper from store
321
+ const stamper = await postageStampsStore.getStamper(
322
+ defaultStamp.batchID,
323
+ {
324
+ owner,
325
+ encryptionKey: hexToUint8Array(encryptionKey),
326
+ },
327
+ )
328
+ if (!stamper) {
329
+ throw new Error(
330
+ `Cannot create stamper for batch ${defaultStamp.batchID.toHex()}`,
331
+ )
332
+ }
333
+
334
+ // 4. Upload encrypted data to Swarm
335
+ const uploadResult = await uploadEncryptedDataWithSigning(
336
+ {
337
+ bee,
338
+ stamper,
339
+ },
340
+ jsonData,
341
+ hexToUint8Array(encryptionKey),
342
+ undefined,
343
+ )
344
+
345
+ // Collect chunk addresses for utilization tracking
346
+ const allChunkAddresses = uploadResult.chunkAddresses
347
+
348
+ // 5. Handle utilization tracking
349
+
350
+ try {
351
+ await handleUtilizationUpdate(accountId, allChunkAddresses)
352
+ } catch (error) {
353
+ // Don't fail sync if utilization fails - continue with feed update
354
+ console.error(
355
+ `[SyncCoordinator ${timestamp()}] Utilization upload failed (+${(performance.now() - startTime).toFixed(2)}ms):`,
356
+ error,
357
+ )
358
+ }
359
+
360
+ // 6. Update epoch feed (after utilization completes)
361
+ const topic = Topic.fromString(
362
+ `${ACCOUNT_SYNC_TOPIC_PREFIX}:${accountId}`,
363
+ )
364
+ const updater = new BasicEpochUpdater(bee, topic, accountKey)
365
+ const feedTimestamp = BigInt(Math.floor(Date.now() / 1000))
366
+
367
+ // Convert 128-char hex reference to 64-byte Uint8Array
368
+ const refBytes = new Reference(uploadResult.reference).toUint8Array()
369
+
370
+ const updateResult = await updater.update(
371
+ feedTimestamp,
372
+ refBytes,
373
+ stamper,
374
+ )
375
+
376
+ // Add SOC chunk to tracked addresses
377
+ allChunkAddresses.push(updateResult.socAddress)
378
+
379
+ return {
380
+ status: "success",
381
+ reference: uploadResult.reference,
382
+ timestamp: feedTimestamp,
383
+ chunkAddresses: allChunkAddresses,
384
+ }
385
+ } catch (error) {
386
+ console.error(
387
+ `[SyncCoordinator ${timestamp()}] Sync failed (+${(performance.now() - startTime).toFixed(2)}ms):`,
388
+ error,
389
+ )
390
+ return {
391
+ status: "error",
392
+ error: error instanceof Error ? error.message : String(error),
393
+ }
394
+ }
395
+ }
396
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Result of a sync operation
3
+ */
4
+ export type SyncResult =
5
+ | {
6
+ status: "success"
7
+ reference: string
8
+ timestamp: bigint
9
+ chunkAddresses: Uint8Array[] // All chunks uploaded during sync
10
+ }
11
+ | { status: "error"; error: string }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Shared test fixtures for backup-encryption, account-state-snapshot, and sync tests.
3
+ */
4
+ import { EthAddress, BatchId, PrivateKey, Bytes } from "@ethersphere/bee-js"
5
+ import type {
6
+ PasskeyAccount,
7
+ EthereumAccount,
8
+ AgentAccount,
9
+ Identity,
10
+ ConnectedApp,
11
+ PostageStamp,
12
+ } from "./schemas"
13
+
14
+ export const TEST_ETH_ADDRESS_HEX = "a".repeat(40)
15
+ export const TEST_ETH_ADDRESS_2_HEX = "b".repeat(40)
16
+ export const TEST_IDENTITY_ADDRESS_HEX = "1".repeat(40)
17
+ export const TEST_IDENTITY_ADDRESS_2_HEX = "2".repeat(40)
18
+ export const TEST_BATCH_ID_HEX = "c".repeat(64)
19
+ export const TEST_BATCH_ID_2_HEX = "e".repeat(64)
20
+ export const TEST_PRIVATE_KEY_HEX = "d".repeat(64)
21
+ export const TEST_ENCRYPTION_KEY_HEX = "f".repeat(64)
22
+ export const DIFFERENT_ENCRYPTION_KEY_HEX = "3".repeat(64)
23
+
24
+ export function createPasskeyAccount(
25
+ overrides?: Partial<PasskeyAccount>,
26
+ ): PasskeyAccount {
27
+ return {
28
+ id: new EthAddress(TEST_ETH_ADDRESS_HEX),
29
+ name: "Test Passkey Account",
30
+ createdAt: 1700000000000,
31
+ type: "passkey" as const,
32
+ credentialId: "credential-abc-123",
33
+ swarmEncryptionKey: TEST_ENCRYPTION_KEY_HEX,
34
+ ...overrides,
35
+ }
36
+ }
37
+
38
+ export function createEthereumAccount(
39
+ overrides?: Partial<EthereumAccount>,
40
+ ): EthereumAccount {
41
+ return {
42
+ id: new EthAddress(TEST_ETH_ADDRESS_HEX),
43
+ name: "Test Ethereum Account",
44
+ createdAt: 1700000000000,
45
+ type: "ethereum" as const,
46
+ ethereumAddress: new EthAddress(TEST_ETH_ADDRESS_2_HEX),
47
+ encryptedMasterKey: new Bytes(new Uint8Array([1, 2, 3, 4])),
48
+ encryptionSalt: new Bytes(new Uint8Array([5, 6, 7, 8])),
49
+ encryptedSecretSeed: new Bytes(new Uint8Array([9, 10, 11, 12])),
50
+ swarmEncryptionKey: TEST_ENCRYPTION_KEY_HEX,
51
+ ...overrides,
52
+ }
53
+ }
54
+
55
+ export function createAgentAccount(
56
+ overrides?: Partial<AgentAccount>,
57
+ ): AgentAccount {
58
+ return {
59
+ id: new EthAddress(TEST_ETH_ADDRESS_HEX),
60
+ name: "Test Agent Account",
61
+ createdAt: 1700000000000,
62
+ type: "agent" as const,
63
+ swarmEncryptionKey: TEST_ENCRYPTION_KEY_HEX,
64
+ ...overrides,
65
+ }
66
+ }
67
+
68
+ export function createIdentity(overrides?: Partial<Identity>): Identity {
69
+ return {
70
+ id: TEST_IDENTITY_ADDRESS_HEX,
71
+ accountId: new EthAddress(TEST_ETH_ADDRESS_HEX),
72
+ name: "Default Identity",
73
+ createdAt: 1700000000000,
74
+ ...overrides,
75
+ }
76
+ }
77
+
78
+ export function createConnectedApp(
79
+ overrides?: Partial<ConnectedApp>,
80
+ ): ConnectedApp {
81
+ return {
82
+ appUrl: "https://app.example.com",
83
+ appName: "Test App",
84
+ lastConnectedAt: 1700000000000,
85
+ identityId: TEST_IDENTITY_ADDRESS_HEX,
86
+ appSecret: "secret-should-be-stripped",
87
+ ...overrides,
88
+ }
89
+ }
90
+
91
+ export function createPostageStamp(
92
+ overrides?: Partial<PostageStamp>,
93
+ ): PostageStamp {
94
+ return {
95
+ accountId: TEST_ETH_ADDRESS_HEX,
96
+ batchID: new BatchId(TEST_BATCH_ID_HEX),
97
+ signerKey: new PrivateKey(TEST_PRIVATE_KEY_HEX),
98
+ utilization: 0,
99
+ usable: true,
100
+ depth: 20,
101
+ amount: 100000000n,
102
+ bucketDepth: 16,
103
+ blockNumber: 12345678,
104
+ immutableFlag: false,
105
+ exists: true,
106
+ createdAt: 1700000000000,
107
+ ...overrides,
108
+ }
109
+ }