@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,352 @@
1
+ /**
2
+ * Backup Encryption Module
3
+ *
4
+ * Provides AES-GCM-256 encryption for .swarmid backup files.
5
+ * The encryption key is derived from the account's swarmEncryptionKey
6
+ * (which is always persisted on all account types), so export requires
7
+ * NO re-authentication. Import requires auth to re-derive the key.
8
+ *
9
+ * Key chain: masterKey → swarmEncryptionKey (stored) → backupKey (HMAC-SHA256)
10
+ */
11
+
12
+ import { z } from "zod"
13
+ import { deriveSecret, hexToUint8Array } from "./key-derivation"
14
+ import {
15
+ serializeAccountStateSnapshot,
16
+ deserializeAccountStateSnapshot,
17
+ } from "./account-state-snapshot"
18
+ import type { AccountStateSnapshotResult } from "./account-state-snapshot"
19
+ import type { Account, Identity, ConnectedApp, PostageStamp } from "../schemas"
20
+
21
+ // ============================================================================
22
+ // Constants
23
+ // ============================================================================
24
+
25
+ const BACKUP_KEY_DERIVATION_CONTEXT = "swarm-id-backup-encryption-v1"
26
+ const BACKUP_VERSION = 1
27
+ const IV_LENGTH_BYTES = 12
28
+
29
+ // ============================================================================
30
+ // Schemas
31
+ // ============================================================================
32
+
33
+ const BackupHeaderBaseSchemaV1 = z.object({
34
+ version: z.literal(BACKUP_VERSION),
35
+ accountName: z.string(),
36
+ exportedAt: z.number(),
37
+ ciphertext: z.string(),
38
+ })
39
+
40
+ export const PasskeyBackupHeaderSchemaV1 = BackupHeaderBaseSchemaV1.extend({
41
+ accountType: z.literal("passkey"),
42
+ credentialId: z.string(),
43
+ })
44
+
45
+ /**
46
+ * Ethereum backup header fields (plaintext, not encrypted):
47
+ *
48
+ * - `ethereumAddress` — validates the user is connecting the correct wallet
49
+ * before attempting decryption.
50
+ *
51
+ * Key material is NOT stored in the export header. During import the user
52
+ * must enter their secret seed, and the master key is re-derived from
53
+ * `secretSeed + publicKey` via `deriveMasterKey()`.
54
+ */
55
+ export const EthereumBackupHeaderSchemaV1 = BackupHeaderBaseSchemaV1.extend({
56
+ accountType: z.literal("ethereum"),
57
+ ethereumAddress: z.string(),
58
+ })
59
+
60
+ export const AgentBackupHeaderSchemaV1 = BackupHeaderBaseSchemaV1.extend({
61
+ accountType: z.literal("agent"),
62
+ })
63
+
64
+ export const EncryptedSwarmIdExportSchemaV1 = z.discriminatedUnion(
65
+ "accountType",
66
+ [
67
+ PasskeyBackupHeaderSchemaV1,
68
+ EthereumBackupHeaderSchemaV1,
69
+ AgentBackupHeaderSchemaV1,
70
+ ],
71
+ )
72
+
73
+ // ============================================================================
74
+ // Types
75
+ // ============================================================================
76
+
77
+ export type PasskeyBackupHeader = z.infer<typeof PasskeyBackupHeaderSchemaV1>
78
+ export type EthereumBackupHeader = z.infer<typeof EthereumBackupHeaderSchemaV1>
79
+ export type AgentBackupHeader = z.infer<typeof AgentBackupHeaderSchemaV1>
80
+ export type EncryptedSwarmIdExport = z.infer<
81
+ typeof EncryptedSwarmIdExportSchemaV1
82
+ >
83
+
84
+ export type ParseHeaderResult =
85
+ | { success: true; header: EncryptedSwarmIdExport }
86
+ | { success: false; error: string }
87
+
88
+ // ============================================================================
89
+ // Key Derivation
90
+ // ============================================================================
91
+
92
+ /**
93
+ * Derive an AES-GCM-256 CryptoKey for backup encryption from the stored
94
+ * swarmEncryptionKey. Uses HMAC-SHA256 with a fixed context string.
95
+ */
96
+ export async function deriveBackupEncryptionKey(
97
+ swarmEncryptionKeyHex: string,
98
+ ): Promise<CryptoKey> {
99
+ const backupKeyHex = await deriveSecret(
100
+ swarmEncryptionKeyHex,
101
+ BACKUP_KEY_DERIVATION_CONTEXT,
102
+ )
103
+ const keyBytes = hexToUint8Array(backupKeyHex)
104
+
105
+ return crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, [
106
+ "encrypt",
107
+ "decrypt",
108
+ ])
109
+ }
110
+
111
+ // ============================================================================
112
+ // Encrypt / Decrypt Payload
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Encrypt a plaintext JSON string with AES-GCM-256.
117
+ * Returns base64-encoded [IV (12 bytes) || ciphertext+tag].
118
+ */
119
+ export async function encryptBackupPayload(
120
+ plaintextJson: string,
121
+ key: CryptoKey,
122
+ ): Promise<string> {
123
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES))
124
+ const encoded = new TextEncoder().encode(plaintextJson)
125
+
126
+ const ciphertextBuffer = await crypto.subtle.encrypt(
127
+ { name: "AES-GCM", iv },
128
+ key,
129
+ encoded,
130
+ )
131
+
132
+ // Concatenate IV + ciphertext+tag
133
+ const combined = new Uint8Array(iv.length + ciphertextBuffer.byteLength)
134
+ combined.set(iv, 0)
135
+ combined.set(new Uint8Array(ciphertextBuffer), iv.length)
136
+
137
+ return uint8ArrayToBase64(combined)
138
+ }
139
+
140
+ /**
141
+ * Decrypt a base64-encoded [IV (12 bytes) || ciphertext+tag] with AES-GCM-256.
142
+ * Returns the plaintext JSON string.
143
+ */
144
+ export async function decryptBackupPayload(
145
+ ciphertextBase64: string,
146
+ key: CryptoKey,
147
+ ): Promise<string> {
148
+ const combined = base64ToUint8Array(ciphertextBase64)
149
+ const iv = combined.slice(0, IV_LENGTH_BYTES)
150
+ const ciphertext = combined.slice(IV_LENGTH_BYTES)
151
+
152
+ const plaintextBuffer = await crypto.subtle.decrypt(
153
+ { name: "AES-GCM", iv },
154
+ key,
155
+ ciphertext,
156
+ )
157
+
158
+ return new TextDecoder().decode(plaintextBuffer)
159
+ }
160
+
161
+ // ============================================================================
162
+ // Header Construction
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Build the plaintext header fields for an encrypted backup,
167
+ * based on the account type.
168
+ *
169
+ * `accountName` and `exportedAt` are deliberately kept in the plaintext
170
+ * header: accountName helps the user identify which credential to use,
171
+ * and exportedAt lets them assess backup recency.
172
+ */
173
+ export type BackupHeaderWithoutCiphertext =
174
+ | Omit<PasskeyBackupHeader, "ciphertext">
175
+ | Omit<EthereumBackupHeader, "ciphertext">
176
+ | Omit<AgentBackupHeader, "ciphertext">
177
+
178
+ export function buildBackupHeader(
179
+ account: Account,
180
+ ): BackupHeaderWithoutCiphertext {
181
+ const base = {
182
+ version: BACKUP_VERSION as typeof BACKUP_VERSION,
183
+ accountName: account.name,
184
+ exportedAt: Date.now(),
185
+ }
186
+
187
+ if (account.type === "passkey") {
188
+ return {
189
+ ...base,
190
+ accountType: "passkey" as const,
191
+ credentialId: account.credentialId,
192
+ }
193
+ }
194
+
195
+ if (account.type === "ethereum") {
196
+ return {
197
+ ...base,
198
+ accountType: "ethereum" as const,
199
+ ethereumAddress: account.ethereumAddress.toHex(),
200
+ }
201
+ }
202
+
203
+ // agent — no extra fields
204
+ return { ...base, accountType: "agent" as const }
205
+ }
206
+
207
+ // ============================================================================
208
+ // High-Level API
209
+ // ============================================================================
210
+
211
+ /**
212
+ * Create a fully encrypted .swarmid export object.
213
+ *
214
+ * 1. Serializes account data to plaintext JSON via serializeAccountStateSnapshot
215
+ * 2. Derives an AES-GCM-256 key from swarmEncryptionKey
216
+ * 3. Encrypts the JSON payload
217
+ * 4. Builds a header with account metadata + ciphertext
218
+ */
219
+ export async function createEncryptedExport(
220
+ account: Account,
221
+ identities: Identity[],
222
+ connectedApps: ConnectedApp[],
223
+ postageStamps: PostageStamp[],
224
+ swarmEncryptionKeyHex: string,
225
+ ): Promise<EncryptedSwarmIdExport> {
226
+ const now = Date.now()
227
+ const exportData = serializeAccountStateSnapshot({
228
+ accountId: account.id.toHex(),
229
+ metadata: {
230
+ accountName: account.name,
231
+ defaultPostageStampBatchID: account.defaultPostageStampBatchID?.toHex(),
232
+ createdAt: account.createdAt,
233
+ lastModified: now,
234
+ },
235
+ identities,
236
+ connectedApps,
237
+ postageStamps,
238
+ timestamp: now,
239
+ })
240
+ const plaintextJson = JSON.stringify(exportData)
241
+ const key = await deriveBackupEncryptionKey(swarmEncryptionKeyHex)
242
+ const ciphertext = await encryptBackupPayload(plaintextJson, key)
243
+
244
+ const header = buildBackupHeader(account)
245
+ return EncryptedSwarmIdExportSchemaV1.parse({ ...header, ciphertext })
246
+ }
247
+
248
+ /**
249
+ * Decrypt an encrypted .swarmid export and return the parsed inner data.
250
+ *
251
+ * 1. Validates the encrypted header with Zod
252
+ * 2. Derives the AES-GCM-256 key from swarmEncryptionKey
253
+ * 3. Decrypts the ciphertext
254
+ * 4. Parses the inner plaintext via deserializeAccountStateSnapshot
255
+ */
256
+ export async function decryptEncryptedExport(
257
+ encryptedData: unknown,
258
+ swarmEncryptionKeyHex: string,
259
+ ): Promise<AccountStateSnapshotResult> {
260
+ const headerResult = parseEncryptedExportHeader(encryptedData)
261
+ if (!headerResult.success) {
262
+ return {
263
+ success: false,
264
+ error: new z.ZodError([
265
+ {
266
+ code: "custom",
267
+ message: headerResult.error,
268
+ path: [],
269
+ },
270
+ ]),
271
+ }
272
+ }
273
+
274
+ const key = await deriveBackupEncryptionKey(swarmEncryptionKeyHex)
275
+
276
+ let plaintextJson: string
277
+ try {
278
+ plaintextJson = await decryptBackupPayload(
279
+ headerResult.header.ciphertext,
280
+ key,
281
+ )
282
+ } catch {
283
+ return {
284
+ success: false,
285
+ error: new z.ZodError([
286
+ {
287
+ code: "custom",
288
+ message: "Decryption failed: wrong key or corrupted data",
289
+ path: ["ciphertext"],
290
+ },
291
+ ]),
292
+ }
293
+ }
294
+
295
+ let innerData: unknown
296
+ try {
297
+ innerData = JSON.parse(plaintextJson)
298
+ } catch {
299
+ return {
300
+ success: false,
301
+ error: new z.ZodError([
302
+ {
303
+ code: "custom",
304
+ message: "Decrypted data is not valid JSON",
305
+ path: ["ciphertext"],
306
+ },
307
+ ]),
308
+ }
309
+ }
310
+ return deserializeAccountStateSnapshot(innerData)
311
+ }
312
+
313
+ /**
314
+ * Parse and validate just the encrypted export header (without decrypting).
315
+ * Useful for reading account metadata before attempting decryption.
316
+ */
317
+ export function parseEncryptedExportHeader(data: unknown): ParseHeaderResult {
318
+ if (typeof data !== "object" || data === null) {
319
+ return { success: false, error: "Input must be a non-null object" }
320
+ }
321
+
322
+ const result = EncryptedSwarmIdExportSchemaV1.safeParse(data)
323
+ if (!result.success) {
324
+ return {
325
+ success: false,
326
+ error: result.error.issues.map((i) => i.message).join("; "),
327
+ }
328
+ }
329
+
330
+ return { success: true, header: result.data }
331
+ }
332
+
333
+ // ============================================================================
334
+ // Base64 Helpers
335
+ // ============================================================================
336
+
337
+ function uint8ArrayToBase64(bytes: Uint8Array): string {
338
+ let binary = ""
339
+ for (const byte of bytes) {
340
+ binary += String.fromCharCode(byte)
341
+ }
342
+ return btoa(binary)
343
+ }
344
+
345
+ function base64ToUint8Array(base64: string): Uint8Array {
346
+ const binary = atob(base64)
347
+ const bytes = new Uint8Array(binary.length)
348
+ for (let i = 0; i < binary.length; i++) {
349
+ bytes[i] = binary.charCodeAt(i)
350
+ }
351
+ return bytes
352
+ }