@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,3095 @@
1
+ import type {
2
+ ClientOptions,
3
+ ConnectOptions,
4
+ AuthStatus,
5
+ ConnectionInfo,
6
+ UploadResult,
7
+ FileData,
8
+ UploadOptions,
9
+ ActUploadOptions,
10
+ DownloadOptions,
11
+ RequestOptions,
12
+ Reference,
13
+ SOCReader,
14
+ SOCWriter,
15
+ SingleOwnerChunk,
16
+ SocRawUploadResult,
17
+ SocRawUploadResponseMessage,
18
+ SocRawUploadMessage,
19
+ SocGetOwnerMessage,
20
+ SocGetOwnerResponseMessage,
21
+ FeedReaderOptions,
22
+ FeedWriterOptions,
23
+ FeedReader,
24
+ FeedWriter,
25
+ EpochFeedDownloadReferenceMessage,
26
+ EpochFeedDownloadReferenceResponseMessage,
27
+ EpochFeedUploadReferenceMessage,
28
+ EpochFeedUploadReferenceResponseMessage,
29
+ FeedGetOwnerMessage,
30
+ FeedGetOwnerResponseMessage,
31
+ EpochFeedDownloadOptions,
32
+ EpochFeedDownloadPayloadResult,
33
+ EpochFeedDownloadReferenceResult,
34
+ EpochFeedUploadOptions,
35
+ EpochFeedUploadResult,
36
+ SequentialFeedReaderOptions,
37
+ SequentialFeedWriterOptions,
38
+ SequentialFeedUpdateOptions,
39
+ SequentialFeedUploadOptions,
40
+ SequentialFeedDownloadRawOptions,
41
+ SequentialFeedUploadRawOptions,
42
+ SequentialFeedPayloadResult,
43
+ SequentialFeedReferenceResult,
44
+ SequentialFeedUploadResult,
45
+ SequentialFeedReader,
46
+ SequentialFeedWriter,
47
+ SequentialFeedGetOwnerMessage,
48
+ SequentialFeedGetOwnerResponseMessage,
49
+ SequentialFeedDownloadPayloadMessage,
50
+ SequentialFeedDownloadPayloadResponseMessage,
51
+ SequentialFeedDownloadRawPayloadMessage,
52
+ SequentialFeedDownloadRawPayloadResponseMessage,
53
+ SequentialFeedDownloadReferenceMessage,
54
+ SequentialFeedDownloadReferenceResponseMessage,
55
+ SequentialFeedUploadPayloadMessage,
56
+ SequentialFeedUploadPayloadResponseMessage,
57
+ SequentialFeedUploadRawPayloadMessage,
58
+ SequentialFeedUploadRawPayloadResponseMessage,
59
+ SequentialFeedUploadReferenceMessage,
60
+ SequentialFeedUploadReferenceResponseMessage,
61
+ CreateFeedManifestMessage,
62
+ CreateFeedManifestResponseMessage,
63
+ SocDownloadMessage,
64
+ SocDownloadResponseMessage,
65
+ SocRawDownloadMessage,
66
+ SocRawDownloadResponseMessage,
67
+ SocUploadResult,
68
+ SocUploadResponseMessage,
69
+ SocUploadMessage,
70
+ ParentToIframeMessage,
71
+ IframeToParentMessage,
72
+ AppMetadata,
73
+ ButtonConfig,
74
+ PostageBatch,
75
+ } from "./types"
76
+ import {
77
+ IframeToParentMessageSchema,
78
+ ParentToIframeMessageSchema,
79
+ AppMetadataSchema,
80
+ } from "./types"
81
+ import { EthAddress, Identifier, PrivateKey, Topic } from "@ethersphere/bee-js"
82
+ import { uint8ArrayToHex } from "./utils/key-derivation"
83
+
84
+ const DEFAULT_TIMEOUT_MS = 30000
85
+ const DEFAULT_INITIALIZATION_TIMEOUT_MS = 30000
86
+
87
+ /**
88
+ * Main client library for integrating Swarm ID authentication and storage capabilities
89
+ * into web applications.
90
+ *
91
+ * SwarmIdClient enables parent windows to interact with a Swarm ID iframe proxy,
92
+ * providing secure authentication, identity management, and data upload/download
93
+ * functionality to the Swarm decentralized storage network.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const client = new SwarmIdClient({
98
+ * iframeOrigin: 'https://swarm-id.example.com',
99
+ * metadata: {
100
+ * name: 'My App',
101
+ * description: 'A decentralized application'
102
+ * },
103
+ * onAuthChange: (authenticated) => {
104
+ * console.log('Auth status changed:', authenticated)
105
+ * }
106
+ * })
107
+ *
108
+ * await client.initialize()
109
+ *
110
+ * const status = await client.checkAuthStatus()
111
+ * if (status.authenticated) {
112
+ * const result = await client.uploadData(new Uint8Array([1, 2, 3]))
113
+ * console.log('Uploaded with reference:', result.reference)
114
+ * }
115
+ * ```
116
+ */
117
+ export class SwarmIdClient {
118
+ private iframe: HTMLIFrameElement | undefined
119
+ private iframeOrigin: string
120
+ private iframePath: string
121
+ private timeout: number
122
+ private initializationTimeout: number
123
+ private onAuthChange?: (authenticated: boolean) => void
124
+ private popupMode: "popup" | "window"
125
+ private metadata: AppMetadata
126
+ private buttonConfig?: ButtonConfig
127
+ private containerId?: string
128
+ private ready: boolean = false
129
+ private readyPromise: Promise<void>
130
+ private readyResolve?: () => void
131
+ private readyReject?: (error: Error) => void
132
+ private pendingRequests: Map<
133
+ string,
134
+ {
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ resolve: (value: any) => void
137
+ reject: (error: Error) => void
138
+ timeoutId: NodeJS.Timeout
139
+ }
140
+ > = new Map()
141
+ private requestIdCounter = 0
142
+ private messageListener: ((event: MessageEvent) => void) | undefined
143
+ private proxyInitializedPromise: Promise<void>
144
+ private proxyInitializedResolve?: () => void
145
+ private proxyInitializedReject?: (error: Error) => void
146
+
147
+ /**
148
+ * Creates a new SwarmIdClient instance.
149
+ *
150
+ * @param options - Configuration options for the client
151
+ * @param options.iframeOrigin - The origin URL where the Swarm ID proxy iframe is hosted
152
+ * @param options.iframePath - The path to the proxy iframe (defaults to "/proxy")
153
+ * @param options.timeout - Request timeout in milliseconds (defaults to 30000)
154
+ * @param options.onAuthChange - Callback function invoked when authentication status changes
155
+ * @param options.popupMode - How to display the authentication popup: "popup" or "window" (defaults to "window")
156
+ * @param options.metadata - Application metadata shown to users during authentication
157
+ * @param options.metadata.name - Application name (1-100 characters)
158
+ * @param options.metadata.description - Optional application description (max 500 characters)
159
+ * @param options.metadata.icon - Optional application icon as a data URL (SVG or PNG, max 4KB)
160
+ * @param options.buttonConfig - Button configuration for the authentication UI (optional)
161
+ * @param options.buttonConfig.connectText - Text for the connect button (optional)
162
+ * @param options.buttonConfig.disconnectText - Text for the disconnect button (optional)
163
+ * @param options.buttonConfig.loadingText - Text shown during loading (optional)
164
+ * @param options.buttonConfig.backgroundColor - Background color for buttons (optional)
165
+ * @param options.buttonConfig.color - Text color for buttons (optional)
166
+ * @param options.buttonConfig.borderRadius - Border radius for buttons and iframe (optional)
167
+ * @param options.containerId - ID of container element to place iframe in (optional)
168
+ * @throws {Error} If the provided app metadata is invalid
169
+ */
170
+ constructor(options: ClientOptions) {
171
+ this.iframeOrigin = options.iframeOrigin
172
+ this.iframePath = options.iframePath || "/proxy"
173
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS
174
+ this.initializationTimeout =
175
+ options.initializationTimeout ?? DEFAULT_INITIALIZATION_TIMEOUT_MS
176
+ this.onAuthChange = options.onAuthChange
177
+ this.popupMode = options.popupMode || "window"
178
+ this.metadata = options.metadata
179
+ this.buttonConfig = options.buttonConfig
180
+ this.containerId = options.containerId
181
+
182
+ // Validate metadata
183
+ try {
184
+ AppMetadataSchema.parse(this.metadata)
185
+ } catch (error) {
186
+ throw new Error(`Invalid app metadata: ${error}`)
187
+ }
188
+
189
+ // Create promise that resolves when iframe is ready
190
+ this.readyPromise = new Promise<void>((resolve, reject) => {
191
+ this.readyResolve = resolve
192
+ this.readyReject = reject
193
+
194
+ // Timeout if proxy doesn't respond
195
+ setTimeout(() => {
196
+ reject(
197
+ new Error(
198
+ `Proxy initialization timeout - proxy did not respond within ${this.initializationTimeout}ms`,
199
+ ),
200
+ )
201
+ }, this.initializationTimeout)
202
+ })
203
+
204
+ // Create promise for proxyInitialized message
205
+ this.proxyInitializedPromise = new Promise<void>((resolve, reject) => {
206
+ this.proxyInitializedResolve = resolve
207
+ this.proxyInitializedReject = reject
208
+
209
+ // Timeout if proxy doesn't send proxyInitialized
210
+ setTimeout(() => {
211
+ if (this.proxyInitializedReject) {
212
+ this.proxyInitializedReject(
213
+ new Error(
214
+ `Proxy initialization timeout - proxy did not signal readiness within ${this.initializationTimeout}ms`,
215
+ ),
216
+ )
217
+ }
218
+ }, this.initializationTimeout)
219
+ })
220
+
221
+ this.setupMessageListener()
222
+ }
223
+
224
+ /**
225
+ * Initializes the client by creating and embedding the proxy iframe.
226
+ *
227
+ * This method must be called before using any other client methods.
228
+ * It creates a hidden iframe, waits for the proxy to initialize,
229
+ * identifies the parent application to the proxy, and waits for
230
+ * the proxy to signal readiness.
231
+ *
232
+ * @returns A promise that resolves when the client is fully initialized
233
+ * @throws {Error} If the client is already initialized
234
+ * @throws {Error} If the iframe fails to load
235
+ * @throws {Error} If the proxy does not respond within the timeout period (30 seconds)
236
+ * @throws {Error} If origin validation fails on the proxy side
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const client = new SwarmIdClient({ ... })
241
+ * try {
242
+ * await client.initialize()
243
+ * console.log('Client ready')
244
+ * } catch (error) {
245
+ * console.error('Failed to initialize:', error)
246
+ * }
247
+ * ```
248
+ */
249
+ async initialize(): Promise<void> {
250
+ if (this.iframe) {
251
+ throw new Error("SwarmIdClient already initialized")
252
+ }
253
+
254
+ // Create iframe for proxy
255
+ this.iframe = document.createElement("iframe")
256
+ this.iframe.src = `${this.iframeOrigin}${this.iframePath}`
257
+
258
+ // Common iframe styles
259
+ this.iframe.style.border = "none"
260
+ this.iframe.style.backgroundColor = "transparent"
261
+ this.iframe.style.borderRadius = this.buttonConfig?.borderRadius || "0"
262
+
263
+ // Determine where to place the iframe
264
+ let containerElement: HTMLElement | undefined
265
+ if (this.containerId) {
266
+ containerElement = document.getElementById(this.containerId) || undefined
267
+ if (!containerElement) {
268
+ throw new Error(
269
+ `Container element with ID "${this.containerId}" not found`,
270
+ )
271
+ }
272
+
273
+ // Fill the container
274
+ this.iframe.style.width = "100%"
275
+ this.iframe.style.height = "100%"
276
+ this.iframe.style.display = "block"
277
+ } else {
278
+ // Default: fixed position in bottom-right corner (hidden by default)
279
+ this.iframe.style.display = "none"
280
+ this.iframe.style.position = "fixed"
281
+ this.iframe.style.bottom = "20px"
282
+ this.iframe.style.right = "20px"
283
+ this.iframe.style.width = "300px"
284
+ this.iframe.style.height = "50px"
285
+ this.iframe.style.zIndex = "999999"
286
+ }
287
+
288
+ // Wait for iframe to load
289
+ await new Promise<void>((resolve, reject) => {
290
+ this.iframe!.onload = () => resolve()
291
+ this.iframe!.onerror = () =>
292
+ reject(new Error("Failed to load Swarm ID iframe"))
293
+
294
+ // Append to container or body
295
+ if (containerElement) {
296
+ containerElement.appendChild(this.iframe!)
297
+ } else {
298
+ document.body.appendChild(this.iframe!)
299
+ }
300
+ })
301
+
302
+ // Wait for proxy to signal it's ready
303
+ await this.proxyInitializedPromise
304
+
305
+ // Identify ourselves to the iframe
306
+ this.sendMessage({
307
+ type: "parentIdentify",
308
+ popupMode: this.popupMode,
309
+ metadata: this.metadata,
310
+ buttonConfig: this.buttonConfig,
311
+ })
312
+
313
+ // Wait for iframe to be ready
314
+ await this.readyPromise
315
+ }
316
+
317
+ /**
318
+ * Setup message listener for iframe responses
319
+ */
320
+ private setupMessageListener(): void {
321
+ this.messageListener = (event: MessageEvent) => {
322
+ // Handle proxyInitialized BEFORE any validation to avoid race condition
323
+ // This message is sent immediately when iframe loads and uses wildcard origin
324
+ if (event.data?.type === "proxyInitialized") {
325
+ // Security: Verify message is from OUR iframe (not another window/iframe)
326
+ if (this.iframe && event.source === this.iframe.contentWindow) {
327
+ if (this.proxyInitializedResolve) {
328
+ this.proxyInitializedResolve()
329
+ this.proxyInitializedResolve = undefined // Prevent double resolution
330
+ }
331
+ } else {
332
+ console.warn(
333
+ "[SwarmIdClient] Rejected proxyInitialized from unknown source",
334
+ )
335
+ }
336
+ return
337
+ }
338
+
339
+ // Validate origin (extract just origin part, ignoring any path in iframeOrigin)
340
+ const expectedOrigin = new URL(this.iframeOrigin).origin
341
+ if (event.origin !== expectedOrigin) {
342
+ console.warn(
343
+ "[SwarmIdClient] Rejected message from unauthorized origin:",
344
+ event.origin,
345
+ )
346
+ return
347
+ }
348
+
349
+ // Parse and validate message
350
+ let message: IframeToParentMessage
351
+ try {
352
+ message = IframeToParentMessageSchema.parse(event.data)
353
+ } catch (error) {
354
+ console.warn(
355
+ "[SwarmIdClient] Invalid message format:",
356
+ event.data,
357
+ error,
358
+ )
359
+ return
360
+ }
361
+
362
+ this.handleIframeMessage(message)
363
+ }
364
+
365
+ window.addEventListener("message", this.messageListener)
366
+ }
367
+
368
+ /**
369
+ * Handle messages from iframe
370
+ */
371
+ private handleIframeMessage(message: IframeToParentMessage): void {
372
+ switch (message.type) {
373
+ case "proxyReady":
374
+ this.ready = true
375
+ if (this.readyResolve) {
376
+ this.readyResolve()
377
+ }
378
+ // Always show iframe - it will display login or disconnect button
379
+ if (this.iframe) {
380
+ this.iframe.style.display = "block"
381
+ }
382
+ if (this.onAuthChange) {
383
+ this.onAuthChange(message.authenticated)
384
+ }
385
+ break
386
+
387
+ case "authStatusResponse":
388
+ // Always show iframe - it will display login or disconnect button
389
+ if (this.iframe) {
390
+ this.iframe.style.display = "block"
391
+ }
392
+ if (this.onAuthChange) {
393
+ this.onAuthChange(message.authenticated)
394
+ }
395
+ // Handle as response if there's a matching request
396
+ if ("requestId" in message) {
397
+ const pending = this.pendingRequests.get(message.requestId)
398
+ if (pending) {
399
+ clearTimeout(pending.timeoutId)
400
+ this.pendingRequests.delete(message.requestId)
401
+ pending.resolve(message)
402
+ }
403
+ }
404
+ break
405
+
406
+ case "authSuccess":
407
+ // Keep iframe visible - it will now show disconnect button
408
+ if (this.iframe) {
409
+ this.iframe.style.display = "block"
410
+ }
411
+ if (this.onAuthChange) {
412
+ this.onAuthChange(true)
413
+ }
414
+ break
415
+
416
+ case "initError":
417
+ // Initialization error from proxy (e.g., origin validation failed)
418
+ console.error(
419
+ "[SwarmIdClient] Proxy initialization error:",
420
+ message.error,
421
+ )
422
+ if (this.readyReject) {
423
+ this.readyReject(new Error(message.error))
424
+ }
425
+ break
426
+
427
+ case "disconnectResponse":
428
+ // Handle disconnect response
429
+ if (this.onAuthChange) {
430
+ this.onAuthChange(false)
431
+ }
432
+ // Handle as response if there's a matching request
433
+ if ("requestId" in message) {
434
+ const pending = this.pendingRequests.get(message.requestId)
435
+ if (pending) {
436
+ clearTimeout(pending.timeoutId)
437
+ this.pendingRequests.delete(message.requestId)
438
+ pending.resolve(message)
439
+ }
440
+ }
441
+ break
442
+
443
+ case "error": {
444
+ const pending = this.pendingRequests.get(message.requestId)
445
+ if (pending) {
446
+ clearTimeout(pending.timeoutId)
447
+ this.pendingRequests.delete(message.requestId)
448
+ pending.reject(new Error(message.error))
449
+ }
450
+ break
451
+ }
452
+
453
+ default:
454
+ // Handle response messages with requestId
455
+ if ("requestId" in message) {
456
+ const pending = this.pendingRequests.get(message.requestId)
457
+ if (pending) {
458
+ clearTimeout(pending.timeoutId)
459
+ this.pendingRequests.delete(message.requestId)
460
+ pending.resolve(message)
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Send message to iframe
468
+ */
469
+ private sendMessage(message: ParentToIframeMessage): void {
470
+ if (!this.iframe || !this.iframe.contentWindow) {
471
+ throw new Error("Iframe not initialized")
472
+ }
473
+
474
+ // Validate message before sending
475
+ try {
476
+ ParentToIframeMessageSchema.parse(message)
477
+ } catch (error) {
478
+ throw new Error(`Invalid message format: ${error}`)
479
+ }
480
+
481
+ this.iframe.contentWindow.postMessage(message, this.iframeOrigin)
482
+ }
483
+
484
+ /**
485
+ * Send request and wait for response
486
+ */
487
+ private async sendRequest<
488
+ TResponse,
489
+ TRequest extends ParentToIframeMessage & { requestId: string } =
490
+ ParentToIframeMessage & {
491
+ requestId: string
492
+ },
493
+ >(message: TRequest): Promise<TResponse> {
494
+ return new Promise((resolve, reject) => {
495
+ const timeoutId = setTimeout(() => {
496
+ this.pendingRequests.delete(message.requestId)
497
+ reject(new Error(`Request timeout after ${this.timeout}ms`))
498
+ }, this.timeout)
499
+
500
+ this.pendingRequests.set(message.requestId, {
501
+ resolve,
502
+ reject,
503
+ timeoutId,
504
+ })
505
+
506
+ this.sendMessage(message)
507
+ })
508
+ }
509
+
510
+ /**
511
+ * Generate unique request ID
512
+ */
513
+ private generateRequestId(): string {
514
+ return `req-${++this.requestIdCounter}-${Date.now()}`
515
+ }
516
+
517
+ private normalizeSocIdentifier(
518
+ identifier: Identifier | Uint8Array | string,
519
+ ): string {
520
+ if (identifier instanceof Identifier) {
521
+ return identifier.toHex()
522
+ }
523
+ if (identifier instanceof Uint8Array) {
524
+ return uint8ArrayToHex(identifier)
525
+ }
526
+ return identifier
527
+ }
528
+
529
+ private normalizeSocKey(key: Uint8Array | string): string {
530
+ if (key instanceof Uint8Array) {
531
+ return uint8ArrayToHex(key)
532
+ }
533
+ return key
534
+ }
535
+
536
+ private normalizeFeedTopic(
537
+ topic: Topic | Identifier | Uint8Array | string,
538
+ ): string {
539
+ if (topic instanceof Topic) {
540
+ return uint8ArrayToHex(topic.toUint8Array())
541
+ }
542
+ if (topic instanceof Identifier) {
543
+ return topic.toHex()
544
+ }
545
+ if (topic instanceof Uint8Array) {
546
+ return uint8ArrayToHex(topic)
547
+ }
548
+ return topic
549
+ }
550
+
551
+ private normalizeReference(reference: Uint8Array | string): string {
552
+ if (reference instanceof Uint8Array) {
553
+ return uint8ArrayToHex(reference)
554
+ }
555
+ return reference
556
+ }
557
+
558
+ private normalizeFeedTimestamp(value: bigint | number | string): string {
559
+ if (typeof value === "bigint") {
560
+ return value.toString()
561
+ }
562
+ if (typeof value === "number") {
563
+ return BigInt(Math.floor(value)).toString()
564
+ }
565
+ return value
566
+ }
567
+
568
+ private normalizeFeedIndex(value: bigint | number | string): string {
569
+ if (typeof value === "bigint") {
570
+ return value.toString()
571
+ }
572
+ if (typeof value === "number") {
573
+ return BigInt(Math.floor(value)).toString()
574
+ }
575
+ return value
576
+ }
577
+
578
+ private normalizePayload(data: Uint8Array | string): Uint8Array {
579
+ if (data instanceof Uint8Array) {
580
+ return data
581
+ }
582
+ return new TextEncoder().encode(data)
583
+ }
584
+
585
+ private socChunkFromResponse(response: {
586
+ data: Uint8Array
587
+ identifier: string
588
+ signature: string
589
+ span: number
590
+ payload: Uint8Array
591
+ address: Reference
592
+ owner: string
593
+ }): SingleOwnerChunk {
594
+ return {
595
+ data: response.data,
596
+ identifier: response.identifier,
597
+ signature: response.signature,
598
+ span: response.span,
599
+ payload: response.payload,
600
+ address: response.address,
601
+ owner: response.owner,
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Ensure client is initialized
607
+ */
608
+ private ensureReady(): void {
609
+ if (!this.ready) {
610
+ throw new Error("SwarmIdClient not initialized. Call initialize() first.")
611
+ }
612
+ }
613
+
614
+ // ============================================================================
615
+ // Authentication Methods
616
+ // ============================================================================
617
+
618
+ /**
619
+ * Returns the authentication iframe element.
620
+ *
621
+ * The iframe displays authentication UI based on the current auth status:
622
+ * - If not authenticated: shows a "Connect" button
623
+ * - If authenticated: shows identity info and a "Disconnect" button
624
+ *
625
+ * The iframe is positioned fixed in the bottom-right corner of the viewport.
626
+ *
627
+ * @returns The iframe element displaying the authentication UI
628
+ * @throws {Error} If the client is not initialized
629
+ * @throws {Error} If the iframe is not available
630
+ *
631
+ * @example
632
+ * ```typescript
633
+ * const iframe = client.getAuthIframe()
634
+ * // The iframe is already displayed; this returns a reference to it
635
+ * ```
636
+ */
637
+ getAuthIframe(): HTMLIFrameElement {
638
+ this.ensureReady()
639
+
640
+ if (!this.iframe) {
641
+ throw new Error("Iframe not initialized")
642
+ }
643
+
644
+ return this.iframe
645
+ }
646
+
647
+ /**
648
+ * Checks the current authentication status with the Swarm ID proxy.
649
+ *
650
+ * @returns A promise resolving to the authentication status object
651
+ * @returns return.authenticated - Whether the user is currently authenticated
652
+ * @returns return.origin - The origin that authenticated (if authenticated)
653
+ * @throws {Error} If the client is not initialized
654
+ * @throws {Error} If the request times out
655
+ *
656
+ * @example
657
+ * ```typescript
658
+ * const status = await client.checkAuthStatus()
659
+ * if (status.authenticated) {
660
+ * console.log('Authenticated from:', status.origin)
661
+ * }
662
+ * ```
663
+ */
664
+ async checkAuthStatus(): Promise<AuthStatus> {
665
+ this.ensureReady()
666
+ const requestId = this.generateRequestId()
667
+
668
+ const response = await this.sendRequest<{
669
+ type: "authStatusResponse"
670
+ requestId: string
671
+ authenticated: boolean
672
+ origin?: string
673
+ }>({
674
+ type: "checkAuth",
675
+ requestId,
676
+ })
677
+
678
+ return {
679
+ authenticated: response.authenticated,
680
+ origin: response.origin,
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Gets the current postage batch for the authenticated identity.
686
+ *
687
+ * Returns information about the postage stamp associated with the
688
+ * connected identity, including batch ID, utilization, depth, and TTL.
689
+ *
690
+ * @returns A promise resolving to the PostageBatch or undefined if none is configured
691
+ * @throws {Error} If the client is not initialized
692
+ * @throws {Error} If the request times out
693
+ *
694
+ * @example
695
+ * ```typescript
696
+ * const batch = await client.getPostageBatch()
697
+ * if (batch) {
698
+ * console.log('Batch ID:', batch.batchID)
699
+ * console.log('Utilization:', batch.utilization)
700
+ * console.log('Depth:', batch.depth)
701
+ * console.log('TTL:', batch.batchTTL)
702
+ * } else {
703
+ * console.log('No postage batch configured')
704
+ * }
705
+ * ```
706
+ */
707
+ async getPostageBatch(): Promise<PostageBatch | undefined> {
708
+ this.ensureReady()
709
+ const requestId = this.generateRequestId()
710
+
711
+ const response = await this.sendRequest<{
712
+ type: "getPostageBatchResponse"
713
+ requestId: string
714
+ postageBatch?: PostageBatch
715
+ error?: string
716
+ }>({
717
+ type: "getPostageBatch",
718
+ requestId,
719
+ })
720
+
721
+ if (response.error) {
722
+ throw new Error(response.error)
723
+ }
724
+
725
+ return response.postageBatch
726
+ }
727
+
728
+ /**
729
+ * Disconnects the current session and clears authentication data.
730
+ *
731
+ * After disconnection, the user will need to re-authenticate to perform
732
+ * uploads or access identity-related features. The {@link onAuthChange}
733
+ * callback will be invoked with `false`.
734
+ *
735
+ * @returns A promise that resolves when disconnection is complete
736
+ * @throws {Error} If the client is not initialized
737
+ * @throws {Error} If the disconnect operation fails
738
+ * @throws {Error} If the request times out
739
+ *
740
+ * @example
741
+ * ```typescript
742
+ * await client.disconnect()
743
+ * console.log('User logged out')
744
+ * ```
745
+ */
746
+ async disconnect(): Promise<void> {
747
+ this.ensureReady()
748
+ const requestId = this.generateRequestId()
749
+
750
+ const response = await this.sendRequest<{
751
+ type: "disconnectResponse"
752
+ requestId: string
753
+ success: boolean
754
+ }>({
755
+ type: "disconnect",
756
+ requestId,
757
+ })
758
+
759
+ if (!response.success) {
760
+ throw new Error("Failed to disconnect")
761
+ }
762
+
763
+ // Notify via auth change callback
764
+ if (this.onAuthChange) {
765
+ this.onAuthChange(false)
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Opens the Swarm ID authentication page in a new window.
771
+ *
772
+ * This method creates the same authentication URL as used by the iframe
773
+ * proxy and opens it in a new browser window. The user can authenticate
774
+ * with their Swarm ID, and the resulting authentication will be available
775
+ * to the client when they return.
776
+ *
777
+ * **Browser Compatibility:**
778
+ * - Production (Chrome/Firefox): Works immediately
779
+ * - Localhost (Chrome/Firefox): Works after iframe button grants Storage Access
780
+ * - Safari (any): Download-only mode — auth works, uploads disabled (ITP storage partitioning). Private mode sessions are ephemeral (lost when the private window closes).
781
+ *
782
+ * For localhost development with Chrome/Firefox, click the iframe button first
783
+ * to grant Storage Access. For Safari details, see https://github.com/snaha/swarm-id/issues/167
784
+ *
785
+ * @param options - Configuration options for the connect flow
786
+ * @param options.agent - When true, shows the agent sign-up option on the connect page
787
+ * @throws {Error} If the client is not initialized or the popup fails to open
788
+ *
789
+ * @example
790
+ * ```typescript
791
+ * const client = new SwarmIdClient({ ... })
792
+ * await client.initialize()
793
+ *
794
+ * // Open authentication page
795
+ * await client.connect()
796
+ *
797
+ * // Open with agent sign-up option visible
798
+ * await client.connect({ agent: true })
799
+ * ```
800
+ */
801
+ async connect(options: ConnectOptions = {}): Promise<void> {
802
+ this.ensureReady()
803
+ const requestId = this.generateRequestId()
804
+
805
+ const response = await this.sendRequest<{
806
+ type: "connectResponse"
807
+ requestId: string
808
+ success: boolean
809
+ }>({
810
+ type: "connect",
811
+ requestId,
812
+ agent: options.agent,
813
+ popupMode: options.popupMode,
814
+ })
815
+
816
+ if (!response.success) {
817
+ throw new Error("Failed to open authentication popup")
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Retrieves connection information including upload capability and identity details.
823
+ *
824
+ * Use this method to check if the user can upload data and to get
825
+ * information about the currently connected identity.
826
+ *
827
+ * @returns A promise resolving to the connection info object
828
+ * @returns return.canUpload - Whether the user can upload data (has valid postage stamp)
829
+ * @returns return.identity - The connected identity details (if authenticated)
830
+ * @returns return.identity.id - Unique identifier for the identity
831
+ * @returns return.identity.name - Display name of the identity
832
+ * @returns return.identity.address - Ethereum address associated with the identity
833
+ * @throws {Error} If the client is not initialized
834
+ * @throws {Error} If the request times out
835
+ *
836
+ * @example
837
+ * ```typescript
838
+ * const info = await client.getConnectionInfo()
839
+ * if (info.canUpload) {
840
+ * console.log('Ready to upload as:', info.identity?.name)
841
+ * } else {
842
+ * console.log('No postage stamp available')
843
+ * }
844
+ * ```
845
+ */
846
+ async getConnectionInfo(): Promise<ConnectionInfo> {
847
+ this.ensureReady()
848
+ const requestId = this.generateRequestId()
849
+
850
+ const response = await this.sendRequest<{
851
+ type: "connectionInfoResponse"
852
+ requestId: string
853
+ canUpload: boolean
854
+ storagePartitioned?: boolean
855
+ identity?: { id: string; name: string; address: string }
856
+ }>({
857
+ type: "getConnectionInfo",
858
+ requestId,
859
+ })
860
+
861
+ return {
862
+ canUpload: response.canUpload,
863
+ storagePartitioned: response.storagePartitioned,
864
+ identity: response.identity,
865
+ }
866
+ }
867
+
868
+ // ============================================================================
869
+ // Bee Connectivity Methods
870
+ // ============================================================================
871
+
872
+ /**
873
+ * Checks whether the Bee node is reachable.
874
+ *
875
+ * This method never throws an exception. If the client is not initialized,
876
+ * the request times out, or any other error occurs, it returns `false`.
877
+ *
878
+ * @returns A promise resolving to `true` if the Bee node is reachable, `false` otherwise
879
+ *
880
+ * @example
881
+ * ```typescript
882
+ * const connected = await client.isBeeConnected()
883
+ * if (connected) {
884
+ * console.log('Bee node is online')
885
+ * } else {
886
+ * console.log('Bee node is offline')
887
+ * }
888
+ * ```
889
+ */
890
+ async isBeeConnected(): Promise<boolean> {
891
+ try {
892
+ this.ensureReady()
893
+ const requestId = this.generateRequestId()
894
+
895
+ const response = await this.sendRequest<{
896
+ type: "isConnectedResponse"
897
+ requestId: string
898
+ connected: boolean
899
+ }>({
900
+ type: "isConnected",
901
+ requestId,
902
+ })
903
+
904
+ return response.connected
905
+ } catch {
906
+ return false
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Gets information about the Bee node configuration.
912
+ *
913
+ * This method retrieves the current Bee node's operating mode and feature flags.
914
+ * Use this to determine if deferred uploads are required (dev mode) or if direct
915
+ * uploads are available (production modes).
916
+ *
917
+ * @returns A promise resolving to the node info object
918
+ * @returns return.beeMode - The Bee node operating mode ("dev", "light", "full", "ultra-light")
919
+ * @returns return.chequebookEnabled - Whether the chequebook is enabled
920
+ * @returns return.swapEnabled - Whether SWAP is enabled
921
+ * @throws {Error} If the client is not initialized
922
+ * @throws {Error} If the Bee node is not reachable
923
+ * @throws {Error} If the request times out
924
+ *
925
+ * @example
926
+ * ```typescript
927
+ * const nodeInfo = await client.getNodeInfo()
928
+ * if (nodeInfo.beeMode === 'dev') {
929
+ * // Dev mode requires deferred uploads
930
+ * await client.uploadData(data, { deferred: true })
931
+ * } else {
932
+ * // Production modes can use direct uploads
933
+ * await client.uploadData(data, { deferred: false })
934
+ * }
935
+ * ```
936
+ */
937
+ async getNodeInfo(): Promise<{
938
+ beeMode: string
939
+ chequebookEnabled: boolean
940
+ swapEnabled: boolean
941
+ }> {
942
+ this.ensureReady()
943
+ const requestId = this.generateRequestId()
944
+
945
+ const response = await this.sendRequest<{
946
+ type: "getNodeInfoResponse"
947
+ requestId: string
948
+ beeMode: string
949
+ chequebookEnabled: boolean
950
+ swapEnabled: boolean
951
+ }>({
952
+ type: "getNodeInfo",
953
+ requestId,
954
+ })
955
+
956
+ return {
957
+ beeMode: response.beeMode,
958
+ chequebookEnabled: response.chequebookEnabled,
959
+ swapEnabled: response.swapEnabled,
960
+ }
961
+ }
962
+
963
+ // ============================================================================
964
+ // Data Upload/Download Methods
965
+ // ============================================================================
966
+
967
+ /**
968
+ * Uploads raw binary data to the Swarm network.
969
+ *
970
+ * The data is uploaded using the authenticated user's postage stamp.
971
+ * Progress can be tracked via the optional callback.
972
+ *
973
+ * @param data - The binary data to upload as a Uint8Array
974
+ * @param options - Optional upload configuration
975
+ * @param options.pin - Whether to pin the data locally (defaults to false)
976
+ * @param options.encrypt - Whether to encrypt the data (defaults to false)
977
+ * @param options.tag - Tag ID for tracking upload progress
978
+ * @param options.deferred - Whether to use deferred upload (defaults to false)
979
+ * @param options.redundancyLevel - Redundancy level from 0-4 for data availability
980
+ * @param options.onProgress - Optional callback for tracking upload progress
981
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
982
+ * @returns A promise resolving to the upload result
983
+ * @returns return.reference - The Swarm reference (hash) of the uploaded data
984
+ * @returns return.tagUid - The tag UID if a tag was created
985
+ * @throws {Error} If the client is not initialized
986
+ * @throws {Error} If the user is not authenticated or cannot upload
987
+ * @throws {Error} If the request times out
988
+ *
989
+ * @example
990
+ * ```typescript
991
+ * const data = new TextEncoder().encode('Hello, Swarm!')
992
+ * const result = await client.uploadData(data, {
993
+ * encrypt: true,
994
+ * onProgress: (progress) => {
995
+ * console.log(`Progress: ${progress.processed}/${progress.total}`)
996
+ * },
997
+ * })
998
+ * console.log('Reference:', result.reference)
999
+ * ```
1000
+ */
1001
+ async uploadData(
1002
+ data: Uint8Array,
1003
+ options?: UploadOptions,
1004
+ requestOptions?: RequestOptions,
1005
+ ): Promise<UploadResult> {
1006
+ this.ensureReady()
1007
+ const requestId = this.generateRequestId()
1008
+ const { onProgress, ...serializableOptions } = options ?? {}
1009
+
1010
+ // Setup progress listener if callback provided
1011
+ let progressListener: ((event: MessageEvent) => void) | undefined
1012
+ if (onProgress) {
1013
+ progressListener = (event: MessageEvent) => {
1014
+ if (event.origin !== new URL(this.iframeOrigin).origin) return
1015
+
1016
+ try {
1017
+ const message = IframeToParentMessageSchema.parse(event.data)
1018
+ if (
1019
+ message.type === "uploadProgress" &&
1020
+ message.requestId === requestId
1021
+ ) {
1022
+ onProgress({
1023
+ total: message.total,
1024
+ processed: message.processed,
1025
+ })
1026
+ }
1027
+ } catch {
1028
+ // Ignore invalid messages
1029
+ }
1030
+ }
1031
+ window.addEventListener("message", progressListener)
1032
+ }
1033
+
1034
+ try {
1035
+ const response = await this.sendRequest<{
1036
+ type: "uploadDataResponse"
1037
+ requestId: string
1038
+ reference: Reference
1039
+ tagUid?: number
1040
+ }>({
1041
+ type: "uploadData",
1042
+ requestId,
1043
+ data: new Uint8Array(data),
1044
+ options: serializableOptions,
1045
+ requestOptions,
1046
+ enableProgress: !!onProgress,
1047
+ })
1048
+
1049
+ return {
1050
+ reference: response.reference,
1051
+ tagUid: response.tagUid,
1052
+ }
1053
+ } finally {
1054
+ // Clean up progress listener
1055
+ if (progressListener) {
1056
+ window.removeEventListener("message", progressListener)
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Downloads raw binary data from the Swarm network.
1063
+ *
1064
+ * @param reference - The Swarm reference (hash) of the data to download.
1065
+ * Can be 64 hex chars (32 bytes) or 128 hex chars (64 bytes for encrypted)
1066
+ * @param options - Optional download configuration
1067
+ * @param options.redundancyStrategy - Strategy for handling redundancy (0-3)
1068
+ * @param options.fallback - Whether to use fallback retrieval
1069
+ * @param options.timeoutMs - Download timeout in milliseconds
1070
+ * @param options.actPublisher - ACT publisher for encrypted content
1071
+ * @param options.actHistoryAddress - ACT history address for encrypted content
1072
+ * @param options.actTimestamp - ACT timestamp for encrypted content
1073
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1074
+ * @returns A promise resolving to the downloaded data as a Uint8Array
1075
+ * @throws {Error} If the client is not initialized
1076
+ * @throws {Error} If the reference is not found
1077
+ * @throws {Error} If the request times out
1078
+ *
1079
+ * @example
1080
+ * ```typescript
1081
+ * const data = await client.downloadData('a1b2c3...') // 64 char hex reference
1082
+ * const text = new TextDecoder().decode(data)
1083
+ * console.log('Downloaded:', text)
1084
+ * ```
1085
+ */
1086
+ async downloadData(
1087
+ reference: Reference,
1088
+ options?: DownloadOptions,
1089
+ requestOptions?: RequestOptions,
1090
+ ): Promise<Uint8Array> {
1091
+ this.ensureReady()
1092
+ const requestId = this.generateRequestId()
1093
+
1094
+ const response = await this.sendRequest<{
1095
+ type: "downloadDataResponse"
1096
+ requestId: string
1097
+ data: Uint8Array
1098
+ }>({
1099
+ type: "downloadData",
1100
+ requestId,
1101
+ reference,
1102
+ options,
1103
+ requestOptions,
1104
+ })
1105
+
1106
+ return response.data
1107
+ }
1108
+
1109
+ // ============================================================================
1110
+ // File Upload/Download Methods
1111
+ // ============================================================================
1112
+
1113
+ /**
1114
+ * Uploads a file to the Swarm network.
1115
+ *
1116
+ * Accepts either a File object (from file input) or raw Uint8Array data.
1117
+ * When using a File object, the filename is automatically extracted unless
1118
+ * explicitly overridden.
1119
+ *
1120
+ * @param file - The file to upload (File object or Uint8Array)
1121
+ * @param name - Optional filename (extracted from File object if not provided)
1122
+ * @param options - Optional upload configuration
1123
+ * @param options.pin - Whether to pin the file locally (defaults to false)
1124
+ * @param options.encrypt - Whether to encrypt the file (defaults to false)
1125
+ * @param options.tag - Tag ID for tracking upload progress
1126
+ * @param options.deferred - Whether to use deferred upload (defaults to false)
1127
+ * @param options.redundancyLevel - Redundancy level from 0-4 for data availability
1128
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1129
+ * @returns A promise resolving to the upload result
1130
+ * @returns return.reference - The Swarm reference (hash) of the uploaded file
1131
+ * @returns return.tagUid - The tag UID if a tag was created
1132
+ * @throws {Error} If the client is not initialized
1133
+ * @throws {Error} If the user is not authenticated or cannot upload
1134
+ * @throws {Error} If the request times out
1135
+ *
1136
+ * @example
1137
+ * ```typescript
1138
+ * // From file input
1139
+ * const fileInput = document.querySelector('input[type="file"]')
1140
+ * const file = fileInput.files[0]
1141
+ * const result = await client.uploadFile(file)
1142
+ *
1143
+ * // From Uint8Array with custom name
1144
+ * const data = new Uint8Array([...])
1145
+ * const result = await client.uploadFile(data, 'document.pdf')
1146
+ * ```
1147
+ */
1148
+ async uploadFile(
1149
+ file: File | Uint8Array,
1150
+ name?: string,
1151
+ options?: UploadOptions,
1152
+ requestOptions?: RequestOptions,
1153
+ ): Promise<UploadResult> {
1154
+ this.ensureReady()
1155
+ const requestId = this.generateRequestId()
1156
+ const { onProgress: _onProgress, ...serializableOptions } = options ?? {}
1157
+
1158
+ let data: Uint8Array<ArrayBuffer>
1159
+ let fileName: string | undefined = name
1160
+
1161
+ if (file instanceof File) {
1162
+ const buffer = await file.arrayBuffer()
1163
+ data = new Uint8Array(buffer)
1164
+ fileName = fileName || file.name
1165
+ } else {
1166
+ data = new Uint8Array(
1167
+ file.buffer.slice(0),
1168
+ file.byteOffset,
1169
+ file.byteLength,
1170
+ )
1171
+ }
1172
+
1173
+ const response = await this.sendRequest<{
1174
+ type: "uploadFileResponse"
1175
+ requestId: string
1176
+ reference: Reference
1177
+ tagUid?: number
1178
+ }>({
1179
+ type: "uploadFile",
1180
+ requestId,
1181
+ data,
1182
+ name: fileName,
1183
+ options: serializableOptions,
1184
+ requestOptions,
1185
+ })
1186
+
1187
+ return {
1188
+ reference: response.reference,
1189
+ tagUid: response.tagUid,
1190
+ }
1191
+ }
1192
+
1193
+ /**
1194
+ * Downloads a file from the Swarm network.
1195
+ *
1196
+ * Returns both the file data and its original filename (if available).
1197
+ * For manifest references, an optional path can be specified to retrieve
1198
+ * a specific file from the manifest.
1199
+ *
1200
+ * @param reference - The Swarm reference (hash) of the file to download
1201
+ * @param path - Optional path within a manifest to retrieve a specific file
1202
+ * @param options - Optional download configuration
1203
+ * @param options.redundancyStrategy - Strategy for handling redundancy (0-3)
1204
+ * @param options.fallback - Whether to use fallback retrieval
1205
+ * @param options.timeoutMs - Download timeout in milliseconds
1206
+ * @param options.actPublisher - ACT publisher for encrypted content
1207
+ * @param options.actHistoryAddress - ACT history address for encrypted content
1208
+ * @param options.actTimestamp - ACT timestamp for encrypted content
1209
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1210
+ * @returns A promise resolving to the file data object
1211
+ * @returns return.name - The filename
1212
+ * @returns return.data - The file contents as a Uint8Array
1213
+ * @throws {Error} If the client is not initialized
1214
+ * @throws {Error} If the reference is not found
1215
+ * @throws {Error} If the request times out
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * const file = await client.downloadFile('a1b2c3...')
1220
+ * console.log('Filename:', file.name)
1221
+ *
1222
+ * // Create download link
1223
+ * const blob = new Blob([file.data])
1224
+ * const url = URL.createObjectURL(blob)
1225
+ * ```
1226
+ */
1227
+ async downloadFile(
1228
+ reference: Reference,
1229
+ path?: string,
1230
+ options?: DownloadOptions,
1231
+ requestOptions?: RequestOptions,
1232
+ ): Promise<FileData> {
1233
+ this.ensureReady()
1234
+ const requestId = this.generateRequestId()
1235
+
1236
+ const response = await this.sendRequest<{
1237
+ type: "downloadFileResponse"
1238
+ requestId: string
1239
+ name: string
1240
+ data: number[]
1241
+ }>({
1242
+ type: "downloadFile",
1243
+ requestId,
1244
+ reference,
1245
+ path,
1246
+ options,
1247
+ requestOptions,
1248
+ })
1249
+
1250
+ return {
1251
+ name: response.name,
1252
+ data: new Uint8Array(response.data),
1253
+ }
1254
+ }
1255
+
1256
+ // ============================================================================
1257
+ // Chunk Upload/Download Methods
1258
+ // ============================================================================
1259
+
1260
+ /**
1261
+ * Uploads a single chunk to the Swarm network.
1262
+ *
1263
+ * Chunks are the fundamental unit of storage in Swarm (4KB each).
1264
+ * This method is useful for low-level operations or when implementing
1265
+ * custom chunking strategies.
1266
+ *
1267
+ * @param data - The chunk data to upload (should be exactly 4KB for optimal storage)
1268
+ * @param options - Optional upload configuration
1269
+ * @param options.pin - Whether to pin the chunk locally (defaults to false)
1270
+ * @param options.encrypt - Whether to encrypt the chunk (defaults to false)
1271
+ * @param options.tag - Tag ID for tracking upload progress
1272
+ * @param options.deferred - Whether to use deferred upload (defaults to false)
1273
+ * @param options.redundancyLevel - Redundancy level from 0-4 for data availability
1274
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1275
+ * @returns A promise resolving to the upload result
1276
+ * @returns return.reference - The Swarm reference (hash) of the uploaded chunk
1277
+ * @throws {Error} If the client is not initialized
1278
+ * @throws {Error} If the user is not authenticated or cannot upload
1279
+ * @throws {Error} If the request times out
1280
+ *
1281
+ * @example
1282
+ * ```typescript
1283
+ * const chunk = new Uint8Array(4096) // 4KB chunk
1284
+ * chunk.fill(0x42) // Fill with data
1285
+ * const result = await client.uploadChunk(chunk)
1286
+ * console.log('Chunk reference:', result.reference)
1287
+ * ```
1288
+ */
1289
+ async uploadChunk(
1290
+ data: Uint8Array,
1291
+ options?: UploadOptions,
1292
+ requestOptions?: RequestOptions,
1293
+ ): Promise<UploadResult> {
1294
+ this.ensureReady()
1295
+ const requestId = this.generateRequestId()
1296
+ const { onProgress: _onProgress, ...serializableOptions } = options ?? {}
1297
+
1298
+ const response = await this.sendRequest<{
1299
+ type: "uploadChunkResponse"
1300
+ requestId: string
1301
+ reference: Reference
1302
+ }>({
1303
+ type: "uploadChunk",
1304
+ requestId,
1305
+ data: data as Uint8Array,
1306
+ options: serializableOptions,
1307
+ requestOptions,
1308
+ })
1309
+
1310
+ return {
1311
+ reference: response.reference,
1312
+ }
1313
+ }
1314
+
1315
+ /**
1316
+ * Downloads a single chunk from the Swarm network.
1317
+ *
1318
+ * Retrieves a chunk by its reference hash. This method is useful for
1319
+ * low-level operations or when implementing custom retrieval strategies.
1320
+ *
1321
+ * @param reference - The Swarm reference (hash) of the chunk to download
1322
+ * @param options - Optional download configuration
1323
+ * @param options.redundancyStrategy - Strategy for handling redundancy (0-3)
1324
+ * @param options.fallback - Whether to use fallback retrieval
1325
+ * @param options.timeoutMs - Download timeout in milliseconds
1326
+ * @param options.actPublisher - ACT publisher for encrypted content
1327
+ * @param options.actHistoryAddress - ACT history address for encrypted content
1328
+ * @param options.actTimestamp - ACT timestamp for encrypted content
1329
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1330
+ * @returns A promise resolving to the chunk data as a Uint8Array
1331
+ * @throws {Error} If the client is not initialized
1332
+ * @throws {Error} If the reference is not found
1333
+ * @throws {Error} If the request times out
1334
+ *
1335
+ * @example
1336
+ * ```typescript
1337
+ * const chunk = await client.downloadChunk('a1b2c3...')
1338
+ * console.log('Chunk size:', chunk.length)
1339
+ * ```
1340
+ */
1341
+ async downloadChunk(
1342
+ reference: Reference,
1343
+ options?: DownloadOptions,
1344
+ requestOptions?: RequestOptions,
1345
+ ): Promise<Uint8Array> {
1346
+ this.ensureReady()
1347
+ const requestId = this.generateRequestId()
1348
+
1349
+ const response = await this.sendRequest<{
1350
+ type: "downloadChunkResponse"
1351
+ requestId: string
1352
+ data: number[]
1353
+ }>({
1354
+ type: "downloadChunk",
1355
+ requestId,
1356
+ reference,
1357
+ options,
1358
+ requestOptions,
1359
+ })
1360
+
1361
+ return new Uint8Array(response.data)
1362
+ }
1363
+
1364
+ // ============================================================================
1365
+ // SOC (Single Owner Chunk) Methods
1366
+ // ============================================================================
1367
+
1368
+ /**
1369
+ * Returns an object for reading single owner chunks (SOC).
1370
+ *
1371
+ * @param ownerAddress - Ethereum address of the SOC owner
1372
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1373
+ * @returns SOCReader with `download` (encrypted) and `rawDownload` (unencrypted)
1374
+ * @throws {Error} If the client is not initialized
1375
+ * @throws {Error} If the request times out
1376
+ *
1377
+ * @example
1378
+ * ```typescript
1379
+ * const reader = client.makeSOCReader(owner)
1380
+ * const soc = await reader.download(identifier, encryptionKey)
1381
+ * console.log('Payload:', new TextDecoder().decode(soc.payload))
1382
+ * ```
1383
+ */
1384
+ makeSOCReader(
1385
+ ownerAddress: EthAddress | Uint8Array | string,
1386
+ requestOptions?: RequestOptions,
1387
+ ): SOCReader {
1388
+ this.ensureReady()
1389
+ const owner = new EthAddress(ownerAddress).toHex()
1390
+
1391
+ const sendSocDownload = async (
1392
+ identifier: Identifier | Uint8Array | string,
1393
+ encryptionKey: Uint8Array | string,
1394
+ ): Promise<SingleOwnerChunk> => {
1395
+ const requestId = this.generateRequestId()
1396
+ const response = await this.sendRequest<
1397
+ SocDownloadResponseMessage,
1398
+ SocDownloadMessage
1399
+ >({
1400
+ type: "socDownload",
1401
+ requestId,
1402
+ owner,
1403
+ identifier: this.normalizeSocIdentifier(identifier),
1404
+ encryptionKey: this.normalizeSocKey(encryptionKey),
1405
+ requestOptions,
1406
+ })
1407
+
1408
+ return this.socChunkFromResponse(response)
1409
+ }
1410
+
1411
+ const sendRawSocDownload = async (
1412
+ identifier: Identifier | Uint8Array | string,
1413
+ encryptionKey?: Uint8Array | string,
1414
+ ): Promise<SingleOwnerChunk> => {
1415
+ const requestId = this.generateRequestId()
1416
+ const response = await this.sendRequest<
1417
+ SocRawDownloadResponseMessage,
1418
+ SocRawDownloadMessage
1419
+ >({
1420
+ type: "socRawDownload",
1421
+ requestId,
1422
+ owner,
1423
+ identifier: this.normalizeSocIdentifier(identifier),
1424
+ encryptionKey: encryptionKey
1425
+ ? this.normalizeSocKey(encryptionKey)
1426
+ : undefined,
1427
+ requestOptions,
1428
+ })
1429
+
1430
+ return this.socChunkFromResponse(response)
1431
+ }
1432
+
1433
+ return {
1434
+ getOwner: async () => owner,
1435
+ rawDownload: (identifier, encryptionKey) =>
1436
+ sendRawSocDownload(identifier, encryptionKey),
1437
+ download: (identifier, encryptionKey) =>
1438
+ sendSocDownload(identifier, encryptionKey),
1439
+ }
1440
+ }
1441
+
1442
+ /**
1443
+ * Returns an object for reading and writing single owner chunks (SOC).
1444
+ *
1445
+ * Uploads are encrypted by default. Use `rawUpload` for unencrypted SOCs.
1446
+ *
1447
+ * @param signer - Optional SOC signer private key. If omitted, the proxy uses the app signer.
1448
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1449
+ * @returns SOCWriter with `upload`, `rawUpload`, `download`, and `rawDownload`
1450
+ * @throws {Error} If the client is not initialized
1451
+ * @throws {Error} If the request times out
1452
+ *
1453
+ * @example
1454
+ * ```typescript
1455
+ * const writer = client.makeSOCWriter()
1456
+ * const upload = await writer.upload(identifier, payload)
1457
+ * const soc = await writer.download(identifier, upload.encryptionKey)
1458
+ * ```
1459
+ */
1460
+ makeSOCWriter(
1461
+ signer?: PrivateKey | Uint8Array | string,
1462
+ requestOptions?: RequestOptions,
1463
+ ): SOCWriter {
1464
+ this.ensureReady()
1465
+ const signerObj = signer ? new PrivateKey(signer) : undefined
1466
+ const signerKey = signerObj ? signerObj.toHex() : undefined
1467
+ let owner: string | undefined = signerObj
1468
+ ? signerObj.publicKey().address().toHex()
1469
+ : undefined
1470
+
1471
+ const resolveOwner = async (): Promise<string> => {
1472
+ if (owner) {
1473
+ return owner
1474
+ }
1475
+
1476
+ const requestId = this.generateRequestId()
1477
+ const response = await this.sendRequest<
1478
+ SocGetOwnerResponseMessage,
1479
+ SocGetOwnerMessage
1480
+ >({
1481
+ type: "socGetOwner",
1482
+ requestId,
1483
+ })
1484
+
1485
+ owner = response.owner
1486
+ return owner
1487
+ }
1488
+
1489
+ const sendSocDownload = async (
1490
+ identifier: Identifier | Uint8Array | string,
1491
+ encryptionKey: Uint8Array | string,
1492
+ ): Promise<SingleOwnerChunk> => {
1493
+ const requestId = this.generateRequestId()
1494
+
1495
+ const response = await this.sendRequest<
1496
+ SocDownloadResponseMessage,
1497
+ SocDownloadMessage
1498
+ >({
1499
+ type: "socDownload",
1500
+ requestId,
1501
+ owner,
1502
+ identifier: this.normalizeSocIdentifier(identifier),
1503
+ encryptionKey: this.normalizeSocKey(encryptionKey),
1504
+ requestOptions,
1505
+ })
1506
+
1507
+ return this.socChunkFromResponse(response)
1508
+ }
1509
+
1510
+ const sendRawSocDownload = async (
1511
+ identifier: Identifier | Uint8Array | string,
1512
+ encryptionKey?: Uint8Array | string,
1513
+ ): Promise<SingleOwnerChunk> => {
1514
+ const requestId = this.generateRequestId()
1515
+
1516
+ const response = await this.sendRequest<
1517
+ SocRawDownloadResponseMessage,
1518
+ SocRawDownloadMessage
1519
+ >({
1520
+ type: "socRawDownload",
1521
+ requestId,
1522
+ owner,
1523
+ identifier: this.normalizeSocIdentifier(identifier),
1524
+ encryptionKey: encryptionKey
1525
+ ? this.normalizeSocKey(encryptionKey)
1526
+ : undefined,
1527
+ requestOptions,
1528
+ })
1529
+
1530
+ return this.socChunkFromResponse(response)
1531
+ }
1532
+
1533
+ const sendSocUpload = async (
1534
+ identifier: Identifier | Uint8Array | string,
1535
+ data: Uint8Array,
1536
+ options?: UploadOptions,
1537
+ ): Promise<SocUploadResult> => {
1538
+ const requestId = this.generateRequestId()
1539
+ const response = await this.sendRequest<
1540
+ SocUploadResponseMessage,
1541
+ SocUploadMessage
1542
+ >({
1543
+ type: "socUpload",
1544
+ requestId,
1545
+ identifier: this.normalizeSocIdentifier(identifier),
1546
+ data: new Uint8Array(data),
1547
+ signer: signerKey,
1548
+ options,
1549
+ requestOptions,
1550
+ })
1551
+
1552
+ owner = response.owner
1553
+
1554
+ return {
1555
+ reference: response.reference,
1556
+ tagUid: response.tagUid,
1557
+ encryptionKey: response.encryptionKey,
1558
+ owner: response.owner,
1559
+ }
1560
+ }
1561
+
1562
+ const sendRawSocUpload = async (
1563
+ identifier: Identifier | Uint8Array | string,
1564
+ data: Uint8Array,
1565
+ options?: UploadOptions,
1566
+ ): Promise<SocRawUploadResult> => {
1567
+ const requestId = this.generateRequestId()
1568
+ const response = await this.sendRequest<
1569
+ SocRawUploadResponseMessage,
1570
+ SocRawUploadMessage
1571
+ >({
1572
+ type: "socRawUpload",
1573
+ requestId,
1574
+ identifier: this.normalizeSocIdentifier(identifier),
1575
+ data: new Uint8Array(data),
1576
+ signer: signerKey,
1577
+ options,
1578
+ requestOptions,
1579
+ })
1580
+
1581
+ owner = response.owner
1582
+
1583
+ return {
1584
+ reference: response.reference,
1585
+ tagUid: response.tagUid,
1586
+ encryptionKey: response.encryptionKey,
1587
+ owner: response.owner,
1588
+ }
1589
+ }
1590
+
1591
+ return {
1592
+ getOwner: resolveOwner,
1593
+ rawDownload: (identifier, encryptionKey) =>
1594
+ sendRawSocDownload(identifier, encryptionKey),
1595
+ download: (identifier, encryptionKey) =>
1596
+ sendSocDownload(identifier, encryptionKey),
1597
+ upload: (identifier, data, options) =>
1598
+ sendSocUpload(identifier, data, options),
1599
+ rawUpload: (identifier, data, options) =>
1600
+ sendRawSocUpload(identifier, data, options),
1601
+ }
1602
+ }
1603
+
1604
+ /**
1605
+ * Returns an object for reading epoch-based feeds.
1606
+ *
1607
+ * @param options - Feed reader options
1608
+ * @param options.topic - Feed topic (32 bytes)
1609
+ * @param options.owner - Optional feed owner address
1610
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1611
+ * @returns FeedReader with `getOwner`, `downloadReference`, and `downloadPayload`
1612
+ * @throws {Error} If the client is not initialized
1613
+ * @throws {Error} If the request times out
1614
+ */
1615
+ makeEpochFeedReader(
1616
+ options: FeedReaderOptions,
1617
+ requestOptions?: RequestOptions,
1618
+ ): FeedReader {
1619
+ this.ensureReady()
1620
+ const topic = this.normalizeFeedTopic(options.topic)
1621
+ let owner: string | undefined = options.owner
1622
+ ? new EthAddress(options.owner).toHex()
1623
+ : undefined
1624
+
1625
+ const resolveOwner = async (): Promise<string> => {
1626
+ if (owner) {
1627
+ return owner
1628
+ }
1629
+
1630
+ const requestId = this.generateRequestId()
1631
+ const response = await this.sendRequest<
1632
+ FeedGetOwnerResponseMessage,
1633
+ FeedGetOwnerMessage
1634
+ >({
1635
+ type: "feedGetOwner",
1636
+ requestId,
1637
+ })
1638
+
1639
+ owner = response.owner
1640
+ return owner
1641
+ }
1642
+
1643
+ const downloadReference = async (
1644
+ options?: EpochFeedDownloadOptions,
1645
+ ): Promise<EpochFeedDownloadReferenceResult> => {
1646
+ const atValue =
1647
+ options?.at !== undefined
1648
+ ? options.at
1649
+ : BigInt(Math.floor(Date.now() / 1000))
1650
+ const requestId = this.generateRequestId()
1651
+ const response = await this.sendRequest<
1652
+ EpochFeedDownloadReferenceResponseMessage,
1653
+ EpochFeedDownloadReferenceMessage
1654
+ >({
1655
+ type: "epochFeedDownloadReference",
1656
+ requestId,
1657
+ topic,
1658
+ owner,
1659
+ at: this.normalizeFeedTimestamp(atValue),
1660
+ after:
1661
+ options?.after !== undefined
1662
+ ? this.normalizeFeedTimestamp(options.after)
1663
+ : undefined,
1664
+ encryptionKey:
1665
+ options?.encryptionKey !== undefined
1666
+ ? this.normalizeSocKey(options.encryptionKey)
1667
+ : undefined,
1668
+ requestOptions,
1669
+ })
1670
+ const reference = response.reference
1671
+ const cleanRef =
1672
+ reference && reference.startsWith("0x") ? reference.slice(2) : reference
1673
+ const encryptionKey =
1674
+ cleanRef && cleanRef.length === 128 ? cleanRef.slice(64) : undefined
1675
+ return { reference, encryptionKey }
1676
+ }
1677
+
1678
+ const downloadPayload = async (
1679
+ options?: EpochFeedDownloadOptions,
1680
+ ): Promise<EpochFeedDownloadPayloadResult> => {
1681
+ const result = await downloadReference(options)
1682
+ if (!result.reference) {
1683
+ return {
1684
+ reference: undefined,
1685
+ payload: undefined,
1686
+ encryptionKey: undefined,
1687
+ }
1688
+ }
1689
+ const payload = await this.downloadData(
1690
+ result.reference,
1691
+ undefined,
1692
+ requestOptions,
1693
+ )
1694
+ return {
1695
+ reference: result.reference,
1696
+ payload,
1697
+ encryptionKey: result.encryptionKey,
1698
+ }
1699
+ }
1700
+
1701
+ const downloadRawReference = async (
1702
+ options?: Omit<EpochFeedDownloadOptions, "encryptionKey">,
1703
+ ): Promise<EpochFeedDownloadReferenceResult> => {
1704
+ const atValue =
1705
+ options?.at !== undefined
1706
+ ? options.at
1707
+ : BigInt(Math.floor(Date.now() / 1000))
1708
+ const requestId = this.generateRequestId()
1709
+ const response = await this.sendRequest<
1710
+ EpochFeedDownloadReferenceResponseMessage,
1711
+ EpochFeedDownloadReferenceMessage
1712
+ >({
1713
+ type: "epochFeedDownloadReference",
1714
+ requestId,
1715
+ topic,
1716
+ owner,
1717
+ at: this.normalizeFeedTimestamp(atValue),
1718
+ after:
1719
+ options?.after !== undefined
1720
+ ? this.normalizeFeedTimestamp(options.after)
1721
+ : undefined,
1722
+ encryptionKey: undefined, // No encryption for raw download
1723
+ requestOptions,
1724
+ })
1725
+ const reference = response.reference
1726
+ return { reference, encryptionKey: undefined }
1727
+ }
1728
+
1729
+ const downloadRawPayload = async (
1730
+ options?: Omit<EpochFeedDownloadOptions, "encryptionKey">,
1731
+ ): Promise<EpochFeedDownloadPayloadResult> => {
1732
+ const result = await downloadRawReference(options)
1733
+ if (!result.reference) {
1734
+ return {
1735
+ reference: undefined,
1736
+ payload: undefined,
1737
+ encryptionKey: undefined,
1738
+ }
1739
+ }
1740
+ const payload = await this.downloadData(
1741
+ result.reference,
1742
+ undefined,
1743
+ requestOptions,
1744
+ )
1745
+ return {
1746
+ reference: result.reference,
1747
+ payload,
1748
+ encryptionKey: undefined,
1749
+ }
1750
+ }
1751
+
1752
+ return {
1753
+ getOwner: resolveOwner,
1754
+ downloadReference,
1755
+ downloadPayload,
1756
+ downloadRawReference,
1757
+ downloadRawPayload,
1758
+ }
1759
+ }
1760
+
1761
+ /**
1762
+ * Returns an object for reading and writing epoch-based feeds.
1763
+ *
1764
+ * @param options - Feed writer options
1765
+ * @param options.topic - Feed topic (32 bytes)
1766
+ * @param options.signer - Optional feed signer private key. If omitted, the proxy uses the app signer.
1767
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
1768
+ * @returns FeedWriter with `getOwner`, `downloadReference`, `downloadPayload`, `uploadPayload`, and `uploadReference`
1769
+ * @throws {Error} If the client is not initialized
1770
+ * @throws {Error} If the request times out
1771
+ */
1772
+ makeEpochFeedWriter(
1773
+ options: FeedWriterOptions,
1774
+ requestOptions?: RequestOptions,
1775
+ ): FeedWriter {
1776
+ this.ensureReady()
1777
+ const topic = this.normalizeFeedTopic(options.topic)
1778
+ const signerObj = options.signer
1779
+ ? new PrivateKey(options.signer)
1780
+ : undefined
1781
+ const signerKey = signerObj ? signerObj.toHex() : undefined
1782
+ let owner: string | undefined = signerObj
1783
+ ? signerObj.publicKey().address().toHex()
1784
+ : undefined
1785
+
1786
+ const resolveOwner = async (): Promise<string> => {
1787
+ if (owner) {
1788
+ return owner
1789
+ }
1790
+
1791
+ const requestId = this.generateRequestId()
1792
+ const response = await this.sendRequest<
1793
+ FeedGetOwnerResponseMessage,
1794
+ FeedGetOwnerMessage
1795
+ >({
1796
+ type: "feedGetOwner",
1797
+ requestId,
1798
+ })
1799
+
1800
+ owner = response.owner
1801
+ return owner
1802
+ }
1803
+
1804
+ const downloadReference = async (
1805
+ options?: EpochFeedDownloadOptions,
1806
+ ): Promise<EpochFeedDownloadReferenceResult> => {
1807
+ const atValue =
1808
+ options?.at !== undefined
1809
+ ? options.at
1810
+ : BigInt(Math.floor(Date.now() / 1000))
1811
+ const requestId = this.generateRequestId()
1812
+ const response = await this.sendRequest<
1813
+ EpochFeedDownloadReferenceResponseMessage,
1814
+ EpochFeedDownloadReferenceMessage
1815
+ >({
1816
+ type: "epochFeedDownloadReference",
1817
+ requestId,
1818
+ topic,
1819
+ owner,
1820
+ at: this.normalizeFeedTimestamp(atValue),
1821
+ after:
1822
+ options?.after !== undefined
1823
+ ? this.normalizeFeedTimestamp(options.after)
1824
+ : undefined,
1825
+ encryptionKey:
1826
+ options?.encryptionKey !== undefined
1827
+ ? this.normalizeSocKey(options.encryptionKey)
1828
+ : undefined,
1829
+ requestOptions,
1830
+ })
1831
+ const reference = response.reference
1832
+ const cleanRef =
1833
+ reference && reference.startsWith("0x") ? reference.slice(2) : reference
1834
+ const encryptionKey =
1835
+ cleanRef && cleanRef.length === 128 ? cleanRef.slice(64) : undefined
1836
+ return { reference, encryptionKey }
1837
+ }
1838
+
1839
+ const downloadPayload = async (
1840
+ options?: EpochFeedDownloadOptions,
1841
+ ): Promise<EpochFeedDownloadPayloadResult> => {
1842
+ const result = await downloadReference(options)
1843
+ if (!result.reference) {
1844
+ return {
1845
+ reference: undefined,
1846
+ payload: undefined,
1847
+ encryptionKey: undefined,
1848
+ }
1849
+ }
1850
+ const payload = await this.downloadData(
1851
+ result.reference,
1852
+ undefined,
1853
+ requestOptions,
1854
+ )
1855
+ return {
1856
+ reference: result.reference,
1857
+ payload,
1858
+ encryptionKey: result.encryptionKey,
1859
+ }
1860
+ }
1861
+
1862
+ const uploadReference = async (
1863
+ reference: Uint8Array | string,
1864
+ options?: EpochFeedUploadOptions,
1865
+ ): Promise<EpochFeedUploadResult> => {
1866
+ const atValue =
1867
+ options?.at !== undefined
1868
+ ? options.at
1869
+ : BigInt(Math.floor(Date.now() / 1000))
1870
+ const normalizedRef = this.normalizeReference(reference)
1871
+ const cleanRef = normalizedRef.startsWith("0x")
1872
+ ? normalizedRef.slice(2)
1873
+ : normalizedRef
1874
+ const derivedKey =
1875
+ cleanRef.length === 128 ? cleanRef.slice(64) : undefined
1876
+ const feedKey = options?.encryptionKey ?? derivedKey
1877
+ const requestId = this.generateRequestId()
1878
+ const response = await this.sendRequest<
1879
+ EpochFeedUploadReferenceResponseMessage,
1880
+ EpochFeedUploadReferenceMessage
1881
+ >({
1882
+ type: "epochFeedUploadReference",
1883
+ requestId,
1884
+ topic,
1885
+ signer: signerKey,
1886
+ at: this.normalizeFeedTimestamp(atValue),
1887
+ reference: normalizedRef,
1888
+ encryptionKey:
1889
+ feedKey !== undefined ? this.normalizeSocKey(feedKey) : undefined,
1890
+ hints: options?.hints,
1891
+ requestOptions,
1892
+ })
1893
+ const socAddress = response.socAddress
1894
+ const encryptionKey = derivedKey
1895
+ return {
1896
+ socAddress,
1897
+ reference: normalizedRef,
1898
+ encryptionKey,
1899
+ epoch: response.epoch,
1900
+ timestamp: response.timestamp,
1901
+ }
1902
+ }
1903
+
1904
+ const uploadPayload = async (
1905
+ data: Uint8Array | string,
1906
+ options?: EpochFeedUploadOptions,
1907
+ ): Promise<EpochFeedUploadResult> => {
1908
+ const atValue =
1909
+ options?.at !== undefined
1910
+ ? options.at
1911
+ : BigInt(Math.floor(Date.now() / 1000))
1912
+ const encrypt = options?.encrypt !== false
1913
+ const uploadResult = await this.uploadData(
1914
+ this.normalizePayload(data),
1915
+ { ...options?.uploadOptions, encrypt },
1916
+ requestOptions,
1917
+ )
1918
+ const cleanRef = uploadResult.reference.startsWith("0x")
1919
+ ? uploadResult.reference.slice(2)
1920
+ : uploadResult.reference
1921
+ const derivedKey =
1922
+ cleanRef.length === 128 ? cleanRef.slice(64) : undefined
1923
+ const feedKey = options?.encryptionKey ?? derivedKey
1924
+ const requestId = this.generateRequestId()
1925
+ const response = await this.sendRequest<
1926
+ EpochFeedUploadReferenceResponseMessage,
1927
+ EpochFeedUploadReferenceMessage
1928
+ >({
1929
+ type: "epochFeedUploadReference",
1930
+ requestId,
1931
+ topic,
1932
+ signer: signerKey,
1933
+ at: this.normalizeFeedTimestamp(atValue),
1934
+ reference: uploadResult.reference,
1935
+ encryptionKey:
1936
+ feedKey !== undefined ? this.normalizeSocKey(feedKey) : undefined,
1937
+ hints: options?.hints,
1938
+ requestOptions,
1939
+ })
1940
+ const socAddress = response.socAddress
1941
+ const encryptionKey = derivedKey
1942
+ return {
1943
+ socAddress,
1944
+ reference: uploadResult.reference,
1945
+ encryptionKey,
1946
+ epoch: response.epoch,
1947
+ timestamp: response.timestamp,
1948
+ }
1949
+ }
1950
+
1951
+ const downloadRawReference = async (
1952
+ options?: Omit<EpochFeedDownloadOptions, "encryptionKey">,
1953
+ ): Promise<EpochFeedDownloadReferenceResult> => {
1954
+ const atValue =
1955
+ options?.at !== undefined
1956
+ ? options.at
1957
+ : BigInt(Math.floor(Date.now() / 1000))
1958
+ const requestId = this.generateRequestId()
1959
+ const response = await this.sendRequest<
1960
+ EpochFeedDownloadReferenceResponseMessage,
1961
+ EpochFeedDownloadReferenceMessage
1962
+ >({
1963
+ type: "epochFeedDownloadReference",
1964
+ requestId,
1965
+ topic,
1966
+ owner,
1967
+ at: this.normalizeFeedTimestamp(atValue),
1968
+ after:
1969
+ options?.after !== undefined
1970
+ ? this.normalizeFeedTimestamp(options.after)
1971
+ : undefined,
1972
+ encryptionKey: undefined, // No encryption for raw download
1973
+ requestOptions,
1974
+ })
1975
+ const reference = response.reference
1976
+ return { reference, encryptionKey: undefined }
1977
+ }
1978
+
1979
+ const downloadRawPayload = async (
1980
+ options?: Omit<EpochFeedDownloadOptions, "encryptionKey">,
1981
+ ): Promise<EpochFeedDownloadPayloadResult> => {
1982
+ const result = await downloadRawReference(options)
1983
+ if (!result.reference) {
1984
+ return {
1985
+ reference: undefined,
1986
+ payload: undefined,
1987
+ encryptionKey: undefined,
1988
+ }
1989
+ }
1990
+ const payload = await this.downloadData(
1991
+ result.reference,
1992
+ undefined,
1993
+ requestOptions,
1994
+ )
1995
+ return {
1996
+ reference: result.reference,
1997
+ payload,
1998
+ encryptionKey: undefined,
1999
+ }
2000
+ }
2001
+
2002
+ const uploadRawReference = async (
2003
+ reference: Uint8Array | string,
2004
+ options?: Omit<EpochFeedUploadOptions, "encryptionKey">,
2005
+ ): Promise<EpochFeedUploadResult> => {
2006
+ const atValue =
2007
+ options?.at !== undefined
2008
+ ? options.at
2009
+ : BigInt(Math.floor(Date.now() / 1000))
2010
+ const normalizedRef = this.normalizeReference(reference)
2011
+ const requestId = this.generateRequestId()
2012
+ const response = await this.sendRequest<
2013
+ EpochFeedUploadReferenceResponseMessage,
2014
+ EpochFeedUploadReferenceMessage
2015
+ >({
2016
+ type: "epochFeedUploadReference",
2017
+ requestId,
2018
+ topic,
2019
+ signer: signerKey,
2020
+ at: this.normalizeFeedTimestamp(atValue),
2021
+ reference: normalizedRef,
2022
+ encryptionKey: undefined, // No encryption for raw upload
2023
+ hints: options?.hints,
2024
+ requestOptions,
2025
+ })
2026
+ const socAddress = response.socAddress
2027
+ return {
2028
+ socAddress,
2029
+ reference: normalizedRef,
2030
+ encryptionKey: undefined,
2031
+ epoch: response.epoch,
2032
+ timestamp: response.timestamp,
2033
+ }
2034
+ }
2035
+
2036
+ const uploadRawPayload = async (
2037
+ data: Uint8Array | string,
2038
+ options?: Omit<EpochFeedUploadOptions, "encryptionKey" | "encrypt">,
2039
+ ): Promise<EpochFeedUploadResult> => {
2040
+ const atValue =
2041
+ options?.at !== undefined
2042
+ ? options.at
2043
+ : BigInt(Math.floor(Date.now() / 1000))
2044
+ // Upload with encrypt: false for raw payload
2045
+ const uploadResult = await this.uploadData(
2046
+ this.normalizePayload(data),
2047
+ { ...options?.uploadOptions, encrypt: false },
2048
+ requestOptions,
2049
+ )
2050
+ const requestId = this.generateRequestId()
2051
+ const response = await this.sendRequest<
2052
+ EpochFeedUploadReferenceResponseMessage,
2053
+ EpochFeedUploadReferenceMessage
2054
+ >({
2055
+ type: "epochFeedUploadReference",
2056
+ requestId,
2057
+ topic,
2058
+ signer: signerKey,
2059
+ at: this.normalizeFeedTimestamp(atValue),
2060
+ reference: uploadResult.reference,
2061
+ encryptionKey: undefined, // No encryption for raw upload
2062
+ hints: options?.hints,
2063
+ requestOptions,
2064
+ })
2065
+ const socAddress = response.socAddress
2066
+ return {
2067
+ socAddress,
2068
+ reference: uploadResult.reference,
2069
+ encryptionKey: undefined,
2070
+ epoch: response.epoch,
2071
+ timestamp: response.timestamp,
2072
+ }
2073
+ }
2074
+
2075
+ return {
2076
+ getOwner: resolveOwner,
2077
+ downloadReference,
2078
+ downloadPayload,
2079
+ downloadRawReference,
2080
+ downloadRawPayload,
2081
+ uploadReference,
2082
+ uploadPayload,
2083
+ uploadRawReference,
2084
+ uploadRawPayload,
2085
+ }
2086
+ }
2087
+
2088
+ /**
2089
+ * Returns a sequential feed reader (chunk API only).
2090
+ *
2091
+ * @param options - Sequential feed reader options
2092
+ * @param options.topic - Feed topic (32 bytes)
2093
+ * @param options.owner - Optional feed owner address
2094
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2095
+ * @returns SequentialFeedReader with payload/reference download helpers
2096
+ */
2097
+ makeSequentialFeedReader(
2098
+ options: SequentialFeedReaderOptions,
2099
+ requestOptions?: RequestOptions,
2100
+ ): SequentialFeedReader {
2101
+ this.ensureReady()
2102
+ const topic = this.normalizeFeedTopic(options.topic)
2103
+ let owner: string | undefined = options.owner
2104
+ ? new EthAddress(options.owner).toHex()
2105
+ : undefined
2106
+
2107
+ const resolveOwner = async (): Promise<string> => {
2108
+ if (owner) {
2109
+ return owner
2110
+ }
2111
+ const requestId = this.generateRequestId()
2112
+ const response = await this.sendRequest<
2113
+ SequentialFeedGetOwnerResponseMessage,
2114
+ SequentialFeedGetOwnerMessage
2115
+ >({
2116
+ type: "seqFeedGetOwner",
2117
+ requestId,
2118
+ })
2119
+ owner = response.owner
2120
+ return owner
2121
+ }
2122
+
2123
+ const downloadPayload = async (
2124
+ encryptionKey: Uint8Array | string,
2125
+ options?: SequentialFeedUpdateOptions,
2126
+ ): Promise<SequentialFeedPayloadResult> => {
2127
+ const requestId = this.generateRequestId()
2128
+ const response = await this.sendRequest<
2129
+ SequentialFeedDownloadPayloadResponseMessage,
2130
+ SequentialFeedDownloadPayloadMessage
2131
+ >({
2132
+ type: "seqFeedDownloadPayload",
2133
+ requestId,
2134
+ topic,
2135
+ owner,
2136
+ index:
2137
+ options?.index !== undefined
2138
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2139
+ : undefined,
2140
+ at:
2141
+ options?.at !== undefined
2142
+ ? this.normalizeFeedTimestamp(
2143
+ options.at as bigint | number | string,
2144
+ )
2145
+ : undefined,
2146
+ hasTimestamp: options?.hasTimestamp,
2147
+ lookupTimeoutMs: options?.lookupTimeoutMs,
2148
+ encryptionKey: this.normalizeSocKey(encryptionKey),
2149
+ requestOptions,
2150
+ })
2151
+
2152
+ return {
2153
+ payload: response.payload,
2154
+ timestamp: response.timestamp,
2155
+ feedIndex: response.feedIndex,
2156
+ feedIndexNext: response.feedIndexNext,
2157
+ }
2158
+ }
2159
+
2160
+ const downloadRawPayload = async (
2161
+ options?: SequentialFeedDownloadRawOptions,
2162
+ ): Promise<SequentialFeedPayloadResult> => {
2163
+ const requestId = this.generateRequestId()
2164
+ const response = await this.sendRequest<
2165
+ SequentialFeedDownloadRawPayloadResponseMessage,
2166
+ SequentialFeedDownloadRawPayloadMessage
2167
+ >({
2168
+ type: "seqFeedDownloadRawPayload",
2169
+ requestId,
2170
+ topic,
2171
+ owner,
2172
+ index:
2173
+ options?.index !== undefined
2174
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2175
+ : undefined,
2176
+ at:
2177
+ options?.at !== undefined
2178
+ ? this.normalizeFeedTimestamp(
2179
+ options.at as bigint | number | string,
2180
+ )
2181
+ : undefined,
2182
+ hasTimestamp: options?.hasTimestamp,
2183
+ lookupTimeoutMs: options?.lookupTimeoutMs,
2184
+ encryptionKey: options?.encryptionKey
2185
+ ? this.normalizeSocKey(options.encryptionKey)
2186
+ : undefined,
2187
+ requestOptions,
2188
+ })
2189
+
2190
+ return {
2191
+ payload: response.payload,
2192
+ timestamp: response.timestamp,
2193
+ feedIndex: response.feedIndex,
2194
+ feedIndexNext: response.feedIndexNext,
2195
+ }
2196
+ }
2197
+
2198
+ const downloadReference = async (
2199
+ encryptionKey: Uint8Array | string,
2200
+ options?: SequentialFeedUpdateOptions,
2201
+ ): Promise<SequentialFeedReferenceResult> => {
2202
+ const requestId = this.generateRequestId()
2203
+ const response = await this.sendRequest<
2204
+ SequentialFeedDownloadReferenceResponseMessage,
2205
+ SequentialFeedDownloadReferenceMessage
2206
+ >({
2207
+ type: "seqFeedDownloadReference",
2208
+ requestId,
2209
+ topic,
2210
+ owner,
2211
+ index:
2212
+ options?.index !== undefined
2213
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2214
+ : undefined,
2215
+ at:
2216
+ options?.at !== undefined
2217
+ ? this.normalizeFeedTimestamp(
2218
+ options.at as bigint | number | string,
2219
+ )
2220
+ : undefined,
2221
+ hasTimestamp: options?.hasTimestamp,
2222
+ lookupTimeoutMs: options?.lookupTimeoutMs,
2223
+ encryptionKey: this.normalizeSocKey(encryptionKey),
2224
+ requestOptions,
2225
+ })
2226
+
2227
+ return {
2228
+ reference: response.reference,
2229
+ feedIndex: response.feedIndex,
2230
+ feedIndexNext: response.feedIndexNext,
2231
+ }
2232
+ }
2233
+
2234
+ return {
2235
+ getOwner: resolveOwner,
2236
+ downloadPayload,
2237
+ downloadRawPayload,
2238
+ downloadReference,
2239
+ }
2240
+ }
2241
+
2242
+ /**
2243
+ * Returns a sequential feed writer (chunk API only).
2244
+ *
2245
+ * @param options - Sequential feed writer options
2246
+ * @param options.topic - Feed topic (32 bytes)
2247
+ * @param options.signer - Optional signer private key. If omitted, proxy uses app signer.
2248
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2249
+ * @returns SequentialFeedWriter with payload/reference upload helpers
2250
+ */
2251
+ makeSequentialFeedWriter(
2252
+ options: SequentialFeedWriterOptions,
2253
+ requestOptions?: RequestOptions,
2254
+ ): SequentialFeedWriter {
2255
+ this.ensureReady()
2256
+ const topic = this.normalizeFeedTopic(options.topic)
2257
+ const signerObj = options.signer
2258
+ ? new PrivateKey(options.signer)
2259
+ : undefined
2260
+ const signerKey = signerObj ? signerObj.toHex() : undefined
2261
+ let owner: string | undefined = signerObj
2262
+ ? signerObj.publicKey().address().toHex()
2263
+ : undefined
2264
+
2265
+ const resolveOwner = async (): Promise<string> => {
2266
+ if (owner) {
2267
+ return owner
2268
+ }
2269
+ const requestId = this.generateRequestId()
2270
+ const response = await this.sendRequest<
2271
+ SequentialFeedGetOwnerResponseMessage,
2272
+ SequentialFeedGetOwnerMessage
2273
+ >({
2274
+ type: "seqFeedGetOwner",
2275
+ requestId,
2276
+ })
2277
+ owner = response.owner
2278
+ return owner
2279
+ }
2280
+
2281
+ const downloadPayload = async (
2282
+ encryptionKey: Uint8Array | string,
2283
+ options?: SequentialFeedUpdateOptions,
2284
+ ): Promise<SequentialFeedPayloadResult> => {
2285
+ const requestId = this.generateRequestId()
2286
+ const response = await this.sendRequest<
2287
+ SequentialFeedDownloadPayloadResponseMessage,
2288
+ SequentialFeedDownloadPayloadMessage
2289
+ >({
2290
+ type: "seqFeedDownloadPayload",
2291
+ requestId,
2292
+ topic,
2293
+ owner,
2294
+ index:
2295
+ options?.index !== undefined
2296
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2297
+ : undefined,
2298
+ at:
2299
+ options?.at !== undefined
2300
+ ? this.normalizeFeedTimestamp(
2301
+ options.at as bigint | number | string,
2302
+ )
2303
+ : undefined,
2304
+ hasTimestamp: options?.hasTimestamp,
2305
+ encryptionKey: this.normalizeSocKey(encryptionKey),
2306
+ requestOptions,
2307
+ })
2308
+
2309
+ return {
2310
+ payload: response.payload,
2311
+ timestamp: response.timestamp,
2312
+ feedIndex: response.feedIndex,
2313
+ feedIndexNext: response.feedIndexNext,
2314
+ }
2315
+ }
2316
+
2317
+ const downloadRawPayload = async (
2318
+ options?: SequentialFeedDownloadRawOptions,
2319
+ ): Promise<SequentialFeedPayloadResult> => {
2320
+ const requestId = this.generateRequestId()
2321
+ const response = await this.sendRequest<
2322
+ SequentialFeedDownloadRawPayloadResponseMessage,
2323
+ SequentialFeedDownloadRawPayloadMessage
2324
+ >({
2325
+ type: "seqFeedDownloadRawPayload",
2326
+ requestId,
2327
+ topic,
2328
+ owner,
2329
+ index:
2330
+ options?.index !== undefined
2331
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2332
+ : undefined,
2333
+ at:
2334
+ options?.at !== undefined
2335
+ ? this.normalizeFeedTimestamp(
2336
+ options.at as bigint | number | string,
2337
+ )
2338
+ : undefined,
2339
+ hasTimestamp: options?.hasTimestamp,
2340
+ encryptionKey: options?.encryptionKey
2341
+ ? this.normalizeSocKey(options.encryptionKey)
2342
+ : undefined,
2343
+ requestOptions,
2344
+ })
2345
+
2346
+ return {
2347
+ payload: response.payload,
2348
+ timestamp: response.timestamp,
2349
+ feedIndex: response.feedIndex,
2350
+ feedIndexNext: response.feedIndexNext,
2351
+ }
2352
+ }
2353
+
2354
+ const downloadReference = async (
2355
+ encryptionKey: Uint8Array | string,
2356
+ options?: SequentialFeedUpdateOptions,
2357
+ ): Promise<SequentialFeedReferenceResult> => {
2358
+ const requestId = this.generateRequestId()
2359
+ const response = await this.sendRequest<
2360
+ SequentialFeedDownloadReferenceResponseMessage,
2361
+ SequentialFeedDownloadReferenceMessage
2362
+ >({
2363
+ type: "seqFeedDownloadReference",
2364
+ requestId,
2365
+ topic,
2366
+ owner,
2367
+ index:
2368
+ options?.index !== undefined
2369
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2370
+ : undefined,
2371
+ at:
2372
+ options?.at !== undefined
2373
+ ? this.normalizeFeedTimestamp(
2374
+ options.at as bigint | number | string,
2375
+ )
2376
+ : undefined,
2377
+ hasTimestamp: options?.hasTimestamp,
2378
+ encryptionKey: this.normalizeSocKey(encryptionKey),
2379
+ requestOptions,
2380
+ })
2381
+
2382
+ return {
2383
+ reference: response.reference,
2384
+ feedIndex: response.feedIndex,
2385
+ feedIndexNext: response.feedIndexNext,
2386
+ }
2387
+ }
2388
+
2389
+ const uploadPayload = async (
2390
+ data: Uint8Array | string,
2391
+ options?: SequentialFeedUploadOptions,
2392
+ ): Promise<SequentialFeedUploadResult> => {
2393
+ const requestId = this.generateRequestId()
2394
+ const response = await this.sendRequest<
2395
+ SequentialFeedUploadPayloadResponseMessage,
2396
+ SequentialFeedUploadPayloadMessage
2397
+ >({
2398
+ type: "seqFeedUploadPayload",
2399
+ requestId,
2400
+ topic,
2401
+ signer: signerKey,
2402
+ data: this.normalizePayload(data),
2403
+ index:
2404
+ options?.index !== undefined
2405
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2406
+ : undefined,
2407
+ at:
2408
+ options?.at !== undefined
2409
+ ? this.normalizeFeedTimestamp(
2410
+ options.at as bigint | number | string,
2411
+ )
2412
+ : undefined,
2413
+ hasTimestamp: options?.hasTimestamp,
2414
+ lookupTimeoutMs: options?.lookupTimeoutMs,
2415
+ options,
2416
+ requestOptions,
2417
+ })
2418
+
2419
+ owner = response.owner
2420
+
2421
+ return {
2422
+ reference: response.reference,
2423
+ feedIndex: response.feedIndex,
2424
+ owner: response.owner,
2425
+ encryptionKey: response.encryptionKey,
2426
+ tagUid: response.tagUid,
2427
+ }
2428
+ }
2429
+
2430
+ const uploadRawPayload = async (
2431
+ data: Uint8Array | string,
2432
+ options?: SequentialFeedUploadRawOptions,
2433
+ ): Promise<SequentialFeedUploadResult> => {
2434
+ const requestId = this.generateRequestId()
2435
+ const response = await this.sendRequest<
2436
+ SequentialFeedUploadRawPayloadResponseMessage,
2437
+ SequentialFeedUploadRawPayloadMessage
2438
+ >({
2439
+ type: "seqFeedUploadRawPayload",
2440
+ requestId,
2441
+ topic,
2442
+ signer: signerKey,
2443
+ data: this.normalizePayload(data),
2444
+ index:
2445
+ options?.index !== undefined
2446
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2447
+ : undefined,
2448
+ at:
2449
+ options?.at !== undefined
2450
+ ? this.normalizeFeedTimestamp(
2451
+ options.at as bigint | number | string,
2452
+ )
2453
+ : undefined,
2454
+ hasTimestamp: options?.hasTimestamp,
2455
+ lookupTimeoutMs: options?.lookupTimeoutMs,
2456
+ encryptionKey: options?.encryptionKey
2457
+ ? this.normalizeSocKey(options.encryptionKey)
2458
+ : undefined,
2459
+ options,
2460
+ requestOptions,
2461
+ })
2462
+
2463
+ owner = response.owner
2464
+
2465
+ return {
2466
+ reference: response.reference,
2467
+ feedIndex: response.feedIndex,
2468
+ owner: response.owner,
2469
+ encryptionKey: response.encryptionKey,
2470
+ tagUid: response.tagUid,
2471
+ }
2472
+ }
2473
+
2474
+ const uploadReference = async (
2475
+ reference: Uint8Array | string,
2476
+ options?: SequentialFeedUploadOptions,
2477
+ ): Promise<SequentialFeedUploadResult> => {
2478
+ const requestId = this.generateRequestId()
2479
+ const response = await this.sendRequest<
2480
+ SequentialFeedUploadReferenceResponseMessage,
2481
+ SequentialFeedUploadReferenceMessage
2482
+ >({
2483
+ type: "seqFeedUploadReference",
2484
+ requestId,
2485
+ topic,
2486
+ signer: signerKey,
2487
+ reference: this.normalizeReference(reference),
2488
+ index:
2489
+ options?.index !== undefined
2490
+ ? this.normalizeFeedIndex(options.index as bigint | number | string)
2491
+ : undefined,
2492
+ at:
2493
+ options?.at !== undefined
2494
+ ? this.normalizeFeedTimestamp(
2495
+ options.at as bigint | number | string,
2496
+ )
2497
+ : undefined,
2498
+ hasTimestamp: options?.hasTimestamp,
2499
+ lookupTimeoutMs: options?.lookupTimeoutMs,
2500
+ options,
2501
+ requestOptions,
2502
+ })
2503
+
2504
+ owner = response.owner
2505
+
2506
+ return {
2507
+ reference: response.reference,
2508
+ feedIndex: response.feedIndex,
2509
+ owner: response.owner,
2510
+ encryptionKey: response.encryptionKey,
2511
+ tagUid: response.tagUid,
2512
+ }
2513
+ }
2514
+
2515
+ return {
2516
+ getOwner: resolveOwner,
2517
+ downloadPayload,
2518
+ downloadRawPayload,
2519
+ downloadReference,
2520
+ uploadPayload,
2521
+ uploadRawPayload,
2522
+ uploadReference,
2523
+ }
2524
+ }
2525
+
2526
+ // ============================================================================
2527
+ // Feed Manifest Methods
2528
+ // ============================================================================
2529
+
2530
+ /**
2531
+ * Creates a feed manifest for accessing feed content via URL.
2532
+ *
2533
+ * A feed manifest enables accessing the latest feed content via a URL path
2534
+ * (e.g., `/bzz/{manifest-reference}/`). The manifest stores metadata about
2535
+ * the feed including owner, topic, and type.
2536
+ *
2537
+ * @param topic - Feed topic (32-byte hex string)
2538
+ * @param options - Optional configuration
2539
+ * @param options.owner - Feed owner address; if omitted, uses app signer
2540
+ * @param options.uploadOptions - Upload configuration (pin, deferred, etc.)
2541
+ * @param requestOptions - Request configuration (timeout, headers)
2542
+ * @returns Promise resolving to the manifest reference
2543
+ * @throws {Error} If the client is not initialized
2544
+ * @throws {Error} If no owner is provided and no app signer is available
2545
+ * @throws {Error} If the request times out
2546
+ *
2547
+ * @example
2548
+ * ```typescript
2549
+ * // Create manifest for a feed (uses app signer as owner)
2550
+ * const manifestRef = await client.createFeedManifest(topic)
2551
+ * console.log('Feed accessible at /bzz/' + manifestRef)
2552
+ *
2553
+ * // Create manifest with explicit owner
2554
+ * const manifestRef = await client.createFeedManifest(topic, {
2555
+ * owner: '0x1234...',
2556
+ * uploadOptions: { pin: true }
2557
+ * })
2558
+ * ```
2559
+ */
2560
+ async createFeedManifest(
2561
+ topic: string,
2562
+ options?: {
2563
+ owner?: string
2564
+ /** Feed type: "Sequence" for sequential feeds, "Epoch" for epoch feeds. Default: "Sequence" */
2565
+ feedType?: "Sequence" | "Epoch"
2566
+ uploadOptions?: UploadOptions
2567
+ },
2568
+ requestOptions?: RequestOptions,
2569
+ ): Promise<string> {
2570
+ this.ensureReady()
2571
+ const normalizedTopic = this.normalizeFeedTopic(topic)
2572
+ const requestId = this.generateRequestId()
2573
+
2574
+ const response = await this.sendRequest<
2575
+ CreateFeedManifestResponseMessage,
2576
+ CreateFeedManifestMessage
2577
+ >({
2578
+ type: "createFeedManifest",
2579
+ requestId,
2580
+ topic: normalizedTopic,
2581
+ owner: options?.owner,
2582
+ feedType: options?.feedType,
2583
+ uploadOptions: options?.uploadOptions,
2584
+ requestOptions,
2585
+ })
2586
+
2587
+ return response.reference
2588
+ }
2589
+
2590
+ // ============================================================================
2591
+ // GSOC Methods
2592
+ // ============================================================================
2593
+
2594
+ /**
2595
+ * Mines a private key whose SOC address is proximate to a target overlay.
2596
+ *
2597
+ * This is a synchronous, pure computation that does not require authentication.
2598
+ * The mined signer can be used with {@link gsocSend} to send GSOC messages
2599
+ * that route to the target overlay node.
2600
+ *
2601
+ * @param targetOverlay - The target overlay address to mine proximity for
2602
+ * @param identifier - The GSOC identifier
2603
+ * @param proximity - Optional proximity depth (defaults to 12 in bee-js)
2604
+ * @returns A promise resolving to the mined signer as a hex string (private key)
2605
+ * @throws {Error} If the client is not initialized
2606
+ * @throws {Error} If no valid signer can be mined
2607
+ * @throws {Error} If the request times out
2608
+ *
2609
+ * @example
2610
+ * ```typescript
2611
+ * const signer = await client.gsocMine(targetOverlay, identifier)
2612
+ * // Use signer with gsocSend
2613
+ * await client.gsocSend(signer, identifier, data)
2614
+ * ```
2615
+ */
2616
+ async gsocMine(
2617
+ targetOverlay: string,
2618
+ identifier: string,
2619
+ proximity?: number,
2620
+ ): Promise<string> {
2621
+ this.ensureReady()
2622
+ const requestId = this.generateRequestId()
2623
+
2624
+ const response = await this.sendRequest<{
2625
+ type: "gsocMineResponse"
2626
+ requestId: string
2627
+ signer: string
2628
+ }>({
2629
+ type: "gsocMine",
2630
+ requestId,
2631
+ targetOverlay,
2632
+ identifier,
2633
+ proximity,
2634
+ })
2635
+
2636
+ return response.signer
2637
+ }
2638
+
2639
+ /**
2640
+ * Sends a GSOC (Global Single Owner Chunk) message using a mined signer.
2641
+ *
2642
+ * The signer should be obtained from {@link gsocMine}. The message is sent
2643
+ * using the proxy's stored postage batch ID.
2644
+ *
2645
+ * @param signer - The mined signer as a hex string (from gsocMine)
2646
+ * @param identifier - The GSOC identifier
2647
+ * @param data - The message data to send
2648
+ * @param options - Optional upload configuration
2649
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2650
+ * @returns A promise resolving to the upload result with reference and optional tagUid
2651
+ * @throws {Error} If the client is not initialized
2652
+ * @throws {Error} If the user is not authenticated
2653
+ * @throws {Error} If no postage batch ID is available
2654
+ * @throws {Error} If the request times out
2655
+ *
2656
+ * @example
2657
+ * ```typescript
2658
+ * const signer = await client.gsocMine(targetOverlay, identifier)
2659
+ * const result = await client.gsocSend(signer, identifier, new TextEncoder().encode('Hello!'))
2660
+ * console.log('GSOC reference:', result.reference)
2661
+ * ```
2662
+ */
2663
+ async gsocSend(
2664
+ signer: string,
2665
+ identifier: string,
2666
+ data: Uint8Array,
2667
+ options?: UploadOptions,
2668
+ requestOptions?: RequestOptions,
2669
+ ): Promise<UploadResult> {
2670
+ this.ensureReady()
2671
+ const requestId = this.generateRequestId()
2672
+
2673
+ const response = await this.sendRequest<{
2674
+ type: "gsocSendResponse"
2675
+ requestId: string
2676
+ reference: Reference
2677
+ tagUid?: number
2678
+ }>({
2679
+ type: "gsocSend",
2680
+ requestId,
2681
+ signer,
2682
+ identifier,
2683
+ data: new Uint8Array(data),
2684
+ options,
2685
+ requestOptions,
2686
+ })
2687
+
2688
+ return {
2689
+ reference: response.reference,
2690
+ tagUid: response.tagUid,
2691
+ }
2692
+ }
2693
+
2694
+ // ============================================================================
2695
+ // ACT (Access Control Tries) Methods
2696
+ // ============================================================================
2697
+
2698
+ /**
2699
+ * Uploads data with ACT (Access Control Tries) protection.
2700
+ *
2701
+ * This method encrypts the data and creates an ACT that controls who can decrypt it.
2702
+ * Only the specified grantees (and the publisher) can decrypt and access the data.
2703
+ *
2704
+ * @param data - The binary data to upload as a Uint8Array
2705
+ * @param grantees - Array of grantee public keys as compressed hex strings (33 bytes = 66 hex chars)
2706
+ * @param options - Optional upload configuration
2707
+ * @param options.pin - Whether to pin the data locally (defaults to false)
2708
+ * @param options.tag - Tag ID for tracking upload progress
2709
+ * @param options.deferred - Whether to use deferred upload (defaults to false)
2710
+ * @param options.redundancyLevel - Redundancy level from 0-4 for data availability
2711
+ * @param options.onProgress - Optional callback for tracking upload progress
2712
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2713
+ * @returns A promise resolving to the ACT upload result
2714
+ * @returns return.encryptedReference - The encrypted reference that must be stored with the ACT
2715
+ * @returns return.actReference - The Swarm reference (hash) of the ACT manifest
2716
+ * @returns return.historyReference - The Swarm reference of the history manifest (use for future operations)
2717
+ * @returns return.granteeListReference - The Swarm reference of the encrypted grantee list
2718
+ * @returns return.publisherPubKey - The publisher's compressed public key (share with grantees)
2719
+ * @returns return.tagUid - The tag UID if a tag was created
2720
+ * @throws {Error} If the client is not initialized
2721
+ * @throws {Error} If the user is not authenticated or cannot upload
2722
+ * @throws {Error} If the request times out
2723
+ *
2724
+ * @example
2725
+ * ```typescript
2726
+ * const data = new TextEncoder().encode('Secret message')
2727
+ * const grantees = ['03a1b2c3...'] // Compressed public keys of allowed readers
2728
+ * const result = await client.actUploadData(data, grantees, {
2729
+ * onProgress: (progress) => {
2730
+ * console.log(`Progress: ${progress.processed}/${progress.total}`)
2731
+ * },
2732
+ * })
2733
+ * console.log('History Reference:', result.historyReference)
2734
+ * console.log('Encrypted Reference:', result.encryptedReference)
2735
+ * console.log('Publisher Public Key:', result.publisherPubKey)
2736
+ * ```
2737
+ */
2738
+ async actUploadData(
2739
+ data: Uint8Array,
2740
+ grantees: string[],
2741
+ options?: ActUploadOptions,
2742
+ requestOptions?: RequestOptions,
2743
+ ): Promise<{
2744
+ encryptedReference: string
2745
+ historyReference: string
2746
+ granteeListReference: string
2747
+ publisherPubKey: string
2748
+ actReference: string
2749
+ tagUid?: number
2750
+ }> {
2751
+ this.ensureReady()
2752
+ const requestId = this.generateRequestId()
2753
+ const { onProgress, ...serializableOptions } = options ?? {}
2754
+
2755
+ // Setup progress listener if callback provided
2756
+ let progressListener: ((event: MessageEvent) => void) | undefined
2757
+ if (onProgress) {
2758
+ progressListener = (event: MessageEvent) => {
2759
+ if (event.origin !== new URL(this.iframeOrigin).origin) return
2760
+
2761
+ try {
2762
+ const message = IframeToParentMessageSchema.parse(event.data)
2763
+ if (
2764
+ message.type === "uploadProgress" &&
2765
+ message.requestId === requestId
2766
+ ) {
2767
+ onProgress({
2768
+ total: message.total,
2769
+ processed: message.processed,
2770
+ })
2771
+ }
2772
+ } catch {
2773
+ // Ignore invalid messages
2774
+ }
2775
+ }
2776
+ window.addEventListener("message", progressListener)
2777
+ }
2778
+
2779
+ try {
2780
+ const response = await this.sendRequest<{
2781
+ type: "actUploadDataResponse"
2782
+ requestId: string
2783
+ encryptedReference: string
2784
+ historyReference: string
2785
+ granteeListReference: string
2786
+ publisherPubKey: string
2787
+ actReference: string
2788
+ tagUid?: number
2789
+ }>({
2790
+ type: "actUploadData",
2791
+ requestId,
2792
+ data: new Uint8Array(data),
2793
+ grantees,
2794
+ options: serializableOptions,
2795
+ requestOptions,
2796
+ enableProgress: !!onProgress,
2797
+ })
2798
+
2799
+ return {
2800
+ encryptedReference: response.encryptedReference,
2801
+ historyReference: response.historyReference,
2802
+ granteeListReference: response.granteeListReference,
2803
+ publisherPubKey: response.publisherPubKey,
2804
+ actReference: response.actReference,
2805
+ tagUid: response.tagUid,
2806
+ }
2807
+ } finally {
2808
+ // Clean up progress listener
2809
+ if (progressListener) {
2810
+ window.removeEventListener("message", progressListener)
2811
+ }
2812
+ }
2813
+ }
2814
+
2815
+ /**
2816
+ * Downloads ACT-protected data from the Swarm network.
2817
+ *
2818
+ * This method decrypts the ACT to recover the content reference,
2819
+ * then downloads and returns the decrypted data. Only authorized
2820
+ * grantees (including the publisher) can successfully decrypt.
2821
+ *
2822
+ * @param encryptedReference - The encrypted reference from actUploadData
2823
+ * @param historyReference - The history reference from actUploadData
2824
+ * @param publisherPubKey - The publisher's compressed public key from actUploadData
2825
+ * @param timestamp - Optional timestamp to look up a specific ACT version
2826
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2827
+ * @returns A promise resolving to the decrypted data as a Uint8Array
2828
+ * @throws {Error} If the client is not initialized
2829
+ * @throws {Error} If the user is not authorized to decrypt the ACT
2830
+ * @throws {Error} If the references are not found
2831
+ * @throws {Error} If the request times out
2832
+ *
2833
+ * @example
2834
+ * ```typescript
2835
+ * // Using the references from actUploadData
2836
+ * const data = await client.actDownloadData(
2837
+ * encryptedReference,
2838
+ * historyReference,
2839
+ * publisherPubKey
2840
+ * )
2841
+ * const text = new TextDecoder().decode(data)
2842
+ * console.log('Decrypted:', text)
2843
+ * ```
2844
+ */
2845
+ async actDownloadData(
2846
+ encryptedReference: string,
2847
+ historyReference: string,
2848
+ publisherPubKey: string,
2849
+ timestamp?: number,
2850
+ requestOptions?: RequestOptions,
2851
+ ): Promise<Uint8Array> {
2852
+ this.ensureReady()
2853
+ const requestId = this.generateRequestId()
2854
+
2855
+ const response = await this.sendRequest<{
2856
+ type: "actDownloadDataResponse"
2857
+ requestId: string
2858
+ data: Uint8Array
2859
+ }>({
2860
+ type: "actDownloadData",
2861
+ requestId,
2862
+ encryptedReference,
2863
+ historyReference,
2864
+ publisherPubKey,
2865
+ timestamp,
2866
+ requestOptions,
2867
+ })
2868
+
2869
+ return response.data
2870
+ }
2871
+
2872
+ /**
2873
+ * Adds new grantees to an existing ACT.
2874
+ *
2875
+ * This method adds new public keys to the ACT's access list.
2876
+ * Only the publisher (original uploader) can add grantees.
2877
+ * Returns new references since Swarm content is immutable.
2878
+ *
2879
+ * @param historyReference - The current history reference
2880
+ * @param grantees - Array of new grantee public keys as compressed hex strings
2881
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2882
+ * @returns A promise resolving to the new references
2883
+ * @returns return.historyReference - The new history reference after adding grantees
2884
+ * @returns return.granteeListReference - The new grantee list reference
2885
+ * @returns return.actReference - The new ACT reference
2886
+ * @throws {Error} If the client is not initialized
2887
+ * @throws {Error} If the user is not the publisher
2888
+ * @throws {Error} If the request times out
2889
+ *
2890
+ * @example
2891
+ * ```typescript
2892
+ * const newGrantees = ['03d4e5f6...'] // New public keys to grant access
2893
+ * const result = await client.actAddGrantees(historyReference, newGrantees)
2894
+ * console.log('New History Reference:', result.historyReference)
2895
+ * // The encrypted reference remains the same
2896
+ * ```
2897
+ */
2898
+ async actAddGrantees(
2899
+ historyReference: string,
2900
+ grantees: string[],
2901
+ requestOptions?: RequestOptions,
2902
+ ): Promise<{
2903
+ historyReference: string
2904
+ granteeListReference: string
2905
+ actReference: string
2906
+ }> {
2907
+ this.ensureReady()
2908
+ const requestId = this.generateRequestId()
2909
+
2910
+ const response = await this.sendRequest<{
2911
+ type: "actAddGranteesResponse"
2912
+ requestId: string
2913
+ historyReference: string
2914
+ granteeListReference: string
2915
+ actReference: string
2916
+ }>({
2917
+ type: "actAddGrantees",
2918
+ requestId,
2919
+ historyReference,
2920
+ grantees,
2921
+ requestOptions,
2922
+ })
2923
+
2924
+ return {
2925
+ historyReference: response.historyReference,
2926
+ granteeListReference: response.granteeListReference,
2927
+ actReference: response.actReference,
2928
+ }
2929
+ }
2930
+
2931
+ /**
2932
+ * Revokes grantees from an existing ACT.
2933
+ *
2934
+ * This method removes public keys from the ACT's access list and performs
2935
+ * key rotation to ensure revoked grantees cannot decrypt new versions.
2936
+ * Returns new references including a new encrypted reference.
2937
+ *
2938
+ * IMPORTANT: The original encrypted reference can still be decrypted by
2939
+ * revoked grantees if they have cached it. Key rotation only protects
2940
+ * access through the new references.
2941
+ *
2942
+ * @param historyReference - The current history reference
2943
+ * @param encryptedReference - The current encrypted reference (needed for key rotation)
2944
+ * @param revokeGrantees - Array of grantee public keys to revoke as compressed hex strings
2945
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
2946
+ * @returns A promise resolving to the new references after revocation
2947
+ * @returns return.encryptedReference - The new encrypted reference (key rotated)
2948
+ * @returns return.historyReference - The new history reference after revocation
2949
+ * @returns return.granteeListReference - The new grantee list reference
2950
+ * @returns return.actReference - The new ACT reference after revocation
2951
+ * @throws {Error} If the client is not initialized
2952
+ * @throws {Error} If the user is not the publisher
2953
+ * @throws {Error} If the request times out
2954
+ *
2955
+ * @example
2956
+ * ```typescript
2957
+ * const revokeKeys = ['03a1b2c3...'] // Public keys to revoke
2958
+ * const result = await client.actRevokeGrantees(historyReference, encryptedReference, revokeKeys)
2959
+ * console.log('New History Reference:', result.historyReference)
2960
+ * console.log('New Encrypted Reference:', result.encryptedReference)
2961
+ * // All references are new due to key rotation
2962
+ * ```
2963
+ */
2964
+ async actRevokeGrantees(
2965
+ historyReference: string,
2966
+ encryptedReference: string,
2967
+ revokeGrantees: string[],
2968
+ requestOptions?: RequestOptions,
2969
+ ): Promise<{
2970
+ encryptedReference: string
2971
+ historyReference: string
2972
+ granteeListReference: string
2973
+ actReference: string
2974
+ }> {
2975
+ this.ensureReady()
2976
+ const requestId = this.generateRequestId()
2977
+
2978
+ const response = await this.sendRequest<{
2979
+ type: "actRevokeGranteesResponse"
2980
+ requestId: string
2981
+ encryptedReference: string
2982
+ historyReference: string
2983
+ granteeListReference: string
2984
+ actReference: string
2985
+ }>({
2986
+ type: "actRevokeGrantees",
2987
+ requestId,
2988
+ historyReference,
2989
+ encryptedReference,
2990
+ revokeGrantees,
2991
+ requestOptions,
2992
+ })
2993
+
2994
+ return {
2995
+ encryptedReference: response.encryptedReference,
2996
+ historyReference: response.historyReference,
2997
+ granteeListReference: response.granteeListReference,
2998
+ actReference: response.actReference,
2999
+ }
3000
+ }
3001
+
3002
+ /**
3003
+ * Retrieves the list of grantees from an ACT.
3004
+ *
3005
+ * Only the publisher (original uploader) can view the grantee list,
3006
+ * as it is encrypted with the publisher's key.
3007
+ *
3008
+ * @param historyReference - The history reference
3009
+ * @param requestOptions - Optional request configuration (timeout, headers, endlesslyRetry)
3010
+ * @returns A promise resolving to an array of grantee public keys as compressed hex strings
3011
+ * @throws {Error} If the client is not initialized
3012
+ * @throws {Error} If the user is not the publisher
3013
+ * @throws {Error} If the request times out
3014
+ *
3015
+ * @example
3016
+ * ```typescript
3017
+ * const grantees = await client.actGetGrantees(historyReference)
3018
+ * console.log('Current grantees:', grantees.length)
3019
+ * grantees.forEach(pubKey => console.log(' -', pubKey))
3020
+ * ```
3021
+ */
3022
+ async actGetGrantees(
3023
+ historyReference: string,
3024
+ requestOptions?: RequestOptions,
3025
+ ): Promise<string[]> {
3026
+ this.ensureReady()
3027
+ const requestId = this.generateRequestId()
3028
+
3029
+ const response = await this.sendRequest<{
3030
+ type: "actGetGranteesResponse"
3031
+ requestId: string
3032
+ grantees: string[]
3033
+ }>({
3034
+ type: "actGetGrantees",
3035
+ requestId,
3036
+ historyReference,
3037
+ requestOptions,
3038
+ })
3039
+
3040
+ return response.grantees
3041
+ }
3042
+
3043
+ // ============================================================================
3044
+ // Cleanup
3045
+ // ============================================================================
3046
+
3047
+ /**
3048
+ * Destroys the client and releases all resources.
3049
+ *
3050
+ * This method should be called when the client is no longer needed.
3051
+ * It performs the following cleanup:
3052
+ * - Cancels all pending requests with an error
3053
+ * - Removes the message event listener
3054
+ * - Removes the iframe from the DOM
3055
+ * - Resets the client to an uninitialized state
3056
+ *
3057
+ * After calling destroy(), the client instance cannot be reused.
3058
+ * Create a new instance if you need to reconnect.
3059
+ *
3060
+ * @example
3061
+ * ```typescript
3062
+ * // Clean up when component unmounts
3063
+ * useEffect(() => {
3064
+ * const client = new SwarmIdClient({ ... })
3065
+ * client.initialize()
3066
+ *
3067
+ * return () => {
3068
+ * client.destroy()
3069
+ * }
3070
+ * }, [])
3071
+ * ```
3072
+ */
3073
+ destroy(): void {
3074
+ // Clear pending requests
3075
+ this.pendingRequests.forEach((pending) => {
3076
+ clearTimeout(pending.timeoutId)
3077
+ pending.reject(new Error("Client destroyed"))
3078
+ })
3079
+ this.pendingRequests.clear()
3080
+
3081
+ // Remove message listener
3082
+ if (this.messageListener) {
3083
+ window.removeEventListener("message", this.messageListener)
3084
+ this.messageListener = undefined
3085
+ }
3086
+
3087
+ // Remove iframe
3088
+ if (this.iframe && this.iframe.parentNode) {
3089
+ this.iframe.parentNode.removeChild(this.iframe)
3090
+ this.iframe = undefined
3091
+ }
3092
+
3093
+ this.ready = false
3094
+ }
3095
+ }