@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,28 @@
1
+ // Public API
2
+ export {
3
+ // Account-level key derivation
4
+ deriveAccountBackupKey,
5
+ deriveAccountSwarmEncryptionKey,
6
+ backupKeyToPrivateKey,
7
+ } from "../utils/key-derivation"
8
+ export { serializeAccountState, deserializeAccountState } from "./serialization"
9
+
10
+ // Sync account
11
+ export { createSyncAccount, ACCOUNT_SYNC_TOPIC_PREFIX } from "./sync-account"
12
+ export type { SyncAccountOptions, SyncAccountFunction } from "./sync-account"
13
+
14
+ // Restore account from Swarm
15
+ export { restoreAccountFromSwarm } from "./restore-account"
16
+ export type { RestoreAccountResult } from "./restore-account"
17
+
18
+ // Store interfaces
19
+ export type {
20
+ AccountsStoreInterface,
21
+ IdentitiesStoreInterface,
22
+ ConnectedAppsStoreInterface,
23
+ PostageStampsStoreInterface,
24
+ StamperOptions,
25
+ FlushableStamper,
26
+ } from "./store-interfaces"
27
+
28
+ export type { SyncResult } from "./types"
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Restore Account from Swarm
3
+ *
4
+ * When a passkey auth succeeds but no local account exists (e.g. new device),
5
+ * this utility derives the keys needed to find and decrypt the account
6
+ * snapshot stored in Swarm and returns the restored state.
7
+ */
8
+
9
+ import {
10
+ Bee,
11
+ PrivateKey,
12
+ EthAddress,
13
+ Topic,
14
+ Reference,
15
+ type Bytes,
16
+ } from "@ethersphere/bee-js"
17
+ import {
18
+ deriveAccountSwarmEncryptionKey,
19
+ deriveSecret,
20
+ } from "../utils/key-derivation"
21
+ import { ACCOUNT_SYNC_TOPIC_PREFIX } from "./sync-account"
22
+ import { AsyncEpochFinder } from "../proxy/feeds/epochs/async-finder"
23
+ import { downloadDataWithChunkAPI } from "../proxy/download-data"
24
+ import { deserializeAccountState } from "./serialization"
25
+ import type { AccountStateSnapshot } from "../utils/account-state-snapshot"
26
+
27
+ /**
28
+ * Result of a successful account restore from Swarm
29
+ */
30
+ export interface RestoreAccountResult {
31
+ snapshot: AccountStateSnapshot
32
+ swarmEncryptionKey: string
33
+ credentialId: string
34
+ }
35
+
36
+ /**
37
+ * Restore account state from Swarm using passkey authentication result
38
+ *
39
+ * @param bee - Bee client instance
40
+ * @param masterKey - Master key from passkey authentication
41
+ * @param ethereumAddress - Account ID (EthAddress) from passkey authentication
42
+ * @param credentialId - Credential ID from passkey authentication
43
+ * @returns Restored account state, or undefined if no backup found in Swarm
44
+ */
45
+ export async function restoreAccountFromSwarm(
46
+ bee: Bee,
47
+ masterKey: Bytes,
48
+ ethereumAddress: EthAddress,
49
+ credentialId: string,
50
+ ): Promise<RestoreAccountResult | undefined> {
51
+ const accountId = ethereumAddress.toHex()
52
+
53
+ // 1. Derive the swarm encryption key from the master key
54
+ const swarmEncryptionKey = await deriveAccountSwarmEncryptionKey(
55
+ masterKey.toHex(),
56
+ )
57
+
58
+ // 2. Derive the backup key (used as feed owner)
59
+ const backupKeyHex = await deriveSecret(swarmEncryptionKey, "backup-key")
60
+ const backupKey = new PrivateKey(backupKeyHex)
61
+ const owner = backupKey.publicKey().address()
62
+
63
+ // 3. Build the feed topic
64
+ const topic = Topic.fromString(`${ACCOUNT_SYNC_TOPIC_PREFIX}:${accountId}`)
65
+
66
+ // 4. Look up the latest epoch feed entry
67
+ // Note: feed SOCs are uploaded unencrypted (sync-account.ts doesn't pass
68
+ // encryptionKey to updater.update()), so the finder must NOT use one either.
69
+ const finder = new AsyncEpochFinder(bee, topic, owner)
70
+ const now = BigInt(Math.floor(Date.now() / 1000))
71
+
72
+ const refBytes = await finder.findAt(now)
73
+
74
+ if (!refBytes) {
75
+ return undefined
76
+ }
77
+
78
+ // 5. Download and decrypt the account snapshot
79
+ const reference = new Reference(refBytes)
80
+ const data = await downloadDataWithChunkAPI(bee, reference.toHex())
81
+
82
+ // 6. Deserialize the snapshot
83
+ const snapshot = deserializeAccountState(data)
84
+
85
+ return {
86
+ snapshot,
87
+ swarmEncryptionKey,
88
+ credentialId,
89
+ }
90
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ serializeAccountStateSnapshot,
3
+ deserializeAccountStateSnapshot,
4
+ } from "../utils/account-state-snapshot"
5
+ import type { AccountStateSnapshot } from "../utils/account-state-snapshot"
6
+
7
+ /**
8
+ * Serialize account state to JSON bytes
9
+ *
10
+ * @param state - Account payload to serialize
11
+ * @returns JSON encoded as Uint8Array
12
+ */
13
+ export function serializeAccountState(state: AccountStateSnapshot): Uint8Array {
14
+ const json = JSON.stringify(serializeAccountStateSnapshot(state))
15
+
16
+ return new TextEncoder().encode(json)
17
+ }
18
+
19
+ /**
20
+ * Deserialize JSON bytes to account payload
21
+ *
22
+ * @param data - JSON bytes to deserialize
23
+ * @returns Account payload
24
+ */
25
+ export function deserializeAccountState(
26
+ data: Uint8Array,
27
+ ): AccountStateSnapshot {
28
+ const json = new TextDecoder().decode(data)
29
+ const parsed = JSON.parse(json)
30
+ const result = deserializeAccountStateSnapshot(parsed)
31
+
32
+ if (!result.success) {
33
+ throw new Error(
34
+ `Failed to deserialize account state: ${result.error.message}`,
35
+ )
36
+ }
37
+
38
+ return result.data
39
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Store Interfaces for Sync Coordinator
3
+ *
4
+ * These interfaces define the minimal contract that stores must implement
5
+ * to be used with the sync coordinator. This allows the coordinator to be
6
+ * used with different store implementations (Svelte stores, plain objects, etc.)
7
+ */
8
+
9
+ import type { EthAddress, BatchId, Stamper } from "@ethersphere/bee-js"
10
+ import type { Account, Identity, ConnectedApp, PostageStamp } from "../schemas"
11
+
12
+ /**
13
+ * Options for creating a stamper with utilization tracking
14
+ */
15
+ export interface StamperOptions {
16
+ owner: EthAddress
17
+ encryptionKey: Uint8Array
18
+ }
19
+
20
+ /**
21
+ * Extended stamper interface with optional flush capability
22
+ *
23
+ * Some stampers (like UtilizationAwareStamper) support flushing
24
+ * dirty bucket state to cache.
25
+ */
26
+ export interface FlushableStamper extends Stamper {
27
+ flush?(): Promise<void>
28
+ }
29
+
30
+ /**
31
+ * Interface for accessing account data
32
+ */
33
+ export interface AccountsStoreInterface {
34
+ getAccount(id: EthAddress): Account | undefined
35
+ }
36
+
37
+ /**
38
+ * Interface for accessing identity data
39
+ */
40
+ export interface IdentitiesStoreInterface {
41
+ getIdentitiesByAccount(accountId: EthAddress): Identity[]
42
+ }
43
+
44
+ /**
45
+ * Interface for accessing connected app data
46
+ */
47
+ export interface ConnectedAppsStoreInterface {
48
+ getAppsByIdentityId(identityId: string): ConnectedApp[]
49
+ }
50
+
51
+ /**
52
+ * Interface for accessing and managing postage stamp data
53
+ */
54
+ export interface PostageStampsStoreInterface {
55
+ getStamp(batchID: BatchId): PostageStamp | undefined
56
+ getStampsByAccount(accountId: string): PostageStamp[]
57
+ getStamper(
58
+ batchID: BatchId,
59
+ options?: StamperOptions,
60
+ ): Promise<FlushableStamper | undefined>
61
+ updateStampUtilization(batchID: BatchId, utilization: number): void
62
+ }
@@ -0,0 +1,302 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { EthAddress, BatchId } from "@ethersphere/bee-js"
3
+ import { createSyncAccount } from "./sync-account"
4
+ import { deserializeAccountState } from "./serialization"
5
+ import type {
6
+ AccountsStoreInterface,
7
+ IdentitiesStoreInterface,
8
+ ConnectedAppsStoreInterface,
9
+ PostageStampsStoreInterface,
10
+ } from "./store-interfaces"
11
+ import type { UtilizationStoreDB } from "../storage/utilization-store"
12
+ import type { DebouncedUtilizationUploader } from "../storage/debounced-uploader"
13
+ import {
14
+ TEST_ETH_ADDRESS_HEX,
15
+ TEST_IDENTITY_ADDRESS_HEX,
16
+ TEST_BATCH_ID_HEX,
17
+ createPasskeyAccount,
18
+ createIdentity,
19
+ createConnectedApp,
20
+ createPostageStamp,
21
+ } from "../test-fixtures"
22
+
23
+ // ============================================================================
24
+ // Mock Factories
25
+ // ============================================================================
26
+
27
+ function createMockStores() {
28
+ const account = createPasskeyAccount({
29
+ credentialId: "test-credential",
30
+ name: "Test Account",
31
+ defaultPostageStampBatchID: new BatchId(TEST_BATCH_ID_HEX),
32
+ })
33
+ const identity = createIdentity()
34
+ const connectedApp = createConnectedApp({ appSecret: undefined })
35
+ const stamp = createPostageStamp()
36
+
37
+ const accountsStore: AccountsStoreInterface = {
38
+ getAccount: vi.fn((id: EthAddress) =>
39
+ id.toHex() === TEST_ETH_ADDRESS_HEX ? account : undefined,
40
+ ),
41
+ }
42
+
43
+ const identitiesStore: IdentitiesStoreInterface = {
44
+ getIdentitiesByAccount: vi.fn((accountId: EthAddress) =>
45
+ accountId.toHex() === TEST_ETH_ADDRESS_HEX ? [identity] : [],
46
+ ),
47
+ }
48
+
49
+ const connectedAppsStore: ConnectedAppsStoreInterface = {
50
+ getAppsByIdentityId: vi.fn((identityId: string) =>
51
+ identityId === TEST_IDENTITY_ADDRESS_HEX ? [connectedApp] : [],
52
+ ),
53
+ }
54
+
55
+ const mockStamper = {
56
+ stamp: vi.fn().mockResolvedValue({
57
+ batchId: new Uint8Array(32),
58
+ index: new Uint8Array(8),
59
+ timestamp: new Uint8Array(8),
60
+ signature: new Uint8Array(65),
61
+ }),
62
+ flush: vi.fn().mockResolvedValue(undefined),
63
+ }
64
+
65
+ const postageStampsStore: PostageStampsStoreInterface = {
66
+ getStamp: vi.fn((batchID: BatchId) =>
67
+ batchID.toHex() === TEST_BATCH_ID_HEX ? stamp : undefined,
68
+ ),
69
+ getStampsByAccount: vi.fn((accountId: string) =>
70
+ accountId === TEST_ETH_ADDRESS_HEX ? [stamp] : [],
71
+ ),
72
+ getStamper: vi.fn().mockResolvedValue(mockStamper),
73
+ updateStampUtilization: vi.fn(),
74
+ }
75
+
76
+ return {
77
+ accountsStore,
78
+ identitiesStore,
79
+ connectedAppsStore,
80
+ postageStampsStore,
81
+ mockStamper,
82
+ }
83
+ }
84
+
85
+ // ============================================================================
86
+ // Upload & Epoch Mock Setup
87
+ // ============================================================================
88
+
89
+ // Track what was uploaded
90
+ let capturedUploadData: Uint8Array | undefined
91
+ let capturedEncryptionKey: Uint8Array | undefined
92
+ let uploadCallCount: number
93
+ let epochUpdateCallCount: number
94
+ let capturedEpochReference: Uint8Array | undefined
95
+
96
+ const FAKE_UPLOAD_REFERENCE = "ab".repeat(32)
97
+ const FAKE_SOC_ADDRESS = new Uint8Array(32).fill(0xee)
98
+
99
+ vi.mock("../proxy/upload-encrypted-data", () => ({
100
+ uploadEncryptedDataWithSigning: vi.fn(
101
+ async (
102
+ _context: unknown,
103
+ data: Uint8Array,
104
+ encryptionKey: Uint8Array | undefined,
105
+ ) => {
106
+ capturedUploadData = data
107
+ capturedEncryptionKey = encryptionKey
108
+ uploadCallCount++
109
+ return {
110
+ reference: FAKE_UPLOAD_REFERENCE,
111
+ chunkAddresses: [new Uint8Array(32).fill(0xaa)],
112
+ }
113
+ },
114
+ ),
115
+ }))
116
+
117
+ // Use a real class for the mock so `new BasicEpochUpdater(...)` works
118
+ const mockUpdate = vi.fn()
119
+
120
+ vi.mock("../proxy/feeds/epochs", () => {
121
+ return {
122
+ BasicEpochUpdater: class MockBasicEpochUpdater {
123
+ update = mockUpdate
124
+ getOwner = vi.fn(() => new EthAddress("a".repeat(40)))
125
+ },
126
+ }
127
+ })
128
+
129
+ // Mock utilization to avoid complexity in these tests
130
+ vi.mock("../utils/batch-utilization", () => ({
131
+ updateAfterWrite: vi.fn().mockResolvedValue({
132
+ state: { chunks: new Map() },
133
+ tracker: { hasDirtyChunks: () => false, getDirtyChunks: () => [] },
134
+ }),
135
+ saveUtilizationState: vi.fn().mockResolvedValue(undefined),
136
+ calculateUtilization: vi.fn().mockReturnValue(0.01),
137
+ }))
138
+
139
+ // ============================================================================
140
+ // Tests
141
+ // ============================================================================
142
+
143
+ describe("createSyncAccount", () => {
144
+ beforeEach(() => {
145
+ capturedUploadData = undefined
146
+ capturedEncryptionKey = undefined
147
+ capturedEpochReference = undefined
148
+ uploadCallCount = 0
149
+ epochUpdateCallCount = 0
150
+
151
+ mockUpdate.mockReset()
152
+ mockUpdate.mockImplementation(
153
+ async (_timestamp: bigint, reference: Uint8Array) => {
154
+ capturedEpochReference = reference
155
+ epochUpdateCallCount++
156
+ return {
157
+ socAddress: FAKE_SOC_ADDRESS,
158
+ epoch: { start: 0n, level: 0 },
159
+ timestamp: BigInt(Math.floor(Date.now() / 1000)),
160
+ }
161
+ },
162
+ )
163
+ })
164
+
165
+ it("should upload encrypted data and update epoch feed", async () => {
166
+ const stores = createMockStores()
167
+
168
+ const syncAccount = createSyncAccount({
169
+ bee: {} as never,
170
+ ...stores,
171
+ utilizationStore: {} as UtilizationStoreDB,
172
+ utilizationUploader: {
173
+ scheduleUpload: vi.fn().mockResolvedValue(undefined),
174
+ } as unknown as DebouncedUtilizationUploader,
175
+ })
176
+
177
+ const result = await syncAccount(TEST_ETH_ADDRESS_HEX)
178
+
179
+ expect(result).toBeDefined()
180
+ expect(result!.status).toBe("success")
181
+ if (result!.status !== "success") return
182
+
183
+ // Verify upload happened
184
+ expect(uploadCallCount).toBe(1)
185
+ expect(capturedUploadData).toBeDefined()
186
+ expect(capturedEncryptionKey).toBeDefined()
187
+
188
+ // Verify epoch feed was updated
189
+ expect(epochUpdateCallCount).toBe(1)
190
+ expect(capturedEpochReference).toBeDefined()
191
+
192
+ // Verify result contains reference and chunk addresses
193
+ expect(result.reference).toBe(FAKE_UPLOAD_REFERENCE)
194
+ expect(result.chunkAddresses.length).toBeGreaterThanOrEqual(2) // data chunks + SOC
195
+ })
196
+
197
+ it("should serialize account state with all fields including accountName", async () => {
198
+ const stores = createMockStores()
199
+
200
+ const syncAccount = createSyncAccount({
201
+ bee: {} as never,
202
+ ...stores,
203
+ utilizationStore: {} as UtilizationStoreDB,
204
+ utilizationUploader: {
205
+ scheduleUpload: vi.fn().mockResolvedValue(undefined),
206
+ } as unknown as DebouncedUtilizationUploader,
207
+ })
208
+
209
+ await syncAccount(TEST_ETH_ADDRESS_HEX)
210
+
211
+ // Deserialize the captured upload data to verify contents
212
+ expect(capturedUploadData).toBeDefined()
213
+ const deserialized = deserializeAccountState(capturedUploadData!)
214
+
215
+ expect(deserialized.version).toBe(1)
216
+ expect(deserialized.accountId).toBe(TEST_ETH_ADDRESS_HEX)
217
+ expect(deserialized.metadata.accountName).toBe("Test Account")
218
+ expect(deserialized.metadata.defaultPostageStampBatchID).toBe(
219
+ TEST_BATCH_ID_HEX,
220
+ )
221
+ expect(deserialized.metadata.createdAt).toBe(1700000000000)
222
+ expect(deserialized.identities).toHaveLength(1)
223
+ expect(deserialized.identities[0].name).toBe("Default Identity")
224
+ expect(deserialized.connectedApps).toHaveLength(1)
225
+ expect(deserialized.connectedApps[0].appName).toBe("Test App")
226
+ expect(deserialized.postageStamps).toHaveLength(1)
227
+ expect(deserialized.postageStamps[0].depth).toBe(20)
228
+ })
229
+
230
+ it("should return undefined when account not found", async () => {
231
+ const stores = createMockStores()
232
+ ;(
233
+ stores.accountsStore.getAccount as ReturnType<typeof vi.fn>
234
+ ).mockReturnValue(undefined)
235
+
236
+ const syncAccount = createSyncAccount({
237
+ bee: {} as never,
238
+ ...stores,
239
+ utilizationStore: {} as UtilizationStoreDB,
240
+ utilizationUploader: {
241
+ scheduleUpload: vi.fn().mockResolvedValue(undefined),
242
+ } as unknown as DebouncedUtilizationUploader,
243
+ })
244
+
245
+ const result = await syncAccount(TEST_ETH_ADDRESS_HEX)
246
+ expect(result).toBeUndefined()
247
+ expect(uploadCallCount).toBe(0)
248
+ })
249
+
250
+ it("should return undefined when no default stamp available", async () => {
251
+ const stores = createMockStores()
252
+ const account = createPasskeyAccount({
253
+ defaultPostageStampBatchID: new BatchId(TEST_BATCH_ID_HEX),
254
+ })
255
+ ;(
256
+ stores.accountsStore.getAccount as ReturnType<typeof vi.fn>
257
+ ).mockReturnValue({
258
+ ...account,
259
+ defaultPostageStampBatchID: undefined,
260
+ })
261
+ ;(
262
+ stores.identitiesStore.getIdentitiesByAccount as ReturnType<typeof vi.fn>
263
+ ).mockReturnValue([
264
+ { ...createIdentity(), defaultPostageStampBatchID: undefined },
265
+ ])
266
+
267
+ const syncAccount = createSyncAccount({
268
+ bee: {} as never,
269
+ ...stores,
270
+ utilizationStore: {} as UtilizationStoreDB,
271
+ utilizationUploader: {
272
+ scheduleUpload: vi.fn().mockResolvedValue(undefined),
273
+ } as unknown as DebouncedUtilizationUploader,
274
+ })
275
+
276
+ const result = await syncAccount(TEST_ETH_ADDRESS_HEX)
277
+ expect(result).toBeUndefined()
278
+ expect(uploadCallCount).toBe(0)
279
+ })
280
+
281
+ it("should include SOC address in returned chunk addresses", async () => {
282
+ const stores = createMockStores()
283
+
284
+ const syncAccount = createSyncAccount({
285
+ bee: {} as never,
286
+ ...stores,
287
+ utilizationStore: {} as UtilizationStoreDB,
288
+ utilizationUploader: {
289
+ scheduleUpload: vi.fn().mockResolvedValue(undefined),
290
+ } as unknown as DebouncedUtilizationUploader,
291
+ })
292
+
293
+ const result = await syncAccount(TEST_ETH_ADDRESS_HEX)
294
+ expect(result).toBeDefined()
295
+ expect(result!.status).toBe("success")
296
+ if (result!.status !== "success") return
297
+
298
+ // Last address should be the SOC address from epoch feed update
299
+ const lastAddress = result.chunkAddresses[result.chunkAddresses.length - 1]
300
+ expect(lastAddress).toEqual(FAKE_SOC_ADDRESS)
301
+ })
302
+ })