@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,595 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { EthAddress, BatchId, PrivateKey } from "@ethersphere/bee-js"
3
+ import {
4
+ serializeAccountStateSnapshot,
5
+ deserializeAccountStateSnapshot,
6
+ AccountStateSnapshotSchemaV1,
7
+ } from "./account-state-snapshot"
8
+ import type { Account, ConnectedApp, PostageStamp } from "../schemas"
9
+ import {
10
+ TEST_ETH_ADDRESS_HEX,
11
+ TEST_IDENTITY_ADDRESS_HEX,
12
+ TEST_IDENTITY_ADDRESS_2_HEX,
13
+ TEST_BATCH_ID_HEX,
14
+ TEST_BATCH_ID_2_HEX,
15
+ TEST_PRIVATE_KEY_HEX,
16
+ createPasskeyAccount,
17
+ createEthereumAccount,
18
+ createAgentAccount,
19
+ createIdentity,
20
+ createConnectedApp,
21
+ createPostageStamp,
22
+ } from "../test-fixtures"
23
+
24
+ /**
25
+ * Helper: extract metadata from an Account and serialize via the snapshot function.
26
+ */
27
+ function serializeFromAccount(
28
+ account: Account,
29
+ identities: Parameters<typeof serializeAccountStateSnapshot>[0]["identities"],
30
+ connectedApps: Parameters<
31
+ typeof serializeAccountStateSnapshot
32
+ >[0]["connectedApps"],
33
+ postageStamps: Parameters<
34
+ typeof serializeAccountStateSnapshot
35
+ >[0]["postageStamps"],
36
+ ) {
37
+ return serializeAccountStateSnapshot({
38
+ accountId: account.id.toHex(),
39
+ metadata: {
40
+ accountName: account.name,
41
+ defaultPostageStampBatchID: account.defaultPostageStampBatchID?.toHex(),
42
+ createdAt: account.createdAt,
43
+ lastModified: Date.now(),
44
+ },
45
+ identities,
46
+ connectedApps,
47
+ postageStamps,
48
+ timestamp: Date.now(),
49
+ })
50
+ }
51
+
52
+ // ============================================================================
53
+ // Round-trip Tests
54
+ // ============================================================================
55
+
56
+ describe("round-trip: serialize → JSON → deserialize", () => {
57
+ it("should round-trip a passkey account with identities, apps, and stamps", () => {
58
+ const account = createPasskeyAccount()
59
+ const identities = [createIdentity()]
60
+ const connectedApps = [createConnectedApp()]
61
+ const postageStamps = [createPostageStamp()]
62
+
63
+ const serialized = serializeFromAccount(
64
+ account,
65
+ identities,
66
+ connectedApps,
67
+ postageStamps,
68
+ )
69
+ const json = JSON.stringify(serialized)
70
+ const parsed = JSON.parse(json)
71
+ const result = deserializeAccountStateSnapshot(parsed)
72
+
73
+ expect(result.success).toBe(true)
74
+ if (!result.success) return
75
+
76
+ expect(result.data.accountId).toBe(TEST_ETH_ADDRESS_HEX)
77
+ expect(result.data.metadata.accountName).toBe("Test Passkey Account")
78
+ expect(result.data.identities).toHaveLength(1)
79
+ expect(result.data.identities[0].accountId).toBeInstanceOf(EthAddress)
80
+ expect(result.data.connectedApps).toHaveLength(1)
81
+ expect(result.data.connectedApps[0].appName).toBe("Test App")
82
+ expect(result.data.postageStamps).toHaveLength(1)
83
+ expect(result.data.postageStamps[0].batchID).toBeInstanceOf(BatchId)
84
+ expect(result.data.postageStamps[0].signerKey).toBeInstanceOf(PrivateKey)
85
+ })
86
+
87
+ it("should round-trip an ethereum account with metadata", () => {
88
+ const account = createEthereumAccount()
89
+ const identities = [createIdentity()]
90
+ const connectedApps: ConnectedApp[] = []
91
+ const postageStamps: PostageStamp[] = []
92
+
93
+ const serialized = serializeFromAccount(
94
+ account,
95
+ identities,
96
+ connectedApps,
97
+ postageStamps,
98
+ )
99
+ const json = JSON.stringify(serialized)
100
+ const parsed = JSON.parse(json)
101
+ const result = deserializeAccountStateSnapshot(parsed)
102
+
103
+ expect(result.success).toBe(true)
104
+ if (!result.success) return
105
+
106
+ expect(result.data.accountId).toBe(TEST_ETH_ADDRESS_HEX)
107
+ expect(result.data.metadata.accountName).toBe("Test Ethereum Account")
108
+ expect(result.data.metadata.createdAt).toBe(1700000000000)
109
+ })
110
+
111
+ it("should round-trip an agent account", () => {
112
+ const account = createAgentAccount()
113
+ const identities = [createIdentity()]
114
+ const connectedApps: ConnectedApp[] = []
115
+ const postageStamps: PostageStamp[] = []
116
+
117
+ const serialized = serializeFromAccount(
118
+ account,
119
+ identities,
120
+ connectedApps,
121
+ postageStamps,
122
+ )
123
+ const json = JSON.stringify(serialized)
124
+ const parsed = JSON.parse(json)
125
+ const result = deserializeAccountStateSnapshot(parsed)
126
+
127
+ expect(result.success).toBe(true)
128
+ if (!result.success) return
129
+
130
+ expect(result.data.metadata.accountName).toBe("Test Agent Account")
131
+ })
132
+
133
+ it("should produce valid JSON for actual file I/O simulation", () => {
134
+ const account = createPasskeyAccount({
135
+ defaultPostageStampBatchID: new BatchId(TEST_BATCH_ID_HEX),
136
+ })
137
+ const identities = [
138
+ createIdentity({ settings: { appSessionDuration: 3600 } }),
139
+ ]
140
+ const connectedApps = [
141
+ createConnectedApp({
142
+ appIcon: "https://example.com/icon.png",
143
+ appDescription: "A test app",
144
+ connectedUntil: 1700100000000,
145
+ }),
146
+ ]
147
+ const postageStamps = [createPostageStamp({ batchTTL: 86400 })]
148
+
149
+ const serialized = serializeFromAccount(
150
+ account,
151
+ identities,
152
+ connectedApps,
153
+ postageStamps,
154
+ )
155
+
156
+ // Simulate file write + read
157
+ const fileContent = JSON.stringify(serialized, undefined, 2)
158
+ const fileData = JSON.parse(fileContent)
159
+ const result = deserializeAccountStateSnapshot(fileData)
160
+
161
+ expect(result.success).toBe(true)
162
+ if (!result.success) return
163
+
164
+ expect(result.data.metadata.defaultPostageStampBatchID).toBe(
165
+ TEST_BATCH_ID_HEX,
166
+ )
167
+ expect(result.data.identities[0].settings?.appSessionDuration).toBe(3600)
168
+ expect(result.data.connectedApps[0].appIcon).toBe(
169
+ "https://example.com/icon.png",
170
+ )
171
+ expect(result.data.postageStamps[0].batchTTL).toBe(86400)
172
+ })
173
+ })
174
+
175
+ // ============================================================================
176
+ // appSecret Persistence Tests
177
+ // ============================================================================
178
+
179
+ describe("appSecret in snapshots", () => {
180
+ it("should include appSecret in serialized export when present on input", () => {
181
+ const account = createPasskeyAccount()
182
+ const connectedApps = [createConnectedApp({ appSecret: "my-secret-value" })]
183
+
184
+ const serialized = serializeFromAccount(account, [], connectedApps, [])
185
+
186
+ const apps = serialized.connectedApps as Record<string, unknown>[]
187
+ expect(apps[0]).toHaveProperty("appSecret", "my-secret-value")
188
+ })
189
+
190
+ it("should preserve appSecret through round-trip", () => {
191
+ const account = createPasskeyAccount()
192
+ const serialized = serializeFromAccount(account, [], [], [])
193
+
194
+ const raw = JSON.parse(JSON.stringify(serialized))
195
+ raw.connectedApps = [
196
+ {
197
+ appUrl: "https://example.com",
198
+ appName: "Test App",
199
+ lastConnectedAt: 1700000000000,
200
+ identityId: TEST_IDENTITY_ADDRESS_HEX,
201
+ appSecret: "preserved-secret",
202
+ },
203
+ ]
204
+
205
+ const result = deserializeAccountStateSnapshot(raw)
206
+
207
+ expect(result.success).toBe(true)
208
+ if (!result.success) return
209
+
210
+ expect(result.data.connectedApps[0].appSecret).toBe("preserved-secret")
211
+ })
212
+ })
213
+
214
+ // ============================================================================
215
+ // Edge Cases
216
+ // ============================================================================
217
+
218
+ describe("edge cases", () => {
219
+ it("should handle empty arrays for identities, connectedApps, and postageStamps", () => {
220
+ const account = createPasskeyAccount()
221
+ const serialized = serializeFromAccount(account, [], [], [])
222
+ const result = deserializeAccountStateSnapshot(
223
+ JSON.parse(JSON.stringify(serialized)),
224
+ )
225
+
226
+ expect(result.success).toBe(true)
227
+ if (!result.success) return
228
+
229
+ expect(result.data.identities).toEqual([])
230
+ expect(result.data.connectedApps).toEqual([])
231
+ expect(result.data.postageStamps).toEqual([])
232
+ })
233
+
234
+ it("should handle optional fields absent on identity", () => {
235
+ const identity = createIdentity({
236
+ settings: undefined,
237
+ defaultPostageStampBatchID: undefined,
238
+ })
239
+ const serialized = serializeFromAccount(
240
+ createPasskeyAccount(),
241
+ [identity],
242
+ [],
243
+ [],
244
+ )
245
+ const result = deserializeAccountStateSnapshot(
246
+ JSON.parse(JSON.stringify(serialized)),
247
+ )
248
+
249
+ expect(result.success).toBe(true)
250
+ if (!result.success) return
251
+
252
+ expect(result.data.identities[0].settings).toBeUndefined()
253
+ expect(result.data.identities[0].defaultPostageStampBatchID).toBeUndefined()
254
+ })
255
+
256
+ it("should handle optional fields absent on connected app", () => {
257
+ const app = createConnectedApp({
258
+ appIcon: undefined,
259
+ appDescription: undefined,
260
+ connectedUntil: undefined,
261
+ appSecret: undefined,
262
+ })
263
+ const serialized = serializeFromAccount(
264
+ createPasskeyAccount(),
265
+ [],
266
+ [app],
267
+ [],
268
+ )
269
+ const result = deserializeAccountStateSnapshot(
270
+ JSON.parse(JSON.stringify(serialized)),
271
+ )
272
+
273
+ expect(result.success).toBe(true)
274
+ if (!result.success) return
275
+
276
+ expect(result.data.connectedApps[0].appIcon).toBeUndefined()
277
+ expect(result.data.connectedApps[0].appDescription).toBeUndefined()
278
+ expect(result.data.connectedApps[0].connectedUntil).toBeUndefined()
279
+ })
280
+
281
+ it("should handle optional fields absent on postage stamp", () => {
282
+ const stamp = createPostageStamp({ batchTTL: undefined })
283
+ const serialized = serializeFromAccount(
284
+ createPasskeyAccount(),
285
+ [],
286
+ [],
287
+ [stamp],
288
+ )
289
+ const result = deserializeAccountStateSnapshot(
290
+ JSON.parse(JSON.stringify(serialized)),
291
+ )
292
+
293
+ expect(result.success).toBe(true)
294
+ if (!result.success) return
295
+
296
+ expect(result.data.postageStamps[0].batchTTL).toBeUndefined()
297
+ })
298
+
299
+ it("should handle optional defaultPostageStampBatchID absent on account metadata", () => {
300
+ const account = createPasskeyAccount({
301
+ defaultPostageStampBatchID: undefined,
302
+ })
303
+ const serialized = serializeFromAccount(account, [], [], [])
304
+ const result = deserializeAccountStateSnapshot(
305
+ JSON.parse(JSON.stringify(serialized)),
306
+ )
307
+
308
+ expect(result.success).toBe(true)
309
+ if (!result.success) return
310
+
311
+ expect(result.data.metadata.defaultPostageStampBatchID).toBeUndefined()
312
+ })
313
+
314
+ it("should handle multiple entities of each type", () => {
315
+ const account = createPasskeyAccount()
316
+ const identities = [
317
+ createIdentity({ id: TEST_IDENTITY_ADDRESS_HEX, name: "Identity One" }),
318
+ createIdentity({ id: TEST_IDENTITY_ADDRESS_2_HEX, name: "Identity Two" }),
319
+ createIdentity({ id: "3".repeat(40), name: "Identity Three" }),
320
+ ]
321
+ const connectedApps = [
322
+ createConnectedApp({
323
+ appUrl: "https://app1.example.com",
324
+ identityId: TEST_IDENTITY_ADDRESS_HEX,
325
+ }),
326
+ createConnectedApp({
327
+ appUrl: "https://app2.example.com",
328
+ identityId: TEST_IDENTITY_ADDRESS_2_HEX,
329
+ }),
330
+ ]
331
+ const postageStamps = [
332
+ createPostageStamp({ batchID: new BatchId(TEST_BATCH_ID_HEX) }),
333
+ createPostageStamp({ batchID: new BatchId(TEST_BATCH_ID_2_HEX) }),
334
+ ]
335
+
336
+ const serialized = serializeFromAccount(
337
+ account,
338
+ identities,
339
+ connectedApps,
340
+ postageStamps,
341
+ )
342
+ const result = deserializeAccountStateSnapshot(
343
+ JSON.parse(JSON.stringify(serialized)),
344
+ )
345
+
346
+ expect(result.success).toBe(true)
347
+ if (!result.success) return
348
+
349
+ expect(result.data.identities).toHaveLength(3)
350
+ expect(result.data.connectedApps).toHaveLength(2)
351
+ expect(result.data.postageStamps).toHaveLength(2)
352
+ })
353
+ })
354
+
355
+ // ============================================================================
356
+ // Invalid Data Rejection
357
+ // ============================================================================
358
+
359
+ describe("invalid data rejection", () => {
360
+ it("should reject wrong version number", () => {
361
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
362
+ const raw = JSON.parse(JSON.stringify(serialized))
363
+ raw.version = 2
364
+
365
+ const result = deserializeAccountStateSnapshot(raw)
366
+ expect(result.success).toBe(false)
367
+ })
368
+
369
+ it("should reject missing version", () => {
370
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
371
+ const raw = JSON.parse(JSON.stringify(serialized))
372
+ delete raw.version
373
+
374
+ const result = deserializeAccountStateSnapshot(raw)
375
+ expect(result.success).toBe(false)
376
+ })
377
+
378
+ it("should reject missing accountId", () => {
379
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
380
+ const raw = JSON.parse(JSON.stringify(serialized))
381
+ delete raw.accountId
382
+
383
+ const result = deserializeAccountStateSnapshot(raw)
384
+ expect(result.success).toBe(false)
385
+ })
386
+
387
+ it("should reject missing metadata", () => {
388
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
389
+ const raw = JSON.parse(JSON.stringify(serialized))
390
+ delete raw.metadata
391
+
392
+ const result = deserializeAccountStateSnapshot(raw)
393
+ expect(result.success).toBe(false)
394
+ })
395
+
396
+ it("should reject invalid accountId hex length", () => {
397
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
398
+ const raw = JSON.parse(JSON.stringify(serialized))
399
+ raw.accountId = "abc" // too short
400
+
401
+ const result = deserializeAccountStateSnapshot(raw)
402
+ expect(result.success).toBe(false)
403
+ })
404
+
405
+ it("should reject invalid BatchId hex length", () => {
406
+ const raw = {
407
+ version: 1,
408
+ timestamp: Date.now(),
409
+ accountId: TEST_ETH_ADDRESS_HEX,
410
+ metadata: {
411
+ accountName: "Test",
412
+ createdAt: 1700000000000,
413
+ lastModified: Date.now(),
414
+ },
415
+ identities: [],
416
+ connectedApps: [],
417
+ postageStamps: [
418
+ {
419
+ accountId: TEST_ETH_ADDRESS_HEX,
420
+ batchID: "short",
421
+ signerKey: TEST_PRIVATE_KEY_HEX,
422
+ utilization: 0,
423
+ usable: true,
424
+ depth: 20,
425
+ amount: "100000000",
426
+ bucketDepth: 16,
427
+ blockNumber: 12345678,
428
+ immutableFlag: false,
429
+ exists: true,
430
+ createdAt: 1700000000000,
431
+ },
432
+ ],
433
+ }
434
+
435
+ const result = deserializeAccountStateSnapshot(raw)
436
+ expect(result.success).toBe(false)
437
+ })
438
+
439
+ it("should reject invalid PrivateKey hex length", () => {
440
+ const raw = {
441
+ version: 1,
442
+ timestamp: Date.now(),
443
+ accountId: TEST_ETH_ADDRESS_HEX,
444
+ metadata: {
445
+ accountName: "Test",
446
+ createdAt: 1700000000000,
447
+ lastModified: Date.now(),
448
+ },
449
+ identities: [],
450
+ connectedApps: [],
451
+ postageStamps: [
452
+ {
453
+ accountId: TEST_ETH_ADDRESS_HEX,
454
+ batchID: TEST_BATCH_ID_HEX,
455
+ signerKey: "short",
456
+ utilization: 0,
457
+ usable: true,
458
+ depth: 20,
459
+ amount: "100000000",
460
+ bucketDepth: 16,
461
+ blockNumber: 12345678,
462
+ immutableFlag: false,
463
+ exists: true,
464
+ createdAt: 1700000000000,
465
+ },
466
+ ],
467
+ }
468
+
469
+ const result = deserializeAccountStateSnapshot(raw)
470
+ expect(result.success).toBe(false)
471
+ })
472
+
473
+ it("should reject number where string is expected", () => {
474
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
475
+ const raw = JSON.parse(JSON.stringify(serialized))
476
+ raw.metadata.accountName = 12345
477
+
478
+ const result = deserializeAccountStateSnapshot(raw)
479
+ expect(result.success).toBe(false)
480
+ })
481
+
482
+ it("should reject string where array is expected", () => {
483
+ const serialized = serializeFromAccount(createPasskeyAccount(), [], [], [])
484
+ const raw = JSON.parse(JSON.stringify(serialized))
485
+ raw.identities = "not-an-array"
486
+
487
+ const result = deserializeAccountStateSnapshot(raw)
488
+ expect(result.success).toBe(false)
489
+ })
490
+
491
+ it("should reject non-object input (string)", () => {
492
+ const result = deserializeAccountStateSnapshot("not-an-object")
493
+ expect(result.success).toBe(false)
494
+ })
495
+
496
+ it("should reject non-object input (number)", () => {
497
+ const result = deserializeAccountStateSnapshot(42)
498
+ expect(result.success).toBe(false)
499
+ })
500
+
501
+ it("should reject non-object input (undefined)", () => {
502
+ const result = deserializeAccountStateSnapshot(undefined)
503
+ expect(result.success).toBe(false)
504
+ })
505
+ })
506
+
507
+ // ============================================================================
508
+ // bee-js Type Conversions
509
+ // ============================================================================
510
+
511
+ describe("bee-js type conversions", () => {
512
+ it("should convert hex strings to EthAddress instances in identities", () => {
513
+ const raw = {
514
+ version: 1,
515
+ timestamp: Date.now(),
516
+ accountId: TEST_ETH_ADDRESS_HEX,
517
+ metadata: {
518
+ accountName: "Test",
519
+ createdAt: 1700000000000,
520
+ lastModified: Date.now(),
521
+ },
522
+ identities: [
523
+ {
524
+ id: TEST_IDENTITY_ADDRESS_HEX,
525
+ accountId: TEST_ETH_ADDRESS_HEX,
526
+ name: "Identity",
527
+ createdAt: 1700000000000,
528
+ },
529
+ ],
530
+ connectedApps: [],
531
+ postageStamps: [],
532
+ }
533
+
534
+ const result = deserializeAccountStateSnapshot(raw)
535
+
536
+ expect(result.success).toBe(true)
537
+ if (!result.success) return
538
+
539
+ expect(result.data.identities[0].accountId).toBeInstanceOf(EthAddress)
540
+ })
541
+
542
+ it("should convert hex strings to BatchId and PrivateKey instances", () => {
543
+ const raw = {
544
+ version: 1,
545
+ timestamp: Date.now(),
546
+ accountId: TEST_ETH_ADDRESS_HEX,
547
+ metadata: {
548
+ accountName: "Test",
549
+ createdAt: 1700000000000,
550
+ lastModified: Date.now(),
551
+ },
552
+ identities: [],
553
+ connectedApps: [],
554
+ postageStamps: [
555
+ {
556
+ accountId: TEST_ETH_ADDRESS_HEX,
557
+ batchID: TEST_BATCH_ID_HEX,
558
+ signerKey: TEST_PRIVATE_KEY_HEX,
559
+ utilization: 0,
560
+ usable: true,
561
+ depth: 20,
562
+ amount: "100000000",
563
+ bucketDepth: 16,
564
+ blockNumber: 12345678,
565
+ immutableFlag: false,
566
+ exists: true,
567
+ createdAt: 1700000000000,
568
+ },
569
+ ],
570
+ }
571
+
572
+ const result = deserializeAccountStateSnapshot(raw)
573
+
574
+ expect(result.success).toBe(true)
575
+ if (!result.success) return
576
+
577
+ expect(result.data.postageStamps[0].batchID).toBeInstanceOf(BatchId)
578
+ expect(result.data.postageStamps[0].batchID.toHex()).toBe(TEST_BATCH_ID_HEX)
579
+ expect(result.data.postageStamps[0].signerKey).toBeInstanceOf(PrivateKey)
580
+ expect(result.data.postageStamps[0].signerKey.toHex()).toBe(
581
+ TEST_PRIVATE_KEY_HEX,
582
+ )
583
+ })
584
+ })
585
+
586
+ // ============================================================================
587
+ // Schema Export
588
+ // ============================================================================
589
+
590
+ describe("AccountStateSnapshotSchemaV1", () => {
591
+ it("should be exported and usable for direct validation", () => {
592
+ expect(AccountStateSnapshotSchemaV1).toBeDefined()
593
+ expect(typeof AccountStateSnapshotSchemaV1.safeParse).toBe("function")
594
+ })
595
+ })
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Account State Snapshot Module
3
+ *
4
+ * Shared serialization for account state snapshots used by both
5
+ * file export (.swarmid) and Swarm sync flows.
6
+ *
7
+ * appSecret is included in snapshots so that backups preserve app connections.
8
+ * Since the backup is encrypted with the master key (and appSecret is
9
+ * deterministically derivable from it), this doesn't change the threat model.
10
+ */
11
+
12
+ import type { z } from "zod"
13
+ import { AccountStateSnapshotSchemaV1 } from "../schemas"
14
+ import type {
15
+ Identity,
16
+ ConnectedApp,
17
+ PostageStamp,
18
+ AccountMetadata,
19
+ AccountStateSnapshot,
20
+ } from "../schemas"
21
+ import {
22
+ serializeIdentity,
23
+ serializeConnectedApp,
24
+ serializePostageStamp,
25
+ } from "./storage-managers"
26
+
27
+ // Re-export schema and types for consumers
28
+ export { AccountStateSnapshotSchemaV1 } from "../schemas"
29
+ export type { AccountStateSnapshot } from "../schemas"
30
+
31
+ // ============================================================================
32
+ // Constants
33
+ // ============================================================================
34
+
35
+ const ACCOUNT_STATE_SNAPSHOT_VERSION = 1
36
+
37
+ // ============================================================================
38
+ // Types
39
+ // ============================================================================
40
+
41
+ export type AccountStateSnapshotResult =
42
+ | { success: true; data: AccountStateSnapshot }
43
+ | { success: false; error: z.ZodError }
44
+
45
+ // ============================================================================
46
+ // Serialize
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Serialize account data into a plain object suitable for JSON encoding.
51
+ */
52
+ export function serializeAccountStateSnapshot(input: {
53
+ accountId: string
54
+ metadata: AccountMetadata
55
+ identities: Identity[]
56
+ connectedApps: ConnectedApp[]
57
+ postageStamps: PostageStamp[]
58
+ timestamp: number
59
+ }): Record<string, unknown> {
60
+ return {
61
+ version: ACCOUNT_STATE_SNAPSHOT_VERSION,
62
+ timestamp: input.timestamp,
63
+ accountId: input.accountId,
64
+ metadata: {
65
+ accountName: input.metadata.accountName,
66
+ defaultPostageStampBatchID: input.metadata.defaultPostageStampBatchID,
67
+ createdAt: input.metadata.createdAt,
68
+ lastModified: input.metadata.lastModified,
69
+ },
70
+ identities: input.identities.map(serializeIdentity),
71
+ connectedApps: input.connectedApps.map(serializeConnectedApp),
72
+ postageStamps: input.postageStamps.map(serializePostageStamp),
73
+ }
74
+ }
75
+
76
+ // ============================================================================
77
+ // Deserialize
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Deserialize and validate an account state snapshot.
82
+ * Returns a discriminated union: success with parsed data, or failure with Zod error.
83
+ */
84
+ export function deserializeAccountStateSnapshot(
85
+ data: unknown,
86
+ ): AccountStateSnapshotResult {
87
+ const result = AccountStateSnapshotSchemaV1.safeParse(data)
88
+
89
+ if (!result.success) {
90
+ return { success: false, error: result.error }
91
+ }
92
+
93
+ return { success: true, data: result.data }
94
+ }