@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,442 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { BatchId } from "@ethersphere/bee-js"
3
+ import {
4
+ deriveBackupEncryptionKey,
5
+ encryptBackupPayload,
6
+ decryptBackupPayload,
7
+ buildBackupHeader,
8
+ createEncryptedExport,
9
+ decryptEncryptedExport,
10
+ parseEncryptedExportHeader,
11
+ EncryptedSwarmIdExportSchemaV1,
12
+ } from "./backup-encryption"
13
+ import {
14
+ TEST_ETH_ADDRESS_HEX,
15
+ TEST_ETH_ADDRESS_2_HEX,
16
+ TEST_ENCRYPTION_KEY_HEX,
17
+ DIFFERENT_ENCRYPTION_KEY_HEX,
18
+ createPasskeyAccount,
19
+ createEthereumAccount,
20
+ createAgentAccount,
21
+ createIdentity,
22
+ createConnectedApp,
23
+ createPostageStamp,
24
+ } from "../test-fixtures"
25
+
26
+ // ============================================================================
27
+ // Round-trip Tests
28
+ // ============================================================================
29
+
30
+ describe("round-trip: encrypt → decrypt for each account type", () => {
31
+ it("should round-trip a passkey account", async () => {
32
+ const account = createPasskeyAccount()
33
+ const identities = [createIdentity()]
34
+ const connectedApps = [createConnectedApp()]
35
+ const postageStamps = [createPostageStamp()]
36
+
37
+ const encrypted = await createEncryptedExport(
38
+ account,
39
+ identities,
40
+ connectedApps,
41
+ postageStamps,
42
+ account.swarmEncryptionKey,
43
+ )
44
+
45
+ expect(encrypted.accountType).toBe("passkey")
46
+ expect(encrypted.credentialId).toBe("credential-abc-123")
47
+ expect(typeof encrypted.ciphertext).toBe("string")
48
+ // No plaintext account data in the outer object
49
+ expect(encrypted).not.toHaveProperty("account")
50
+ expect(encrypted).not.toHaveProperty("identities")
51
+ expect(encrypted).not.toHaveProperty("postageStamps")
52
+
53
+ const result = await decryptEncryptedExport(
54
+ encrypted,
55
+ account.swarmEncryptionKey,
56
+ )
57
+
58
+ expect(result.success).toBe(true)
59
+ if (!result.success) return
60
+
61
+ expect(result.data.accountId).toBe(TEST_ETH_ADDRESS_HEX)
62
+ expect(result.data.metadata.accountName).toBe("Test Passkey Account")
63
+ expect(result.data.identities).toHaveLength(1)
64
+ expect(result.data.connectedApps).toHaveLength(1)
65
+ expect(result.data.postageStamps).toHaveLength(1)
66
+ expect(result.data.postageStamps[0].batchID).toBeInstanceOf(BatchId)
67
+ })
68
+
69
+ it("should round-trip an ethereum account", async () => {
70
+ const account = createEthereumAccount()
71
+ const identities = [createIdentity()]
72
+
73
+ const encrypted = await createEncryptedExport(
74
+ account,
75
+ identities,
76
+ [],
77
+ [],
78
+ account.swarmEncryptionKey,
79
+ )
80
+
81
+ expect(encrypted.accountType).toBe("ethereum")
82
+ expect(encrypted.ethereumAddress).toBe(TEST_ETH_ADDRESS_2_HEX)
83
+
84
+ const result = await decryptEncryptedExport(
85
+ encrypted,
86
+ account.swarmEncryptionKey,
87
+ )
88
+
89
+ expect(result.success).toBe(true)
90
+ if (!result.success) return
91
+
92
+ expect(result.data.accountId).toBe(TEST_ETH_ADDRESS_HEX)
93
+ expect(result.data.metadata.accountName).toBe("Test Ethereum Account")
94
+ })
95
+
96
+ it("should round-trip an agent account", async () => {
97
+ const account = createAgentAccount()
98
+ const identities = [createIdentity()]
99
+
100
+ const encrypted = await createEncryptedExport(
101
+ account,
102
+ identities,
103
+ [],
104
+ [],
105
+ account.swarmEncryptionKey,
106
+ )
107
+
108
+ expect(encrypted.accountType).toBe("agent")
109
+
110
+ const result = await decryptEncryptedExport(
111
+ encrypted,
112
+ account.swarmEncryptionKey,
113
+ )
114
+
115
+ expect(result.success).toBe(true)
116
+ if (!result.success) return
117
+
118
+ expect(result.data.metadata.accountName).toBe("Test Agent Account")
119
+ })
120
+
121
+ it("should survive JSON serialization (file I/O simulation)", async () => {
122
+ const account = createPasskeyAccount({
123
+ defaultPostageStampBatchID: new BatchId("c".repeat(64)),
124
+ })
125
+ const identities = [
126
+ createIdentity({ settings: { appSessionDuration: 3600 } }),
127
+ ]
128
+ const connectedApps = [createConnectedApp()]
129
+ const postageStamps = [createPostageStamp({ batchTTL: 86400 })]
130
+
131
+ const encrypted = await createEncryptedExport(
132
+ account,
133
+ identities,
134
+ connectedApps,
135
+ postageStamps,
136
+ account.swarmEncryptionKey,
137
+ )
138
+
139
+ // Simulate file write + read
140
+ const fileContent = JSON.stringify(encrypted, undefined, 2)
141
+ const fileData = JSON.parse(fileContent)
142
+
143
+ const result = await decryptEncryptedExport(
144
+ fileData,
145
+ account.swarmEncryptionKey,
146
+ )
147
+
148
+ expect(result.success).toBe(true)
149
+ if (!result.success) return
150
+
151
+ expect(result.data.metadata.defaultPostageStampBatchID).toBe("c".repeat(64))
152
+ expect(result.data.identities[0].settings?.appSessionDuration).toBe(3600)
153
+ expect(result.data.postageStamps[0].batchTTL).toBe(86400)
154
+ })
155
+ })
156
+
157
+ // ============================================================================
158
+ // appSecret Security Tests
159
+ // ============================================================================
160
+
161
+ describe("appSecret preservation in encrypted export", () => {
162
+ it("should preserve appSecret in connected apps through encrypted export", async () => {
163
+ const account = createPasskeyAccount()
164
+ const connectedApps = [createConnectedApp({ appSecret: "my-secret-value" })]
165
+
166
+ const encrypted = await createEncryptedExport(
167
+ account,
168
+ [],
169
+ connectedApps,
170
+ [],
171
+ account.swarmEncryptionKey,
172
+ )
173
+
174
+ const result = await decryptEncryptedExport(
175
+ encrypted,
176
+ account.swarmEncryptionKey,
177
+ )
178
+
179
+ expect(result.success).toBe(true)
180
+ if (!result.success) return
181
+
182
+ expect(result.data.connectedApps[0].appSecret).toBe("my-secret-value")
183
+ })
184
+ })
185
+
186
+ // ============================================================================
187
+ // Key Derivation Tests
188
+ // ============================================================================
189
+
190
+ describe("key derivation determinism", () => {
191
+ it("should derive the same CryptoKey from the same swarmEncryptionKey", async () => {
192
+ const key1 = await deriveBackupEncryptionKey(TEST_ENCRYPTION_KEY_HEX)
193
+ const key2 = await deriveBackupEncryptionKey(TEST_ENCRYPTION_KEY_HEX)
194
+
195
+ // Verify determinism: encrypt with key1, decrypt with key2
196
+ const plaintext = '{"determinism":"test"}'
197
+ const ciphertext = await encryptBackupPayload(plaintext, key1)
198
+ const decrypted = await decryptBackupPayload(ciphertext, key2)
199
+
200
+ expect(decrypted).toBe(plaintext)
201
+ })
202
+
203
+ it("should derive different keys from different swarmEncryptionKeys", async () => {
204
+ const key1 = await deriveBackupEncryptionKey(TEST_ENCRYPTION_KEY_HEX)
205
+ const key2 = await deriveBackupEncryptionKey(DIFFERENT_ENCRYPTION_KEY_HEX)
206
+
207
+ // Encrypt with key1, should fail to decrypt with key2
208
+ const ciphertext = await encryptBackupPayload('{"test":true}', key1)
209
+ await expect(decryptBackupPayload(ciphertext, key2)).rejects.toThrow()
210
+ })
211
+ })
212
+
213
+ // ============================================================================
214
+ // Wrong Key Rejection
215
+ // ============================================================================
216
+
217
+ describe("wrong key rejection", () => {
218
+ it("should fail to decrypt with a different swarmEncryptionKey", async () => {
219
+ const account = createPasskeyAccount()
220
+
221
+ const encrypted = await createEncryptedExport(
222
+ account,
223
+ [createIdentity()],
224
+ [],
225
+ [],
226
+ account.swarmEncryptionKey,
227
+ )
228
+
229
+ const result = await decryptEncryptedExport(
230
+ encrypted,
231
+ DIFFERENT_ENCRYPTION_KEY_HEX,
232
+ )
233
+
234
+ expect(result.success).toBe(false)
235
+ if (result.success) return
236
+ expect(result.error.issues[0].message).toContain("Decryption failed")
237
+ })
238
+
239
+ it("should fail at the payload level with wrong key", async () => {
240
+ const correctKey = await deriveBackupEncryptionKey(TEST_ENCRYPTION_KEY_HEX)
241
+ const wrongKey = await deriveBackupEncryptionKey(
242
+ DIFFERENT_ENCRYPTION_KEY_HEX,
243
+ )
244
+
245
+ const ciphertext = await encryptBackupPayload('{"test": true}', correctKey)
246
+
247
+ await expect(decryptBackupPayload(ciphertext, wrongKey)).rejects.toThrow()
248
+ })
249
+ })
250
+
251
+ // ============================================================================
252
+ // Header Construction Tests
253
+ // ============================================================================
254
+
255
+ describe("buildBackupHeader", () => {
256
+ it("should include credentialId for passkey accounts", () => {
257
+ const header = buildBackupHeader(createPasskeyAccount())
258
+
259
+ expect(header.version).toBe(1)
260
+ expect(header.accountType).toBe("passkey")
261
+ expect(header.accountName).toBe("Test Passkey Account")
262
+ expect(header.credentialId).toBe("credential-abc-123")
263
+ expect(typeof header.exportedAt).toBe("number")
264
+ // No ethereum-specific fields
265
+ expect(header).not.toHaveProperty("ethereumAddress")
266
+ expect(header).not.toHaveProperty("encryptedMasterKey")
267
+ expect(header).not.toHaveProperty("encryptionSalt")
268
+ })
269
+
270
+ it("should include ethereum-specific fields for ethereum accounts", () => {
271
+ const header = buildBackupHeader(createEthereumAccount())
272
+
273
+ expect(header.accountType).toBe("ethereum")
274
+ expect(header.ethereumAddress).toBe(TEST_ETH_ADDRESS_2_HEX)
275
+ // No passkey-specific fields
276
+ expect(header).not.toHaveProperty("credentialId")
277
+ })
278
+
279
+ it("should have no extra fields for agent accounts", () => {
280
+ const header = buildBackupHeader(createAgentAccount())
281
+
282
+ expect(header.accountType).toBe("agent")
283
+ expect(header).not.toHaveProperty("credentialId")
284
+ expect(header).not.toHaveProperty("ethereumAddress")
285
+ expect(header).not.toHaveProperty("encryptedMasterKey")
286
+ expect(header).not.toHaveProperty("encryptionSalt")
287
+ })
288
+ })
289
+
290
+ // ============================================================================
291
+ // Schema Validation Tests
292
+ // ============================================================================
293
+
294
+ describe("schema validation", () => {
295
+ it("should accept a valid passkey encrypted export", async () => {
296
+ const encrypted = await createEncryptedExport(
297
+ createPasskeyAccount(),
298
+ [],
299
+ [],
300
+ [],
301
+ TEST_ENCRYPTION_KEY_HEX,
302
+ )
303
+
304
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse(encrypted)
305
+ expect(result.success).toBe(true)
306
+ })
307
+
308
+ it("should accept a valid ethereum encrypted export", async () => {
309
+ const encrypted = await createEncryptedExport(
310
+ createEthereumAccount(),
311
+ [],
312
+ [],
313
+ [],
314
+ TEST_ENCRYPTION_KEY_HEX,
315
+ )
316
+
317
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse(encrypted)
318
+ expect(result.success).toBe(true)
319
+ })
320
+
321
+ it("should accept a valid agent encrypted export", async () => {
322
+ const encrypted = await createEncryptedExport(
323
+ createAgentAccount(),
324
+ [],
325
+ [],
326
+ [],
327
+ TEST_ENCRYPTION_KEY_HEX,
328
+ )
329
+
330
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse(encrypted)
331
+ expect(result.success).toBe(true)
332
+ })
333
+
334
+ it("should reject missing ciphertext", () => {
335
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse({
336
+ version: 1,
337
+ accountType: "passkey",
338
+ accountName: "Test",
339
+ credentialId: "cred-123",
340
+ exportedAt: Date.now(),
341
+ // missing ciphertext
342
+ })
343
+ expect(result.success).toBe(false)
344
+ })
345
+
346
+ it("should reject invalid accountType", () => {
347
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse({
348
+ version: 1,
349
+ accountType: "invalid",
350
+ accountName: "Test",
351
+ exportedAt: Date.now(),
352
+ ciphertext: "abc",
353
+ })
354
+ expect(result.success).toBe(false)
355
+ })
356
+
357
+ it("should reject wrong version number", () => {
358
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse({
359
+ version: 2,
360
+ accountType: "passkey",
361
+ accountName: "Test",
362
+ credentialId: "cred",
363
+ exportedAt: Date.now(),
364
+ ciphertext: "abc",
365
+ })
366
+ expect(result.success).toBe(false)
367
+ })
368
+ })
369
+
370
+ // ============================================================================
371
+ // parseEncryptedExportHeader Tests
372
+ // ============================================================================
373
+
374
+ describe("parseEncryptedExportHeader", () => {
375
+ it("should parse a valid passkey header", async () => {
376
+ const encrypted = await createEncryptedExport(
377
+ createPasskeyAccount(),
378
+ [],
379
+ [],
380
+ [],
381
+ TEST_ENCRYPTION_KEY_HEX,
382
+ )
383
+
384
+ const result = parseEncryptedExportHeader(encrypted)
385
+ expect(result.success).toBe(true)
386
+ if (!result.success) return
387
+ expect(result.header.accountType).toBe("passkey")
388
+ })
389
+
390
+ it("should reject non-object input (string)", () => {
391
+ const result = parseEncryptedExportHeader("not-an-object")
392
+ expect(result.success).toBe(false)
393
+ })
394
+
395
+ it("should reject non-object input (number)", () => {
396
+ const result = parseEncryptedExportHeader(42)
397
+ expect(result.success).toBe(false)
398
+ })
399
+
400
+ it("should reject non-object input (undefined)", () => {
401
+ const result = parseEncryptedExportHeader(undefined)
402
+ expect(result.success).toBe(false)
403
+ })
404
+
405
+ it("should reject null input", () => {
406
+ const result = parseEncryptedExportHeader(null)
407
+ expect(result.success).toBe(false)
408
+ })
409
+
410
+ it("should reject empty object", () => {
411
+ const result = parseEncryptedExportHeader({})
412
+ expect(result.success).toBe(false)
413
+ })
414
+ })
415
+
416
+ // ============================================================================
417
+ // Encrypt/Decrypt Payload Directly
418
+ // ============================================================================
419
+
420
+ describe("encryptBackupPayload / decryptBackupPayload", () => {
421
+ it("should encrypt and decrypt a payload", async () => {
422
+ const key = await deriveBackupEncryptionKey(TEST_ENCRYPTION_KEY_HEX)
423
+ const plaintext = '{"hello":"world"}'
424
+
425
+ const ciphertext = await encryptBackupPayload(plaintext, key)
426
+ expect(typeof ciphertext).toBe("string")
427
+ expect(ciphertext).not.toBe(plaintext)
428
+
429
+ const decrypted = await decryptBackupPayload(ciphertext, key)
430
+ expect(decrypted).toBe(plaintext)
431
+ })
432
+
433
+ it("should produce different ciphertext for same input (random IV)", async () => {
434
+ const key = await deriveBackupEncryptionKey(TEST_ENCRYPTION_KEY_HEX)
435
+ const plaintext = '{"test":true}'
436
+
437
+ const ct1 = await encryptBackupPayload(plaintext, key)
438
+ const ct2 = await encryptBackupPayload(plaintext, key)
439
+
440
+ expect(ct1).not.toBe(ct2)
441
+ })
442
+ })