@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,3891 @@
1
+ import type {
2
+ ParentToIframeMessage,
3
+ IframeToParentMessage,
4
+ ButtonStyles,
5
+ ButtonConfig,
6
+ RequestAuthMessage,
7
+ UploadDataMessage,
8
+ DownloadDataMessage,
9
+ UploadFileMessage,
10
+ DownloadFileMessage,
11
+ UploadChunkMessage,
12
+ DownloadChunkMessage,
13
+ GetConnectionInfoMessage,
14
+ IsConnectedMessage,
15
+ GetNodeInfoMessage,
16
+ GsocMineMessage,
17
+ GsocSendMessage,
18
+ SocUploadMessage,
19
+ SocRawUploadMessage,
20
+ SocDownloadMessage,
21
+ SocRawDownloadMessage,
22
+ SocGetOwnerMessage,
23
+ EpochFeedDownloadReferenceMessage,
24
+ EpochFeedUploadReferenceMessage,
25
+ FeedGetOwnerMessage,
26
+ SequentialFeedGetOwnerMessage,
27
+ SequentialFeedDownloadPayloadMessage,
28
+ SequentialFeedDownloadRawPayloadMessage,
29
+ SequentialFeedDownloadReferenceMessage,
30
+ SequentialFeedUploadPayloadMessage,
31
+ SequentialFeedUploadRawPayloadMessage,
32
+ SequentialFeedUploadReferenceMessage,
33
+ ActUploadDataMessage,
34
+ ActDownloadDataMessage,
35
+ ActAddGranteesMessage,
36
+ ActRevokeGranteesMessage,
37
+ ActGetGranteesMessage,
38
+ GetPostageBatchMessage,
39
+ CreateFeedManifestMessage,
40
+ AppMetadata,
41
+ PostageStamp,
42
+ PostageBatch,
43
+ ConnectedApp,
44
+ } from "./types"
45
+ import {
46
+ ParentToIframeMessageSchema,
47
+ PopupToIframeMessageSchema,
48
+ STORAGE_CHALLENGE_KEY,
49
+ } from "./types"
50
+ import type { PopupToIframeMessage } from "./types"
51
+ import {
52
+ Bee,
53
+ BatchId,
54
+ EthAddress,
55
+ PrivateKey,
56
+ Identifier,
57
+ Topic,
58
+ MantarayNode,
59
+ NULL_ADDRESS,
60
+ } from "@ethersphere/bee-js"
61
+ import { makeContentAddressedChunk } from "./chunk"
62
+ import type { BeeRequestOptions } from "@ethersphere/bee-js"
63
+ import { uploadDataWithSigning } from "./proxy/upload-data"
64
+ import {
65
+ uploadEncryptedDataWithSigning,
66
+ uploadEncryptedSOC,
67
+ uploadSOC,
68
+ uploadSOCViaSocEndpoint,
69
+ } from "./proxy/upload-encrypted-data"
70
+ import {
71
+ downloadDataWithChunkAPI,
72
+ downloadSOC,
73
+ downloadEncryptedSOC,
74
+ } from "./proxy/download-data"
75
+ import type { UploadContext, UploadProgress } from "./proxy/types"
76
+ import {
77
+ loadMantarayTreeWithChunkAPI,
78
+ saveMantarayTreeRecursively,
79
+ } from "./proxy/mantaray"
80
+ import { saveMantarayTreeRecursivelyEncrypted } from "./proxy/mantaray-encrypted"
81
+ import { createFeedManifestDirect } from "./proxy/feed-manifest"
82
+ import { UtilizationAwareStamper } from "./utils/batch-utilization"
83
+ import { UtilizationStoreDB } from "./storage/utilization-store"
84
+ import {
85
+ createConnectedAppsStorageManager,
86
+ createIdentitiesStorageManager,
87
+ createPostageStampsStorageManager,
88
+ createNetworkSettingsStorageManager,
89
+ createAccountsStorageManager,
90
+ disconnectApp,
91
+ } from "./utils/storage-managers"
92
+ import { hexToUint8Array, uint8ArrayToHex } from "./utils/key-derivation"
93
+ import {
94
+ createAsyncEpochFinder,
95
+ createEpochUpdater,
96
+ } from "./proxy/feeds/epochs"
97
+ import { createAsyncSequentialFinder } from "./proxy/feeds/sequence"
98
+ import { Binary } from "cafe-utility"
99
+ import { calculateTTLSeconds, fetchSwarmPrice } from "./utils/ttl"
100
+ import { DEFAULT_BEE_NODE_URL, UtilizationUpdateMessageSchema } from "./schemas"
101
+ import { buildAuthUrl } from "./utils/url"
102
+ import {
103
+ createActForContent,
104
+ decryptActReference,
105
+ addGranteesToAct,
106
+ revokeGranteesFromAct,
107
+ getGranteesFromAct,
108
+ parseCompressedPublicKey,
109
+ } from "./proxy/act"
110
+
111
+ const DEFAULT_ACT_FILENAME = "index.bin"
112
+ const DEFAULT_ACT_CONTENT_TYPE = "application/octet-stream"
113
+ const SEQUENTIAL_INDEX_LOOKUP_TIMEOUT_MS = 2000
114
+
115
+ /**
116
+ * Swarm ID Proxy - Runs inside the iframe
117
+ *
118
+ * Responsibilities:
119
+ * - Receive app-specific secrets from auth popup
120
+ * - Store secrets in partitioned localStorage
121
+ * - Proxy Bee API calls from parent dApp
122
+ * - Augment requests with authentication
123
+ * - Return responses to parent dApp
124
+ */
125
+ export class SwarmIdProxy {
126
+ private parentOrigin: string | undefined
127
+ private parentIdentified: boolean = false
128
+ private authenticated: boolean = false
129
+ private authLoading: boolean = true // Start in loading state
130
+ private appSecret: string | undefined
131
+ private postageBatchId: string | undefined
132
+ private signerKey: string | undefined
133
+ private stamper: UtilizationAwareStamper | undefined
134
+ private stamperDepth: number = 23 // Default depth
135
+ private storagePartitioned: boolean = false
136
+ private pendingChallenge: string | undefined
137
+ private storagePartitionedIdentity:
138
+ | { id: string; name: string; address: string }
139
+ | undefined
140
+ private utilizationStore: UtilizationStoreDB | undefined
141
+ private beeApiUrl: string
142
+ private authButtonContainer: HTMLElement | undefined
143
+ private currentStyles: ButtonStyles | undefined
144
+ private buttonConfig: ButtonConfig | undefined
145
+ private popupMode: "popup" | "window" = "window"
146
+ private appMetadata: AppMetadata | undefined
147
+ private bee: Bee
148
+ private unsubscribeConnectedApps: (() => void) | undefined
149
+ private isConnecting: boolean = false
150
+ private parentWindow: WindowProxy | undefined
151
+ private utilizationChannel: BroadcastChannel
152
+
153
+ constructor() {
154
+ // Load Bee API URL from network settings, falling back to default
155
+ const networkSettings = createNetworkSettingsStorageManager().load()
156
+ this.beeApiUrl = networkSettings?.beeNodeUrl || DEFAULT_BEE_NODE_URL
157
+ this.bee = new Bee(this.beeApiUrl)
158
+ this.setupMessageListener()
159
+ this.setupConnectedAppsListener()
160
+
161
+ // Initialize multi-tab coordination via BroadcastChannel
162
+ this.utilizationChannel = new BroadcastChannel("swarm-id-utilization")
163
+ this.setupUtilizationListener()
164
+
165
+ // Announce readiness to parent window immediately
166
+ // This signals that our message listener is ready to receive parentIdentify
167
+ this.announceReady()
168
+ }
169
+
170
+ /**
171
+ * Subscribe to connected apps storage changes for direct mode authentication.
172
+ * When a user completes authentication in the /connect popup (direct mode),
173
+ * the popup writes to localStorage. This storage event notifies the proxy
174
+ * to check for a new valid connection and send authSuccess to the parent.
175
+ * Also handles disconnection when the connection is removed or invalidated.
176
+ *
177
+ * Note: We always set up this listener, even when storage might be partitioned.
178
+ * In some browsers/configurations (like localhost development), storage events
179
+ * work between same-origin windows even in iframes. If storage IS partitioned,
180
+ * the listener simply won't fire, and we fall back to postMessage from the popup.
181
+ */
182
+ private setupConnectedAppsListener(): void {
183
+ // Avoid duplicate subscriptions
184
+ if (this.unsubscribeConnectedApps) {
185
+ return
186
+ }
187
+
188
+ const connectedAppsManager = createConnectedAppsStorageManager()
189
+ this.unsubscribeConnectedApps = connectedAppsManager.subscribe(
190
+ (connectedApps) => {
191
+ this.handleConnectedAppsChange(connectedApps)
192
+ },
193
+ )
194
+ }
195
+
196
+ /**
197
+ * Handle changes to connected apps storage (triggered by storage events from other windows).
198
+ * Handles new connections, identity changes, and disconnections.
199
+ */
200
+ private async handleConnectedAppsChange(
201
+ connectedApps: ConnectedApp[],
202
+ ): Promise<void> {
203
+ if (!this.parentOrigin) {
204
+ return
205
+ }
206
+
207
+ const connectedApp = this.findMostRecentConnection(connectedApps)
208
+
209
+ if (connectedApp) {
210
+ if (!this.authenticated) {
211
+ // New connection
212
+ await this.authenticateFromStorage(connectedApp)
213
+ } else if (connectedApp.appSecret !== this.appSecret) {
214
+ // Identity changed - update to new identity
215
+ await this.authenticateFromStorage(connectedApp)
216
+ }
217
+ // If already authenticated with same secret, nothing to do
218
+ } else if (this.authenticated && !this.storagePartitioned) {
219
+ // No valid connection in storage, but we're authenticated - disconnect.
220
+ // Skip when storage is partitioned: the iframe
221
+ // can't see connected apps, but auth was established via postMessage.
222
+ this.clearAuthData()
223
+ this.sendToParent({
224
+ type: "disconnectResponse",
225
+ requestId: "storage-event",
226
+ success: true,
227
+ })
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Authenticate using data from connected apps storage
233
+ */
234
+ private async authenticateFromStorage(
235
+ connectedApp: ConnectedApp,
236
+ ): Promise<void> {
237
+ this.appSecret = connectedApp.appSecret
238
+ this.authenticated = true
239
+ this.storagePartitioned = false
240
+ this.storagePartitionedIdentity = undefined
241
+ this.authLoading = false
242
+ this.isConnecting = false
243
+
244
+ // Look up postage stamp
245
+ const stamp = this.lookupPostageStampForApp()
246
+ if (stamp) {
247
+ this.postageBatchId = stamp.batchID.toHex()
248
+ this.signerKey = stamp.signerKey.toHex()
249
+ this.stamperDepth = stamp.depth
250
+ await this.initializeStamper()
251
+ }
252
+
253
+ this.showAuthButton()
254
+ this.sendToParent({
255
+ type: "authSuccess",
256
+ origin: this.parentOrigin!,
257
+ })
258
+ }
259
+
260
+ /**
261
+ * Handle messages from the auth popup (same-origin postMessage).
262
+ * Used as storage partitioning fallback when storage events don't fire due to partitioning.
263
+ */
264
+ private async handlePopupMessage(
265
+ message: PopupToIframeMessage,
266
+ ): Promise<void> {
267
+ if (message.type === "setSecret") {
268
+ // Validate the appOrigin matches our parent
269
+ if (message.appOrigin !== this.parentOrigin) {
270
+ console.warn(
271
+ "[Proxy] setSecret appOrigin mismatch:",
272
+ message.appOrigin,
273
+ "!==",
274
+ this.parentOrigin,
275
+ )
276
+ return
277
+ }
278
+
279
+ // Verify challenge matches what we generated in openAuthPopup()
280
+ if (
281
+ !this.pendingChallenge ||
282
+ message.challenge !== this.pendingChallenge
283
+ ) {
284
+ console.warn("[Proxy] setSecret challenge mismatch — ignoring")
285
+ return
286
+ }
287
+
288
+ // Clean up challenge
289
+ this.pendingChallenge = undefined
290
+ localStorage.removeItem(STORAGE_CHALLENGE_KEY)
291
+
292
+ // Authenticate in storage-partitioned mode (no stamps available via postMessage)
293
+ this.appSecret = message.data.secret
294
+ this.authenticated = true
295
+ this.storagePartitioned = true
296
+ this.authLoading = false
297
+ this.isConnecting = false
298
+
299
+ // Store identity info from message (can't read from partitioned localStorage)
300
+ if (
301
+ message.data.identityId &&
302
+ message.data.identityName &&
303
+ message.data.identityAddress
304
+ ) {
305
+ this.storagePartitionedIdentity = {
306
+ id: message.data.identityId,
307
+ name: message.data.identityName,
308
+ address: message.data.identityAddress,
309
+ }
310
+ }
311
+
312
+ // No stamp lookup — localStorage is partitioned, stamps not accessible
313
+ this.postageBatchId = undefined
314
+ this.signerKey = undefined
315
+
316
+ this.showAuthButton()
317
+ this.sendToParent({
318
+ type: "authSuccess",
319
+ origin: this.parentOrigin!,
320
+ })
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Clean up resources when the proxy is destroyed.
326
+ * Call this method when the proxy iframe is being unloaded.
327
+ */
328
+ destroy(): void {
329
+ if (this.unsubscribeConnectedApps) {
330
+ this.unsubscribeConnectedApps()
331
+ this.unsubscribeConnectedApps = undefined
332
+ }
333
+
334
+ // Clean up utilization channel
335
+ this.utilizationChannel.close()
336
+ }
337
+
338
+ /**
339
+ * Setup listener for utilization updates from other tabs.
340
+ * When another tab completes a write, it broadcasts an update.
341
+ * This tab applies the delta update directly from the message.
342
+ */
343
+ private setupUtilizationListener(): void {
344
+ this.utilizationChannel.onmessage = (event) => {
345
+ try {
346
+ const result = UtilizationUpdateMessageSchema.safeParse(event.data)
347
+ if (
348
+ result.success &&
349
+ result.data.batchId === this.postageBatchId &&
350
+ this.stamper
351
+ ) {
352
+ // Apply delta update directly - no IndexedDB read needed
353
+ this.stamper.applyUtilizationUpdate(result.data.buckets)
354
+ }
355
+ } catch (error) {
356
+ console.error("[Proxy] Failed to apply utilization update:", error)
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Execute a write operation with an exclusive lock across all tabs.
363
+ * Uses Web Locks API to ensure only one write happens at a time.
364
+ * Lock is scoped to the batch ID to allow different batches to write concurrently.
365
+ */
366
+ private async withWriteLock<T>(operation: () => Promise<T>): Promise<T> {
367
+ const lockName = `swarm-write-${this.postageBatchId}`
368
+ return navigator.locks.request(
369
+ lockName,
370
+ { mode: "exclusive" },
371
+ async () => {
372
+ try {
373
+ return await operation()
374
+ } finally {
375
+ }
376
+ },
377
+ )
378
+ }
379
+
380
+ /**
381
+ * Announce that proxy is ready to receive messages
382
+ * Broadcasts to parent with wildcard origin since we don't know parent origin yet
383
+ */
384
+ private announceReady(): void {
385
+ if (window.parent && window.parent !== window) {
386
+ window.parent.postMessage(
387
+ { type: "proxyInitialized" },
388
+ "*", // Wildcard since we don't know parent origin yet
389
+ )
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Get the stored postage batch ID
395
+ */
396
+ getPostageBatchId(): string | undefined {
397
+ return this.postageBatchId
398
+ }
399
+
400
+ /**
401
+ * Get the stored signer key
402
+ */
403
+ getSignerKey(): string | undefined {
404
+ return this.signerKey
405
+ }
406
+
407
+ /**
408
+ * Initialize the Stamper for client-side signing
409
+ * Uses UtilizationAwareStamper to track bucket usage
410
+ */
411
+ private async initializeStamper(): Promise<void> {
412
+ if (!this.signerKey || !this.postageBatchId) {
413
+ console.warn(
414
+ "[Proxy] Cannot initialize stamper: missing signer key or batch ID",
415
+ )
416
+ return
417
+ }
418
+
419
+ // Look up account info for utilization tracking
420
+ const accountInfo = this.lookupAccountForApp()
421
+ if (!accountInfo) {
422
+ console.warn("[Proxy] Cannot initialize stamper: account not found")
423
+ return
424
+ }
425
+
426
+ try {
427
+ // Initialize utilization cache if not already done
428
+ if (!this.utilizationStore) {
429
+ this.utilizationStore = new UtilizationStoreDB()
430
+ }
431
+
432
+ // Create utilization-aware stamper with owner and encryption key
433
+ // This enables proper utilization tracking and persistence
434
+ this.stamper = await UtilizationAwareStamper.create(
435
+ this.signerKey,
436
+ new BatchId(this.postageBatchId),
437
+ this.stamperDepth,
438
+ this.utilizationStore,
439
+ accountInfo.owner,
440
+ accountInfo.encryptionKey,
441
+ )
442
+ } catch (error) {
443
+ console.error("[Proxy] Failed to initialize stamper:", error)
444
+ this.stamper = undefined
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Save stamper bucket state to IndexedDB
450
+ * Utilization-aware stamper persists bucket state automatically
451
+ */
452
+ private async saveStamperState(): Promise<void> {
453
+ if (!this.stamper) {
454
+ return
455
+ }
456
+
457
+ try {
458
+ // Capture bucket updates BEFORE flush clears dirtyBuckets
459
+ const buckets = this.stamper.getBucketUpdatesForBroadcast()
460
+
461
+ await this.stamper.flush()
462
+
463
+ // Broadcast utilization update to other tabs with pre-captured buckets
464
+ if (this.postageBatchId && buckets.length > 0) {
465
+ this.utilizationChannel.postMessage({
466
+ type: "utilization-updated",
467
+ batchId: this.postageBatchId,
468
+ buckets,
469
+ })
470
+ }
471
+ } catch (error) {
472
+ console.error("[Proxy] Failed to save stamper state:", error)
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Setup message listener for parent and popup messages
478
+ */
479
+ private setupMessageListener(): void {
480
+ window.addEventListener("message", async (event: MessageEvent) => {
481
+ const { type } = event.data
482
+
483
+ // Handle parent identification (must come first)
484
+ if (type === "parentIdentify") {
485
+ await this.handleParentIdentify(event)
486
+ return
487
+ }
488
+
489
+ // All other messages require parent to be identified first
490
+ if (!this.parentIdentified) {
491
+ console.warn("[Proxy] Ignoring message - parent not identified yet")
492
+ return
493
+ }
494
+
495
+ // Handle same-origin popup messages (storage partitioning postMessage fallback)
496
+ if (event.origin === window.location.origin && type === "setSecret") {
497
+ const popupResult = PopupToIframeMessageSchema.safeParse(event.data)
498
+ if (popupResult.success) {
499
+ await this.handlePopupMessage(popupResult.data)
500
+ return
501
+ }
502
+ }
503
+
504
+ // Validate origin - only accept messages from parent
505
+ if (event.origin !== this.parentOrigin) {
506
+ console.warn(
507
+ "[Proxy] Rejected message from unauthorized origin:",
508
+ event.origin,
509
+ )
510
+ return
511
+ }
512
+
513
+ // Handle setButtonStyles message (UI-only, not in schema)
514
+ if (type === "setButtonStyles") {
515
+ this.currentStyles = event.data.styles
516
+ // Re-render button if not authenticated
517
+ if (!this.authenticated && this.authButtonContainer) {
518
+ this.showAuthButton()
519
+ }
520
+ return
521
+ }
522
+
523
+ let message: ParentToIframeMessage
524
+ try {
525
+ message = ParentToIframeMessageSchema.parse(event.data)
526
+ } catch (error) {
527
+ console.warn("[Proxy] Invalid parent message:", error)
528
+ return
529
+ }
530
+
531
+ try {
532
+ await this.handleParentMessage(message, event)
533
+ } catch (error) {
534
+ console.error("[Proxy] Error handling parent message:", error)
535
+ this.sendErrorToParent(
536
+ event,
537
+ (message as { requestId?: string }).requestId,
538
+ error instanceof Error ? error.message : "Unknown error",
539
+ )
540
+ }
541
+ })
542
+ }
543
+
544
+ /**
545
+ * Handle parent identification
546
+ */
547
+ private async handleParentIdentify(event: MessageEvent): Promise<void> {
548
+ // Prevent parent from changing after first identification
549
+ if (this.parentIdentified) {
550
+ console.error("[Proxy] Parent already identified! Ignoring duplicate.")
551
+ return
552
+ }
553
+
554
+ // Parse the message to get optional parameters
555
+ const message = event.data
556
+ const parentPopupMode = message.popupMode
557
+ const parentMetadata = message.metadata
558
+ const parentButtonConfig = message.buttonConfig
559
+
560
+ // Trust event.origin - this is browser-enforced and cannot be spoofed
561
+ this.parentOrigin = event.origin
562
+ this.parentIdentified = true
563
+ // Store reference to parent window for later postMessage calls
564
+ if (event.source) {
565
+ this.parentWindow = event.source as WindowProxy
566
+ }
567
+
568
+ // Use parent's popup mode if provided
569
+ if (parentPopupMode) {
570
+ this.popupMode = parentPopupMode
571
+ }
572
+
573
+ // Store metadata from parent
574
+ if (parentMetadata) {
575
+ this.appMetadata = parentMetadata
576
+ }
577
+
578
+ // Store button config from parent
579
+ if (parentButtonConfig) {
580
+ this.buttonConfig = parentButtonConfig
581
+ }
582
+
583
+ // Load existing secret if available
584
+ await this.loadAuthData()
585
+
586
+ // Acknowledge receipt
587
+ if (event.source) {
588
+ ;(event.source as WindowProxy).postMessage(
589
+ {
590
+ type: "proxyReady",
591
+ authenticated: this.authenticated,
592
+ parentOrigin: this.parentOrigin,
593
+ } satisfies IframeToParentMessage,
594
+ { targetOrigin: event.origin },
595
+ )
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Handle messages from parent window
601
+ */
602
+ private async handleParentMessage(
603
+ message: ParentToIframeMessage,
604
+ event: MessageEvent,
605
+ ): Promise<void> {
606
+ switch (message.type) {
607
+ case "parentIdentify":
608
+ // Already handled above
609
+ break
610
+
611
+ case "checkAuth":
612
+ this.handleCheckAuth(message, event)
613
+ break
614
+
615
+ case "disconnect":
616
+ this.handleDisconnect(message, event)
617
+ break
618
+
619
+ case "requestAuth":
620
+ this.handleRequestAuth(message, event)
621
+ break
622
+
623
+ case "uploadData":
624
+ await this.handleUploadData(message, event)
625
+ break
626
+
627
+ case "downloadData":
628
+ await this.handleDownloadData(message, event)
629
+ break
630
+
631
+ case "uploadFile":
632
+ await this.handleUploadFile(message, event)
633
+ break
634
+
635
+ case "downloadFile":
636
+ await this.handleDownloadFile(message, event)
637
+ break
638
+
639
+ case "uploadChunk":
640
+ await this.handleUploadChunk(message, event)
641
+ break
642
+
643
+ case "downloadChunk":
644
+ await this.handleDownloadChunk(message, event)
645
+ break
646
+
647
+ case "getConnectionInfo":
648
+ this.handleGetConnectionInfo(message, event)
649
+ break
650
+
651
+ case "isConnected":
652
+ await this.handleIsConnected(message, event)
653
+ break
654
+
655
+ case "getNodeInfo":
656
+ await this.handleGetNodeInfo(message, event)
657
+ break
658
+
659
+ case "gsocMine":
660
+ this.handleGsocMine(message, event)
661
+ break
662
+
663
+ case "gsocSend":
664
+ await this.handleGsocSend(message, event)
665
+ break
666
+ case "socUpload":
667
+ await this.handleSocUpload(message, event)
668
+ break
669
+ case "socRawUpload":
670
+ await this.handleSocRawUpload(message, event)
671
+ break
672
+ case "socDownload":
673
+ await this.handleSocDownload(message, event)
674
+ break
675
+ case "socRawDownload":
676
+ await this.handleSocRawDownload(message, event)
677
+ break
678
+ case "socGetOwner":
679
+ await this.handleSocGetOwner(message, event)
680
+ break
681
+ case "epochFeedDownloadReference":
682
+ await this.handleEpochFeedDownloadReference(message, event)
683
+ break
684
+ case "epochFeedUploadReference":
685
+ await this.handleEpochFeedUploadReference(message, event)
686
+ break
687
+ case "feedGetOwner":
688
+ await this.handleFeedGetOwner(message, event)
689
+ break
690
+ case "seqFeedGetOwner":
691
+ await this.handleSequentialFeedGetOwner(message, event)
692
+ break
693
+ case "seqFeedDownloadPayload":
694
+ await this.handleSequentialFeedDownloadPayload(message, event)
695
+ break
696
+ case "seqFeedDownloadRawPayload":
697
+ await this.handleSequentialFeedDownloadRawPayload(message, event)
698
+ break
699
+ case "seqFeedDownloadReference":
700
+ await this.handleSequentialFeedDownloadReference(message, event)
701
+ break
702
+ case "seqFeedUploadPayload":
703
+ await this.handleSequentialFeedUploadPayload(message, event)
704
+ break
705
+ case "seqFeedUploadRawPayload":
706
+ await this.handleSequentialFeedUploadRawPayload(message, event)
707
+ break
708
+ case "seqFeedUploadReference":
709
+ await this.handleSequentialFeedUploadReference(message, event)
710
+ break
711
+
712
+ case "actUploadData":
713
+ await this.handleActUploadData(message, event)
714
+ break
715
+
716
+ case "actDownloadData":
717
+ await this.handleActDownloadData(message, event)
718
+ break
719
+
720
+ case "actAddGrantees":
721
+ await this.handleActAddGrantees(message, event)
722
+ break
723
+
724
+ case "actRevokeGrantees":
725
+ await this.handleActRevokeGrantees(message, event)
726
+ break
727
+
728
+ case "actGetGrantees":
729
+ await this.handleActGetGrantees(message, event)
730
+ break
731
+
732
+ case "getPostageBatch":
733
+ await this.handleGetPostageBatch(message, event)
734
+ break
735
+
736
+ case "createFeedManifest":
737
+ await this.handleCreateFeedManifest(message, event)
738
+ break
739
+
740
+ case "connect":
741
+ this.handleConnect(message, event)
742
+ break
743
+
744
+ default:
745
+ // TypeScript should ensure this is never reached
746
+ const exhaustiveCheck: never = message
747
+ console.warn("[Proxy] Unhandled message type:", exhaustiveCheck)
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Load authentication data from shared storage (ConnectedApp records).
753
+ */
754
+ private async loadAuthData(): Promise<void> {
755
+ if (!this.parentOrigin) {
756
+ this.authLoading = false
757
+ return
758
+ }
759
+
760
+ const sharedData = this.lookupAppSecretFromSharedStorage()
761
+
762
+ if (sharedData) {
763
+ this.appSecret = sharedData.secret
764
+ this.authenticated = true
765
+ this.authLoading = false
766
+ this.showAuthButton()
767
+
768
+ // Look up postage stamp from shared storage based on connected identity
769
+ const stamp = this.lookupPostageStampForApp()
770
+ if (stamp) {
771
+ this.postageBatchId = stamp.batchID.toHex()
772
+ this.signerKey = stamp.signerKey.toHex()
773
+ this.stamperDepth = stamp.depth
774
+ await this.initializeStamper()
775
+ } else {
776
+ this.postageBatchId = undefined
777
+ this.signerKey = undefined
778
+ }
779
+ } else {
780
+ this.authLoading = false
781
+ this.showAuthButton()
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Look up the postage stamp for the currently connected app's identity
787
+ * by reading from shared localStorage stores.
788
+ */
789
+ private lookupPostageStampForApp(): PostageStamp | undefined {
790
+ if (!this.parentOrigin) {
791
+ return undefined
792
+ }
793
+
794
+ try {
795
+ // Load connected apps to find which identity is connected to this app
796
+ const connectedAppsManager = createConnectedAppsStorageManager()
797
+ const connectedApps = connectedAppsManager.load()
798
+ const connectedApp = this.findMostRecentConnection(connectedApps)
799
+
800
+ if (!connectedApp) {
801
+ return undefined
802
+ }
803
+
804
+ // Load identities to find the account for this identity
805
+ const identitiesManager = createIdentitiesStorageManager()
806
+ const identities = identitiesManager.load()
807
+ const identity = identities.find((i) => i.id === connectedApp.identityId)
808
+
809
+ if (!identity) {
810
+ return undefined
811
+ }
812
+
813
+ // Load postage stamps and find one for this account
814
+ const postageStampsManager = createPostageStampsStorageManager()
815
+ const stamps = postageStampsManager.load()
816
+
817
+ // First try identity's default stamp, then fall back to any account stamp
818
+ let stamp: PostageStamp | undefined
819
+ if (identity.defaultPostageStampBatchID) {
820
+ stamp = stamps.find((s) =>
821
+ s.batchID.equals(identity.defaultPostageStampBatchID!),
822
+ )
823
+ }
824
+
825
+ if (!stamp) {
826
+ stamp = stamps.find((s) => s.accountId === identity.accountId.toHex())
827
+ }
828
+
829
+ if (stamp) {
830
+ }
831
+
832
+ return stamp
833
+ } catch (error) {
834
+ console.error("[Proxy] Error looking up postage stamp:", error)
835
+ return undefined
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Look up the account for the currently connected app's identity
841
+ * by reading from shared localStorage stores.
842
+ *
843
+ * @returns Account info with owner address and encryption key, or undefined if not found
844
+ */
845
+ private lookupAccountForApp():
846
+ | { owner: EthAddress; encryptionKey: Uint8Array }
847
+ | undefined {
848
+ if (!this.parentOrigin) {
849
+ return undefined
850
+ }
851
+
852
+ try {
853
+ // Load connected apps to find which identity is connected to this app
854
+ const connectedAppsManager = createConnectedAppsStorageManager()
855
+ const connectedApps = connectedAppsManager.load()
856
+ const connectedApp = this.findMostRecentConnection(connectedApps)
857
+
858
+ if (!connectedApp) {
859
+ return undefined
860
+ }
861
+
862
+ // Load identities to find the account for this identity
863
+ const identitiesManager = createIdentitiesStorageManager()
864
+ const identities = identitiesManager.load()
865
+ const identity = identities.find((i) => i.id === connectedApp.identityId)
866
+
867
+ if (!identity) {
868
+ return undefined
869
+ }
870
+
871
+ // Load accounts and find the one for this identity
872
+ const accountsManager = createAccountsStorageManager()
873
+ const accounts = accountsManager.load()
874
+ const account = accounts.find((a) => a.id.equals(identity.accountId))
875
+
876
+ if (!account) {
877
+ return undefined
878
+ }
879
+
880
+ return {
881
+ owner: account.id,
882
+ encryptionKey: hexToUint8Array(account.swarmEncryptionKey),
883
+ }
884
+ } catch (error) {
885
+ console.error("[Proxy] Error looking up account:", error)
886
+ return undefined
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Check if a connection is still valid based on connectedUntil timestamp
892
+ */
893
+ private isConnectionValid(connectedApp: ConnectedApp): boolean {
894
+ if (!connectedApp.connectedUntil) return false
895
+ return connectedApp.connectedUntil > Date.now()
896
+ }
897
+
898
+ /**
899
+ * Find the most recently connected valid entry for the current parent origin.
900
+ * Resolves ambiguity when multiple identities are connected to the same app
901
+ * by sorting by lastConnectedAt descending and returning the first valid one.
902
+ */
903
+ private findMostRecentConnection(
904
+ connectedApps: ConnectedApp[],
905
+ ): ConnectedApp | undefined {
906
+ return connectedApps
907
+ .filter(
908
+ (app) =>
909
+ app.appUrl === this.parentOrigin && this.isConnectionValid(app),
910
+ )
911
+ .sort((a, b) => b.lastConnectedAt - a.lastConnectedAt)[0]
912
+ }
913
+
914
+ /**
915
+ * Look up the app secret from shared storage for the current parent origin.
916
+ * Returns the secret and identityId if found and connection is valid.
917
+ */
918
+ private lookupAppSecretFromSharedStorage():
919
+ | { secret: string; identityId: string }
920
+ | undefined {
921
+ if (!this.parentOrigin) {
922
+ return undefined
923
+ }
924
+
925
+ try {
926
+ const connectedAppsManager = createConnectedAppsStorageManager()
927
+ const connectedApps = connectedAppsManager.load()
928
+ const connectedApp = this.findMostRecentConnection(connectedApps)
929
+
930
+ if (!connectedApp) {
931
+ return undefined
932
+ }
933
+
934
+ if (!connectedApp.appSecret) {
935
+ return undefined
936
+ }
937
+
938
+ return {
939
+ secret: connectedApp.appSecret,
940
+ identityId: connectedApp.identityId,
941
+ }
942
+ } catch (error) {
943
+ console.error(
944
+ "[Proxy] Error looking up app secret from shared storage:",
945
+ error,
946
+ )
947
+ return undefined
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Clear authentication data
953
+ */
954
+ private clearAuthData(): void {
955
+ if (!this.parentOrigin) {
956
+ return
957
+ }
958
+
959
+ // Clear stamper state from localStorage
960
+ const stamperKey = `swarm-stamper-${this.parentOrigin}-${this.postageBatchId}`
961
+ localStorage.removeItem(stamperKey)
962
+
963
+ // Invalidate connected app entries in shared storage so reconnect doesn't happen on refresh
964
+ try {
965
+ disconnectApp(this.parentOrigin)
966
+ } catch (error) {
967
+ console.error(
968
+ "[Proxy] Error invalidating connected app in shared storage:",
969
+ error,
970
+ )
971
+ }
972
+
973
+ // Reset auth state
974
+ this.authenticated = false
975
+ this.authLoading = false
976
+ this.appSecret = undefined
977
+ this.postageBatchId = undefined
978
+ this.signerKey = undefined
979
+ this.stamper = undefined
980
+ this.storagePartitioned = false
981
+ this.storagePartitionedIdentity = undefined
982
+ this.pendingChallenge = undefined
983
+
984
+ // Show login button
985
+ this.showAuthButton()
986
+ }
987
+
988
+ /**
989
+ * Send error message to parent
990
+ */
991
+ private sendErrorToParent(
992
+ event: MessageEvent,
993
+ requestId: string | undefined,
994
+ error: string,
995
+ ): void {
996
+ if (event.source && requestId) {
997
+ ;(event.source as WindowProxy).postMessage(
998
+ {
999
+ type: "error",
1000
+ requestId,
1001
+ error,
1002
+ } satisfies IframeToParentMessage,
1003
+ { targetOrigin: event.origin },
1004
+ )
1005
+ }
1006
+ }
1007
+
1008
+ private ensureCanUpload(): void {
1009
+ if (this.storagePartitioned) {
1010
+ throw new Error(
1011
+ "Uploads are unavailable in download-only mode due to browser storage partitioning.",
1012
+ )
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * Send message to parent
1018
+ */
1019
+ private sendToParent(message: IframeToParentMessage): void {
1020
+ if (!this.parentOrigin || !this.parentWindow) {
1021
+ console.warn(
1022
+ "[Proxy] Cannot send message to parent - no parent window reference",
1023
+ )
1024
+ return
1025
+ }
1026
+
1027
+ this.parentWindow.postMessage(message, this.parentOrigin)
1028
+ }
1029
+
1030
+ // ============================================================================
1031
+ // Message Handlers
1032
+ // ============================================================================
1033
+
1034
+ private handleCheckAuth(
1035
+ message: { type: "checkAuth"; requestId: string },
1036
+ event: MessageEvent,
1037
+ ): void {
1038
+ if (event.source) {
1039
+ ;(event.source as WindowProxy).postMessage(
1040
+ {
1041
+ type: "authStatusResponse",
1042
+ requestId: message.requestId,
1043
+ authenticated: this.authenticated,
1044
+ origin: this.authenticated ? this.parentOrigin : undefined,
1045
+ } satisfies IframeToParentMessage,
1046
+ { targetOrigin: event.origin },
1047
+ )
1048
+ }
1049
+ }
1050
+
1051
+ private handleGetConnectionInfo(
1052
+ message: GetConnectionInfoMessage,
1053
+ event: MessageEvent,
1054
+ ): void {
1055
+ let identity: { id: string; name: string; address: string } | undefined =
1056
+ undefined
1057
+
1058
+ // Look up identity info if authenticated
1059
+ if (this.authenticated && this.parentOrigin) {
1060
+ if (this.storagePartitioned && this.storagePartitionedIdentity) {
1061
+ // Storage is partitioned, use identity info from the setSecret message
1062
+ // (localStorage is partitioned, can't read connected apps)
1063
+ identity = this.storagePartitionedIdentity
1064
+ } else {
1065
+ try {
1066
+ const connectedAppsManager = createConnectedAppsStorageManager()
1067
+ const connectedApps = connectedAppsManager.load()
1068
+ const connectedApp = this.findMostRecentConnection(connectedApps)
1069
+
1070
+ if (connectedApp) {
1071
+ const identitiesManager = createIdentitiesStorageManager()
1072
+ const identities = identitiesManager.load()
1073
+ const foundIdentity = identities.find(
1074
+ (i) => i.id === connectedApp.identityId,
1075
+ )
1076
+
1077
+ if (foundIdentity) {
1078
+ identity = {
1079
+ id: foundIdentity.id,
1080
+ name: foundIdentity.name,
1081
+ address: foundIdentity.id,
1082
+ }
1083
+ }
1084
+ }
1085
+ } catch (error) {
1086
+ console.error("[Proxy] Error looking up identity:", error)
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ // canUpload is true if we have stamps and storage is not partitioned
1092
+ const canUpload =
1093
+ !!(this.postageBatchId && this.signerKey) && !this.storagePartitioned
1094
+
1095
+ if (event.source) {
1096
+ ;(event.source as WindowProxy).postMessage(
1097
+ {
1098
+ type: "connectionInfoResponse",
1099
+ requestId: message.requestId,
1100
+ canUpload,
1101
+ storagePartitioned: this.storagePartitioned || undefined,
1102
+ identity,
1103
+ } satisfies IframeToParentMessage,
1104
+ { targetOrigin: event.origin },
1105
+ )
1106
+ }
1107
+ }
1108
+
1109
+ private async handleIsConnected(
1110
+ message: IsConnectedMessage,
1111
+ event: MessageEvent,
1112
+ ): Promise<void> {
1113
+ const connected = await this.bee.isConnected()
1114
+
1115
+ if (event.source) {
1116
+ ;(event.source as WindowProxy).postMessage(
1117
+ {
1118
+ type: "isConnectedResponse",
1119
+ requestId: message.requestId,
1120
+ connected,
1121
+ } satisfies IframeToParentMessage,
1122
+ { targetOrigin: event.origin },
1123
+ )
1124
+ }
1125
+ }
1126
+
1127
+ private async handleGetNodeInfo(
1128
+ message: GetNodeInfoMessage,
1129
+ event: MessageEvent,
1130
+ ): Promise<void> {
1131
+ try {
1132
+ const nodeInfo = await this.bee.getNodeInfo()
1133
+
1134
+ if (event.source) {
1135
+ ;(event.source as WindowProxy).postMessage(
1136
+ {
1137
+ type: "getNodeInfoResponse",
1138
+ requestId: message.requestId,
1139
+ beeMode: nodeInfo.beeMode,
1140
+ chequebookEnabled: nodeInfo.chequebookEnabled,
1141
+ swapEnabled: nodeInfo.swapEnabled,
1142
+ } satisfies IframeToParentMessage,
1143
+ { targetOrigin: event.origin },
1144
+ )
1145
+ }
1146
+ } catch (error) {
1147
+ this.sendErrorToParent(
1148
+ event,
1149
+ message.requestId,
1150
+ error instanceof Error ? error.message : "Failed to get node info",
1151
+ )
1152
+ }
1153
+ }
1154
+
1155
+ private handleDisconnect(
1156
+ message: { type: "disconnect"; requestId: string },
1157
+ event: MessageEvent,
1158
+ ): void {
1159
+ // Clear auth data
1160
+ this.clearAuthData()
1161
+
1162
+ // Send response
1163
+ if (event.source) {
1164
+ ;(event.source as WindowProxy).postMessage(
1165
+ {
1166
+ type: "disconnectResponse",
1167
+ requestId: message.requestId,
1168
+ success: true,
1169
+ } satisfies IframeToParentMessage,
1170
+ { targetOrigin: event.origin },
1171
+ )
1172
+ }
1173
+ }
1174
+
1175
+ private handleRequestAuth(
1176
+ message: RequestAuthMessage,
1177
+ _event: MessageEvent,
1178
+ ): void {
1179
+ // Store styles for button creation
1180
+ this.currentStyles = message.styles
1181
+
1182
+ // If container is set, show the button
1183
+ if (this.authButtonContainer) {
1184
+ this.showAuthButton()
1185
+ }
1186
+ }
1187
+
1188
+ /**
1189
+ * Show authentication button in the UI
1190
+ */
1191
+ private showAuthButton(): void {
1192
+ if (!this.authButtonContainer || this.isConnecting) {
1193
+ return
1194
+ }
1195
+
1196
+ // Clear existing content
1197
+ this.authButtonContainer.innerHTML = ""
1198
+
1199
+ // Create button based on authentication status
1200
+ const button = document.createElement("button")
1201
+ const isAuthenticated = this.authenticated
1202
+ const isLoading = this.authLoading
1203
+
1204
+ // Get text from buttonConfig or use defaults
1205
+ const config = this.buttonConfig || {}
1206
+ const loadingText = config.loadingText || "⏳ Loading..."
1207
+ const disconnectText =
1208
+ config.disconnectText || "🔓 Disconnect from Swarm ID"
1209
+ const connectText = config.connectText || "🔐 Login with Swarm ID"
1210
+
1211
+ if (isLoading) {
1212
+ button.textContent = loadingText
1213
+ button.disabled = true
1214
+ } else if (isAuthenticated) {
1215
+ button.textContent = disconnectText
1216
+ } else {
1217
+ button.textContent = connectText
1218
+ }
1219
+
1220
+ // Apply styles from currentStyles (for backward compat) and buttonConfig
1221
+ const styles = this.currentStyles || {}
1222
+
1223
+ // Make button fill container
1224
+ button.style.width = "100%"
1225
+ button.style.height = "100%"
1226
+ button.style.display = "flex"
1227
+ button.style.alignItems = "center"
1228
+ button.style.justifyContent = "center"
1229
+
1230
+ if (isLoading) {
1231
+ button.style.backgroundColor = "#999"
1232
+ button.style.cursor = "default"
1233
+ } else if (isAuthenticated) {
1234
+ // Different color for disconnect button (use default gray unless overridden)
1235
+ button.style.backgroundColor = "#666"
1236
+ button.style.cursor = styles.cursor || "pointer"
1237
+ } else {
1238
+ // Use buttonConfig colors, then fall back to currentStyles, then defaults
1239
+ button.style.backgroundColor =
1240
+ config.backgroundColor || styles.backgroundColor || "#dd7200"
1241
+ button.style.cursor = styles.cursor || "pointer"
1242
+ }
1243
+ button.style.color = config.color || styles.color || "white"
1244
+ button.style.border = styles.border || "none"
1245
+ button.style.borderRadius =
1246
+ config.borderRadius || styles.borderRadius || "0"
1247
+ button.style.padding = styles.padding || "0"
1248
+ button.style.fontSize = styles.fontSize || "14px"
1249
+ button.style.fontWeight = styles.fontWeight || "600"
1250
+
1251
+ // Click handler
1252
+ button.addEventListener("click", () => {
1253
+ if (isAuthenticated) {
1254
+ // Handle disconnect
1255
+ this.handleDisconnectClick()
1256
+ } else {
1257
+ // Handle login
1258
+ this.handleLoginClick(button)
1259
+ }
1260
+ })
1261
+
1262
+ this.authButtonContainer.appendChild(button)
1263
+ }
1264
+
1265
+ private handleConnect(
1266
+ message: {
1267
+ type: "connect"
1268
+ requestId: string
1269
+ agent?: boolean
1270
+ popupMode?: "popup" | "window"
1271
+ },
1272
+ event: MessageEvent,
1273
+ ): void {
1274
+ const success = this.openAuthPopup({
1275
+ agent: message.agent,
1276
+ popupMode: message.popupMode,
1277
+ })
1278
+ ;(event.source as WindowProxy).postMessage(
1279
+ {
1280
+ type: "connectResponse",
1281
+ requestId: message.requestId,
1282
+ success,
1283
+ },
1284
+ { targetOrigin: event.origin },
1285
+ )
1286
+ }
1287
+
1288
+ /**
1289
+ * Open the authentication popup window.
1290
+ * Returns true if popup was opened, false if parent origin is not set.
1291
+ */
1292
+ private openAuthPopup(options?: {
1293
+ agent?: boolean
1294
+ popupMode?: "popup" | "window"
1295
+ }): boolean {
1296
+ if (!this.parentOrigin) {
1297
+ console.error("[Proxy] Cannot open auth window - parent origin not set")
1298
+ return false
1299
+ }
1300
+
1301
+ // Build authentication URL using shared utility
1302
+ // challenge: used for storage partitioning detection — popup checks if it can read this from localStorage
1303
+ const challenge = crypto.randomUUID()
1304
+ this.pendingChallenge = challenge
1305
+ localStorage.setItem(STORAGE_CHALLENGE_KEY, challenge)
1306
+
1307
+ // Get base path from current location (e.g., /id/pr-140/proxy -> /id/pr-140)
1308
+ const basePath = window.location.pathname.replace(/\/proxy$/, "")
1309
+ const authUrl = buildAuthUrl(
1310
+ window.location.origin + basePath,
1311
+ this.parentOrigin,
1312
+ this.appMetadata,
1313
+ { challenge, agent: options?.agent },
1314
+ )
1315
+
1316
+ // Open as popup or full window based on popupMode (per-call override takes precedence)
1317
+ const effectivePopupMode = options?.popupMode ?? this.popupMode
1318
+ let popup: Window | null = null
1319
+ if (effectivePopupMode === "popup") {
1320
+ popup = window.open(authUrl, "_blank", "width=500,height=600")
1321
+ } else {
1322
+ popup = window.open(authUrl, "_blank")
1323
+ }
1324
+
1325
+ // Check if popup was blocked (common on mobile Safari)
1326
+ if (!popup) {
1327
+ console.warn("[Proxy] Popup was blocked or failed to open")
1328
+ this.isConnecting = false
1329
+ this.showAuthButton()
1330
+ return false
1331
+ }
1332
+
1333
+ // Monitor popup closure to handle user closing without completing auth
1334
+ // Note: We delay the start of monitoring because on Safari, popup.closed can
1335
+ // return true immediately for new tabs before they're fully initialized
1336
+ const POPUP_MONITOR_START_DELAY_MS = 2000
1337
+ const POPUP_CLOSE_CHECK_INTERVAL_MS = 500
1338
+ const POPUP_MONITOR_TIMEOUT_MS = 300000 // 5 minutes
1339
+
1340
+ setTimeout(() => {
1341
+ const checkPopupClosed = setInterval(() => {
1342
+ if (popup?.closed) {
1343
+ clearInterval(checkPopupClosed)
1344
+ // Only process if we're still in connecting state (auth didn't complete via storage event)
1345
+ if (this.isConnecting && !this.authenticated) {
1346
+ this.isConnecting = false
1347
+ this.showAuthButton()
1348
+ }
1349
+ }
1350
+ }, POPUP_CLOSE_CHECK_INTERVAL_MS)
1351
+
1352
+ // Clear interval to prevent memory leak
1353
+ setTimeout(() => {
1354
+ clearInterval(checkPopupClosed)
1355
+ }, POPUP_MONITOR_TIMEOUT_MS)
1356
+ }, POPUP_MONITOR_START_DELAY_MS)
1357
+
1358
+ return true
1359
+ }
1360
+
1361
+ /**
1362
+ * Handle login button click
1363
+ */
1364
+ private handleLoginClick(button: HTMLButtonElement): void {
1365
+ this.isConnecting = true
1366
+ // Disable button and show spinner
1367
+ button.disabled = true
1368
+ button.innerHTML =
1369
+ '<span style="display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.3); border-radius: 50%; border-top-color: white; animation: spin 1s linear infinite;"></span>'
1370
+
1371
+ // Add spinner animation
1372
+ if (!document.getElementById("swarm-id-spinner-style")) {
1373
+ const style = document.createElement("style")
1374
+ style.id = "swarm-id-spinner-style"
1375
+ style.textContent =
1376
+ "@keyframes spin { to { transform: rotate(360deg); } }"
1377
+ document.head.appendChild(style)
1378
+ }
1379
+
1380
+ // Open auth popup
1381
+ this.openAuthPopup()
1382
+ }
1383
+
1384
+ /**
1385
+ * Handle disconnect button click
1386
+ */
1387
+ private handleDisconnectClick(): void {
1388
+ // Clear auth data
1389
+ this.clearAuthData()
1390
+
1391
+ // Notify parent about auth status change
1392
+ this.sendToParent({
1393
+ type: "authStatusResponse",
1394
+ requestId: "disconnect",
1395
+ authenticated: false,
1396
+ origin: undefined,
1397
+ })
1398
+ }
1399
+
1400
+ /**
1401
+ * Set container element for auth button
1402
+ */
1403
+ setAuthButtonContainer(container: HTMLElement): void {
1404
+ this.authButtonContainer = container
1405
+ // Show button now that container is available
1406
+ // (loadAuthData may have already run and set authenticated status)
1407
+ this.showAuthButton()
1408
+ }
1409
+
1410
+ private async handleUploadData(
1411
+ message: UploadDataMessage,
1412
+ event: MessageEvent,
1413
+ ): Promise<void> {
1414
+ const { requestId, data, options, requestOptions, enableProgress } = message
1415
+
1416
+ try {
1417
+ if (!this.authenticated || !this.appSecret) {
1418
+ throw new Error("Not authenticated. Please login first.")
1419
+ }
1420
+
1421
+ this.ensureCanUpload()
1422
+
1423
+ if (!this.signerKey || !this.postageBatchId) {
1424
+ throw new Error(
1425
+ "Signer key and postage batch ID required. Please login first.",
1426
+ )
1427
+ }
1428
+
1429
+ if (!this.stamper) {
1430
+ throw new Error("Stamper not initialized. Please login first.")
1431
+ }
1432
+
1433
+ // Progress callback (if enabled)
1434
+ const onProgress = enableProgress
1435
+ ? (progress: UploadProgress) => {
1436
+ if (event.source) {
1437
+ ;(event.source as WindowProxy).postMessage(
1438
+ {
1439
+ type: "uploadProgress",
1440
+ requestId,
1441
+ total: progress.total,
1442
+ processed: progress.processed,
1443
+ } satisfies IframeToParentMessage,
1444
+ { targetOrigin: event.origin },
1445
+ )
1446
+ }
1447
+ }
1448
+ : undefined
1449
+
1450
+ // Serialize write through Web Locks API to prevent concurrent uploads
1451
+ const uploadResult = await this.withWriteLock(async () => {
1452
+ // Prepare upload context
1453
+ const context: UploadContext = {
1454
+ bee: this.bee,
1455
+ stamper: this.stamper!,
1456
+ }
1457
+
1458
+ // Client-side chunking and signing
1459
+ let result
1460
+ if (options?.encrypt) {
1461
+ result = await uploadEncryptedDataWithSigning(
1462
+ context,
1463
+ data,
1464
+ undefined, // encryption key (auto-generated)
1465
+ options,
1466
+ onProgress,
1467
+ requestOptions,
1468
+ )
1469
+ } else {
1470
+ result = await uploadDataWithSigning(
1471
+ context,
1472
+ data,
1473
+ options,
1474
+ onProgress,
1475
+ requestOptions,
1476
+ )
1477
+ }
1478
+
1479
+ // Save stamper state after successful upload
1480
+ await this.saveStamperState()
1481
+
1482
+ return result
1483
+ })
1484
+
1485
+ // Send final response
1486
+ if (event.source) {
1487
+ ;(event.source as WindowProxy).postMessage(
1488
+ {
1489
+ type: "uploadDataResponse",
1490
+ requestId,
1491
+ reference: uploadResult.reference,
1492
+ tagUid: uploadResult.tagUid,
1493
+ } satisfies IframeToParentMessage,
1494
+ { targetOrigin: event.origin },
1495
+ )
1496
+ }
1497
+ } catch (error) {
1498
+ this.sendErrorToParent(
1499
+ event,
1500
+ requestId,
1501
+ error instanceof Error ? error.message : "Upload failed",
1502
+ )
1503
+ }
1504
+ }
1505
+
1506
+ private async handleDownloadData(
1507
+ message: DownloadDataMessage,
1508
+ event: MessageEvent,
1509
+ ): Promise<void> {
1510
+ const { requestId, reference, options, requestOptions } = message
1511
+
1512
+ if (!this.authenticated || !this.appSecret) {
1513
+ throw new Error("Not authenticated. Please login first.")
1514
+ }
1515
+
1516
+ try {
1517
+ // Download data using chunk API only (supports both regular and encrypted references)
1518
+ const data = await downloadDataWithChunkAPI(
1519
+ this.bee,
1520
+ reference,
1521
+ options,
1522
+ undefined,
1523
+ requestOptions,
1524
+ )
1525
+
1526
+ if (event.source) {
1527
+ ;(event.source as WindowProxy).postMessage(
1528
+ {
1529
+ type: "downloadDataResponse",
1530
+ requestId,
1531
+ data: data as Uint8Array,
1532
+ } satisfies IframeToParentMessage,
1533
+ { targetOrigin: event.origin },
1534
+ )
1535
+ }
1536
+ } catch (error) {
1537
+ this.sendErrorToParent(
1538
+ event,
1539
+ requestId,
1540
+ error instanceof Error ? error.message : "Download failed",
1541
+ )
1542
+ }
1543
+ }
1544
+
1545
+ private async handleUploadFile(
1546
+ message: UploadFileMessage,
1547
+ event: MessageEvent,
1548
+ ): Promise<void> {
1549
+ const { requestId, data, name, options, requestOptions } = message
1550
+ const fileName = name || "index.bin"
1551
+
1552
+ try {
1553
+ if (!this.authenticated || !this.appSecret) {
1554
+ throw new Error("Not authenticated. Please login first.")
1555
+ }
1556
+
1557
+ this.ensureCanUpload()
1558
+
1559
+ if (!this.postageBatchId) {
1560
+ throw new Error(
1561
+ "No postage batch ID available. Please authenticate with a valid batch ID.",
1562
+ )
1563
+ }
1564
+
1565
+ if (!this.stamper) {
1566
+ throw new Error("Stamper not initialized. Please login first.")
1567
+ }
1568
+
1569
+ // Serialize write through Web Locks API to prevent concurrent uploads
1570
+ const manifestResult = await this.withWriteLock(async () => {
1571
+ // Prepare upload context
1572
+ const context: UploadContext = {
1573
+ bee: this.bee,
1574
+ stamper: this.stamper!,
1575
+ }
1576
+
1577
+ // Step 1: Upload file data (encrypted by default, unless encrypt=false)
1578
+ const shouldEncryptContent = options?.encrypt !== false
1579
+ let contentUpload: { reference: string; tagUid?: number }
1580
+
1581
+ if (shouldEncryptContent) {
1582
+ contentUpload = await uploadEncryptedDataWithSigning(
1583
+ context,
1584
+ data,
1585
+ undefined, // generate random encryption key
1586
+ options,
1587
+ undefined, // no progress callback for now
1588
+ requestOptions,
1589
+ )
1590
+ } else {
1591
+ contentUpload = await uploadDataWithSigning(
1592
+ context,
1593
+ data,
1594
+ options,
1595
+ undefined, // no progress callback for now
1596
+ requestOptions,
1597
+ )
1598
+ }
1599
+
1600
+ // Step 2: Create Mantaray manifest wrapping the content
1601
+ const manifest = new MantarayNode()
1602
+ const contentReferenceBytes = hexToUint8Array(contentUpload.reference)
1603
+
1604
+ // Add file fork with metadata
1605
+ manifest.addFork(fileName, contentReferenceBytes, {
1606
+ "Content-Type": "application/octet-stream",
1607
+ Filename: fileName,
1608
+ })
1609
+
1610
+ // Add "/" fork with website-index-document pointing to the file
1611
+ manifest.addFork("/", NULL_ADDRESS, {
1612
+ "website-index-document": fileName,
1613
+ })
1614
+
1615
+ // Step 3: Upload manifest (encrypted only if encryptManifest=true)
1616
+ let manifestTag = options?.tag
1617
+ if (!manifestTag) {
1618
+ const tagResponse = await context.bee.createTag()
1619
+ manifestTag = tagResponse.uid
1620
+ }
1621
+
1622
+ const shouldEncryptManifest = options?.encryptManifest === true
1623
+ let result: { rootReference: string; tagUid?: number }
1624
+
1625
+ if (shouldEncryptManifest) {
1626
+ result = await saveMantarayTreeRecursivelyEncrypted(
1627
+ manifest,
1628
+ async (encryptedData, address, isRoot) => {
1629
+ const envelope = context.stamper.stamp({
1630
+ hash: () => address,
1631
+ build: () => encryptedData,
1632
+ span: 0n,
1633
+ writer: undefined as any,
1634
+ })
1635
+ await context.bee.uploadChunk(
1636
+ envelope,
1637
+ encryptedData,
1638
+ { ...options, tag: manifestTag, deferred: false },
1639
+ requestOptions,
1640
+ )
1641
+ return {
1642
+ tagUid: isRoot ? manifestTag : undefined,
1643
+ }
1644
+ },
1645
+ )
1646
+ } else {
1647
+ result = await saveMantarayTreeRecursively(
1648
+ manifest,
1649
+ async (chunkData, isRoot) => {
1650
+ const chunk = makeContentAddressedChunk(chunkData)
1651
+ const envelope = context.stamper.stamp({
1652
+ hash: () => chunk.address.toUint8Array(),
1653
+ build: () => chunk.data,
1654
+ span: chunk.span.toBigInt(),
1655
+ writer: undefined as any,
1656
+ })
1657
+ const uploadResult = await context.bee.uploadChunk(
1658
+ envelope,
1659
+ chunk.data,
1660
+ { ...options, tag: manifestTag, deferred: false },
1661
+ requestOptions,
1662
+ )
1663
+ return {
1664
+ reference: uploadResult.reference.toHex(),
1665
+ tagUid: isRoot ? manifestTag : undefined,
1666
+ }
1667
+ },
1668
+ )
1669
+ }
1670
+
1671
+ // Save stamper state after successful upload
1672
+ await this.saveStamperState()
1673
+
1674
+ return result
1675
+ })
1676
+
1677
+ // Return 128-char encrypted manifest reference (accessible via /bzz/)
1678
+ if (event.source) {
1679
+ ;(event.source as WindowProxy).postMessage(
1680
+ {
1681
+ type: "uploadFileResponse",
1682
+ requestId,
1683
+ reference: manifestResult.rootReference,
1684
+ tagUid: manifestResult.tagUid,
1685
+ } satisfies IframeToParentMessage,
1686
+ { targetOrigin: event.origin },
1687
+ )
1688
+ }
1689
+ } catch (error) {
1690
+ this.sendErrorToParent(
1691
+ event,
1692
+ requestId,
1693
+ error instanceof Error ? error.message : "Upload failed",
1694
+ )
1695
+ }
1696
+ }
1697
+
1698
+ private async handleDownloadFile(
1699
+ message: DownloadFileMessage,
1700
+ event: MessageEvent,
1701
+ ): Promise<void> {
1702
+ const { requestId, reference, path, options, requestOptions } = message
1703
+
1704
+ if (!this.authenticated || !this.appSecret) {
1705
+ throw new Error("Not authenticated. Please login first.")
1706
+ }
1707
+
1708
+ try {
1709
+ // Always load the manifest first - file uploads create manifests
1710
+ const manifest = await loadMantarayTreeWithChunkAPI(
1711
+ this.bee,
1712
+ reference,
1713
+ requestOptions,
1714
+ )
1715
+
1716
+ let targetPath: string
1717
+ if (path) {
1718
+ // Use provided path
1719
+ targetPath = path
1720
+ } else {
1721
+ // No path: get index document from manifest metadata
1722
+ const { indexDocument } = manifest.getDocsMetadata()
1723
+ if (!indexDocument) {
1724
+ throw new Error(
1725
+ "Manifest does not contain an index document reference",
1726
+ )
1727
+ }
1728
+ targetPath = indexDocument
1729
+ }
1730
+
1731
+ // Find the content node at the target path
1732
+ const contentNode = manifest.find(targetPath)
1733
+ if (!contentNode) {
1734
+ throw new Error(`Path not found in manifest: ${targetPath}`)
1735
+ }
1736
+ if (!contentNode.targetAddress) {
1737
+ throw new Error(`Path "${targetPath}" does not have a target address`)
1738
+ }
1739
+
1740
+ // Get filename from metadata or use the path
1741
+ const name =
1742
+ contentNode.metadata?.["Filename"] ||
1743
+ targetPath.split("/").pop() ||
1744
+ "file"
1745
+
1746
+ // Download actual content from the target address
1747
+ const targetRef = uint8ArrayToHex(contentNode.targetAddress)
1748
+
1749
+ const data = await downloadDataWithChunkAPI(
1750
+ this.bee,
1751
+ targetRef,
1752
+ options,
1753
+ undefined,
1754
+ requestOptions,
1755
+ )
1756
+
1757
+ if (event.source) {
1758
+ ;(event.source as WindowProxy).postMessage(
1759
+ {
1760
+ type: "downloadFileResponse",
1761
+ requestId,
1762
+ name,
1763
+ data,
1764
+ } satisfies IframeToParentMessage,
1765
+ { targetOrigin: event.origin },
1766
+ )
1767
+ }
1768
+ } catch (error) {
1769
+ this.sendErrorToParent(
1770
+ event,
1771
+ requestId,
1772
+ error instanceof Error ? error.message : "Download failed",
1773
+ )
1774
+ }
1775
+ }
1776
+
1777
+ private async handleUploadChunk(
1778
+ message: UploadChunkMessage,
1779
+ event: MessageEvent,
1780
+ ): Promise<void> {
1781
+ const { requestId, data, options, requestOptions } = message
1782
+
1783
+ try {
1784
+ if (!this.authenticated || !this.appSecret) {
1785
+ throw new Error("Not authenticated. Please login first.")
1786
+ }
1787
+
1788
+ this.ensureCanUpload()
1789
+
1790
+ if (!this.signerKey || !this.postageBatchId) {
1791
+ throw new Error(
1792
+ "Signer key and postage batch ID required. Please authenticate.",
1793
+ )
1794
+ }
1795
+ // Validate chunk size (must be between 1 and 4096 bytes)
1796
+ if (data.length < 1 || data.length > 4096) {
1797
+ throw new Error(
1798
+ `Invalid chunk size: ${data.length} bytes. Chunks must be between 1 and 4096 bytes.`,
1799
+ )
1800
+ }
1801
+
1802
+ if (!this.stamper) {
1803
+ await this.initializeStamper()
1804
+ }
1805
+
1806
+ if (!this.stamper) {
1807
+ throw new Error("Failed to initialize stamper for signing")
1808
+ }
1809
+
1810
+ // Serialize write through Web Locks API to prevent concurrent uploads
1811
+ const uploadResult = await this.withWriteLock(async () => {
1812
+ // Create content-addressed chunk
1813
+ const chunk = makeContentAddressedChunk(data)
1814
+
1815
+ // Create adapter for cafe-utility Chunk interface
1816
+ const chunkAdapter = {
1817
+ hash: () => chunk.address.toUint8Array(),
1818
+ build: () => chunk.data,
1819
+ span: 0n, // not used by stamper.stamp
1820
+ writer: undefined as any, // not used by stamper.stamp
1821
+ }
1822
+
1823
+ // Sign the chunk to create envelope
1824
+ const envelope = this.stamper!.stamp(chunkAdapter)
1825
+
1826
+ // Create a tag if not provided (required for dev mode)
1827
+ let tag = options?.tag
1828
+ if (!tag) {
1829
+ const tagResponse = await this.bee.createTag()
1830
+ tag = tagResponse.uid
1831
+ }
1832
+
1833
+ // Use non-deferred mode for faster uploads (returns immediately)
1834
+ const uploadOptions = { ...options, tag, deferred: false, pin: false }
1835
+
1836
+ // Upload with envelope signature
1837
+ const result = await this.bee.uploadChunk(
1838
+ envelope,
1839
+ chunk.data,
1840
+ uploadOptions,
1841
+ requestOptions,
1842
+ )
1843
+
1844
+ // Save stamper state after successful upload
1845
+ await this.saveStamperState()
1846
+
1847
+ return result
1848
+ })
1849
+
1850
+ if (event.source) {
1851
+ ;(event.source as WindowProxy).postMessage(
1852
+ {
1853
+ type: "uploadChunkResponse",
1854
+ requestId,
1855
+ reference: uploadResult.reference.toHex(),
1856
+ } satisfies IframeToParentMessage,
1857
+ { targetOrigin: event.origin },
1858
+ )
1859
+ }
1860
+ } catch (error) {
1861
+ this.sendErrorToParent(
1862
+ event,
1863
+ requestId,
1864
+ error instanceof Error ? error.message : "Upload failed",
1865
+ )
1866
+ }
1867
+ }
1868
+
1869
+ private async handleDownloadChunk(
1870
+ message: DownloadChunkMessage,
1871
+ event: MessageEvent,
1872
+ ): Promise<void> {
1873
+ const { requestId, reference, options, requestOptions } = message
1874
+
1875
+ if (!this.authenticated || !this.appSecret) {
1876
+ throw new Error("Not authenticated. Please login first.")
1877
+ }
1878
+
1879
+ try {
1880
+ // Download chunk using bee-js (returns Uint8Array directly)
1881
+ const data = await this.bee.downloadChunk(
1882
+ reference,
1883
+ options,
1884
+ requestOptions,
1885
+ )
1886
+
1887
+ if (event.source) {
1888
+ ;(event.source as WindowProxy).postMessage(
1889
+ {
1890
+ type: "downloadChunkResponse",
1891
+ requestId,
1892
+ data: data as Uint8Array,
1893
+ } satisfies IframeToParentMessage,
1894
+ { targetOrigin: event.origin },
1895
+ )
1896
+ }
1897
+ } catch (error) {
1898
+ this.sendErrorToParent(
1899
+ event,
1900
+ requestId,
1901
+ error instanceof Error ? error.message : "Download failed",
1902
+ )
1903
+ }
1904
+ }
1905
+
1906
+ private handleGsocMine(message: GsocMineMessage, event: MessageEvent): void {
1907
+ const { requestId, targetOverlay, identifier, proximity } = message
1908
+
1909
+ try {
1910
+ const signer = this.bee.gsocMine(targetOverlay, identifier, proximity)
1911
+
1912
+ if (event.source) {
1913
+ ;(event.source as WindowProxy).postMessage(
1914
+ {
1915
+ type: "gsocMineResponse",
1916
+ requestId,
1917
+ signer: signer.toHex(),
1918
+ } satisfies IframeToParentMessage,
1919
+ { targetOrigin: event.origin },
1920
+ )
1921
+ }
1922
+ } catch (error) {
1923
+ this.sendErrorToParent(
1924
+ event,
1925
+ requestId,
1926
+ error instanceof Error ? error.message : "GSOC mine failed",
1927
+ )
1928
+ }
1929
+ }
1930
+
1931
+ private async handleGsocSend(
1932
+ message: GsocSendMessage,
1933
+ event: MessageEvent,
1934
+ ): Promise<void> {
1935
+ const { requestId, signer, identifier, data, options } = message
1936
+
1937
+ try {
1938
+ if (!this.authenticated || !this.appSecret) {
1939
+ throw new Error("Not authenticated. Please login first.")
1940
+ }
1941
+
1942
+ this.ensureCanUpload()
1943
+
1944
+ if (!this.postageBatchId || !this.stamper) {
1945
+ throw new Error(
1946
+ "Postage batch ID and stamper required. Please login first.",
1947
+ )
1948
+ }
1949
+
1950
+ const signerKey = new PrivateKey(signer)
1951
+ const id = new Identifier(identifier)
1952
+
1953
+ // Serialize write through Web Locks API to prevent concurrent uploads
1954
+ // Use client-side SOC upload (same as handleSocRawUpload) to work with gateways
1955
+ const result = await this.withWriteLock(async () => {
1956
+ const uploadResult = await uploadSOC(
1957
+ this.bee,
1958
+ this.stamper!,
1959
+ signerKey,
1960
+ id,
1961
+ data,
1962
+ options,
1963
+ )
1964
+
1965
+ await this.saveStamperState()
1966
+
1967
+ return uploadResult
1968
+ })
1969
+
1970
+ if (event.source) {
1971
+ ;(event.source as WindowProxy).postMessage(
1972
+ {
1973
+ type: "gsocSendResponse",
1974
+ requestId,
1975
+ reference: uint8ArrayToHex(result.socAddress),
1976
+ tagUid: result.tagUid,
1977
+ } satisfies IframeToParentMessage,
1978
+ { targetOrigin: event.origin },
1979
+ )
1980
+ }
1981
+ } catch (error) {
1982
+ this.sendErrorToParent(
1983
+ event,
1984
+ requestId,
1985
+ error instanceof Error ? error.message : "GSOC send failed",
1986
+ )
1987
+ }
1988
+ }
1989
+
1990
+ // ============================================================================
1991
+ // SOC (Single Owner Chunk) Handlers
1992
+ // ============================================================================
1993
+
1994
+ private async handleSocUpload(
1995
+ message: SocUploadMessage,
1996
+ event: MessageEvent,
1997
+ ): Promise<void> {
1998
+ const { requestId, identifier, data, signer, options } = message
1999
+
2000
+ try {
2001
+ if (!this.authenticated || !this.appSecret) {
2002
+ throw new Error("Not authenticated. Please login first.")
2003
+ }
2004
+
2005
+ this.ensureCanUpload()
2006
+
2007
+ if (!this.postageBatchId || !this.stamper) {
2008
+ throw new Error(
2009
+ "Postage batch ID and stamper required. Please login first.",
2010
+ )
2011
+ }
2012
+
2013
+ const signerKey = signer ?? this.appSecret
2014
+ const signerKeyObj = new PrivateKey(signerKey)
2015
+ const id = new Identifier(identifier)
2016
+
2017
+ // Serialize write through Web Locks API to prevent concurrent uploads
2018
+ const result = await this.withWriteLock(async () => {
2019
+ const uploadResult = await uploadEncryptedSOC(
2020
+ this.bee,
2021
+ this.stamper!,
2022
+ signerKeyObj,
2023
+ id,
2024
+ data,
2025
+ undefined,
2026
+ options,
2027
+ )
2028
+
2029
+ await this.saveStamperState()
2030
+
2031
+ return uploadResult
2032
+ })
2033
+
2034
+ if (event.source) {
2035
+ ;(event.source as WindowProxy).postMessage(
2036
+ {
2037
+ type: "socUploadResponse",
2038
+ requestId,
2039
+ reference: uint8ArrayToHex(result.socAddress),
2040
+ tagUid: result.tagUid,
2041
+ encryptionKey: uint8ArrayToHex(result.encryptionKey),
2042
+ owner: signerKeyObj.publicKey().address().toHex(),
2043
+ } satisfies IframeToParentMessage,
2044
+ { targetOrigin: event.origin },
2045
+ )
2046
+ }
2047
+ } catch (error) {
2048
+ this.sendErrorToParent(
2049
+ event,
2050
+ requestId,
2051
+ error instanceof Error ? error.message : "SOC upload failed",
2052
+ )
2053
+ }
2054
+ }
2055
+
2056
+ private async handleSocRawUpload(
2057
+ message: SocRawUploadMessage,
2058
+ event: MessageEvent,
2059
+ ): Promise<void> {
2060
+ const { requestId, identifier, data, signer, options } = message
2061
+
2062
+ try {
2063
+ if (!this.authenticated || !this.appSecret) {
2064
+ throw new Error("Not authenticated. Please login first.")
2065
+ }
2066
+
2067
+ this.ensureCanUpload()
2068
+
2069
+ if (!this.postageBatchId || !this.stamper) {
2070
+ throw new Error(
2071
+ "Postage batch ID and stamper required. Please login first.",
2072
+ )
2073
+ }
2074
+
2075
+ const signerKey = signer ?? this.appSecret
2076
+ const signerKeyObj = new PrivateKey(signerKey)
2077
+ const id = new Identifier(identifier)
2078
+
2079
+ // Serialize write through Web Locks API to prevent concurrent uploads
2080
+ const result = await this.withWriteLock(async () => {
2081
+ const uploadResult = await uploadSOC(
2082
+ this.bee,
2083
+ this.stamper!,
2084
+ signerKeyObj,
2085
+ id,
2086
+ data,
2087
+ options,
2088
+ )
2089
+
2090
+ await this.saveStamperState()
2091
+
2092
+ return uploadResult
2093
+ })
2094
+
2095
+ if (event.source) {
2096
+ ;(event.source as WindowProxy).postMessage(
2097
+ {
2098
+ type: "socRawUploadResponse",
2099
+ requestId,
2100
+ reference: uint8ArrayToHex(result.socAddress),
2101
+ tagUid: result.tagUid,
2102
+ encryptionKey: undefined,
2103
+ owner: signerKeyObj.publicKey().address().toHex(),
2104
+ } satisfies IframeToParentMessage,
2105
+ { targetOrigin: event.origin },
2106
+ )
2107
+ }
2108
+ } catch (error) {
2109
+ this.sendErrorToParent(
2110
+ event,
2111
+ requestId,
2112
+ error instanceof Error ? error.message : "SOC raw upload failed",
2113
+ )
2114
+ }
2115
+ }
2116
+
2117
+ private async handleSocDownload(
2118
+ message: SocDownloadMessage,
2119
+ event: MessageEvent,
2120
+ ): Promise<void> {
2121
+ const { requestId, owner, identifier, encryptionKey, requestOptions } =
2122
+ message
2123
+
2124
+ try {
2125
+ let resolvedOwner = owner
2126
+ if (!resolvedOwner) {
2127
+ if (!this.appSecret) {
2128
+ throw new Error("Not authenticated. Please login first.")
2129
+ }
2130
+ resolvedOwner = new PrivateKey(this.appSecret)
2131
+ .publicKey()
2132
+ .address()
2133
+ .toHex()
2134
+ }
2135
+
2136
+ const soc = await downloadEncryptedSOC(
2137
+ this.bee,
2138
+ resolvedOwner,
2139
+ identifier,
2140
+ encryptionKey,
2141
+ requestOptions,
2142
+ )
2143
+
2144
+ if (event.source) {
2145
+ ;(event.source as WindowProxy).postMessage(
2146
+ {
2147
+ type: "socDownloadResponse",
2148
+ requestId,
2149
+ data: soc.data,
2150
+ identifier: soc.identifier,
2151
+ signature: soc.signature,
2152
+ span: soc.span,
2153
+ payload: soc.payload,
2154
+ address: soc.address,
2155
+ owner: soc.owner,
2156
+ } satisfies IframeToParentMessage,
2157
+ { targetOrigin: event.origin },
2158
+ )
2159
+ }
2160
+ } catch (error) {
2161
+ this.sendErrorToParent(
2162
+ event,
2163
+ requestId,
2164
+ error instanceof Error ? error.message : "SOC download failed",
2165
+ )
2166
+ }
2167
+ }
2168
+
2169
+ private async handleSocRawDownload(
2170
+ message: SocRawDownloadMessage,
2171
+ event: MessageEvent,
2172
+ ): Promise<void> {
2173
+ const { requestId, owner, identifier, encryptionKey, requestOptions } =
2174
+ message
2175
+
2176
+ try {
2177
+ let resolvedOwner = owner
2178
+ if (!resolvedOwner) {
2179
+ if (!this.appSecret) {
2180
+ throw new Error("Not authenticated. Please login first.")
2181
+ }
2182
+ resolvedOwner = new PrivateKey(this.appSecret)
2183
+ .publicKey()
2184
+ .address()
2185
+ .toHex()
2186
+ }
2187
+
2188
+ const soc = encryptionKey
2189
+ ? await downloadEncryptedSOC(
2190
+ this.bee,
2191
+ resolvedOwner,
2192
+ identifier,
2193
+ encryptionKey,
2194
+ requestOptions,
2195
+ )
2196
+ : await downloadSOC(this.bee, resolvedOwner, identifier, requestOptions)
2197
+
2198
+ if (event.source) {
2199
+ ;(event.source as WindowProxy).postMessage(
2200
+ {
2201
+ type: "socRawDownloadResponse",
2202
+ requestId,
2203
+ data: soc.data,
2204
+ identifier: soc.identifier,
2205
+ signature: soc.signature,
2206
+ span: soc.span,
2207
+ payload: soc.payload,
2208
+ address: soc.address,
2209
+ owner: soc.owner,
2210
+ } satisfies IframeToParentMessage,
2211
+ { targetOrigin: event.origin },
2212
+ )
2213
+ }
2214
+ } catch (error) {
2215
+ this.sendErrorToParent(
2216
+ event,
2217
+ requestId,
2218
+ error instanceof Error ? error.message : "SOC raw download failed",
2219
+ )
2220
+ }
2221
+ }
2222
+
2223
+ private async handleSocGetOwner(
2224
+ message: SocGetOwnerMessage,
2225
+ event: MessageEvent,
2226
+ ): Promise<void> {
2227
+ const { requestId } = message
2228
+
2229
+ try {
2230
+ if (!this.appSecret) {
2231
+ throw new Error("Not authenticated. Please login first.")
2232
+ }
2233
+
2234
+ const owner = new PrivateKey(this.appSecret).publicKey().address().toHex()
2235
+
2236
+ if (event.source) {
2237
+ ;(event.source as WindowProxy).postMessage(
2238
+ {
2239
+ type: "socGetOwnerResponse",
2240
+ requestId,
2241
+ owner,
2242
+ } satisfies IframeToParentMessage,
2243
+ { targetOrigin: event.origin },
2244
+ )
2245
+ }
2246
+ } catch (error) {
2247
+ this.sendErrorToParent(
2248
+ event,
2249
+ requestId,
2250
+ error instanceof Error ? error.message : "SOC get owner failed",
2251
+ )
2252
+ }
2253
+ }
2254
+
2255
+ private parseFeedTimestamp(value: string | number): bigint {
2256
+ if (typeof value === "number") {
2257
+ return BigInt(Math.floor(value))
2258
+ }
2259
+ // Validate string is a valid integer representation
2260
+ if (!/^-?\d+$/.test(value)) {
2261
+ throw new Error(
2262
+ `Invalid timestamp format: "${value}" (expected decimal integer)`,
2263
+ )
2264
+ }
2265
+ return BigInt(value)
2266
+ }
2267
+
2268
+ private parseFeedIndex(value: string | number): bigint {
2269
+ if (typeof value === "number") {
2270
+ return BigInt(Math.floor(value))
2271
+ }
2272
+ // Validate string is a valid integer representation
2273
+ if (!/^-?\d+$/.test(value)) {
2274
+ throw new Error(
2275
+ `Invalid index format: "${value}" (expected decimal integer)`,
2276
+ )
2277
+ }
2278
+ return BigInt(value)
2279
+ }
2280
+
2281
+ private makeSequentialFeedIdentifier(
2282
+ topic: Uint8Array,
2283
+ index: bigint,
2284
+ ): Uint8Array {
2285
+ const indexBytes = Binary.numberToUint64(index, "BE")
2286
+ return Binary.keccak256(Binary.concatBytes(topic, indexBytes))
2287
+ }
2288
+
2289
+ private async findLatestSequentialIndex(
2290
+ topic: Uint8Array,
2291
+ owner: EthAddress,
2292
+ requestOptions?: BeeRequestOptions,
2293
+ lookupTimeoutMs?: number,
2294
+ ): Promise<bigint | undefined> {
2295
+ const lookupOptions: BeeRequestOptions = {
2296
+ ...requestOptions,
2297
+ timeout: lookupTimeoutMs ?? SEQUENTIAL_INDEX_LOOKUP_TIMEOUT_MS,
2298
+ }
2299
+ const finder = createAsyncSequentialFinder({
2300
+ bee: this.bee,
2301
+ topic: new Topic(topic),
2302
+ owner,
2303
+ })
2304
+ const result = await finder.findAt(0n, 0n, lookupOptions)
2305
+ return result.current
2306
+ }
2307
+
2308
+ private sequentialNextIndex(index: bigint): bigint {
2309
+ const max = (1n << 64n) - 1n
2310
+ return index === max ? 0n : index + 1n
2311
+ }
2312
+
2313
+ private async handleFeedGetOwner(
2314
+ message: FeedGetOwnerMessage,
2315
+ event: MessageEvent,
2316
+ ): Promise<void> {
2317
+ const { requestId } = message
2318
+
2319
+ try {
2320
+ if (!this.appSecret) {
2321
+ throw new Error("Not authenticated. Please login first.")
2322
+ }
2323
+
2324
+ const owner = new PrivateKey(this.appSecret).publicKey().address().toHex()
2325
+
2326
+ if (event.source) {
2327
+ ;(event.source as WindowProxy).postMessage(
2328
+ {
2329
+ type: "feedGetOwnerResponse",
2330
+ requestId,
2331
+ owner,
2332
+ } satisfies IframeToParentMessage,
2333
+ { targetOrigin: event.origin },
2334
+ )
2335
+ }
2336
+ } catch (error) {
2337
+ this.sendErrorToParent(
2338
+ event,
2339
+ requestId,
2340
+ error instanceof Error ? error.message : "Feed get owner failed",
2341
+ )
2342
+ }
2343
+ }
2344
+
2345
+ private async handleEpochFeedDownloadReference(
2346
+ message: EpochFeedDownloadReferenceMessage,
2347
+ event: MessageEvent,
2348
+ ): Promise<void> {
2349
+ const { requestId, topic, owner, at, after, encryptionKey } = message
2350
+
2351
+ try {
2352
+ let resolvedOwner = owner
2353
+ if (!resolvedOwner) {
2354
+ if (!this.appSecret) {
2355
+ throw new Error("Not authenticated. Please login first.")
2356
+ }
2357
+ resolvedOwner = new PrivateKey(this.appSecret)
2358
+ .publicKey()
2359
+ .address()
2360
+ .toHex()
2361
+ }
2362
+
2363
+ const topicObj = new Topic(hexToUint8Array(topic))
2364
+ const ownerObj = new EthAddress(resolvedOwner)
2365
+ const atValue = this.parseFeedTimestamp(at)
2366
+ const afterValue =
2367
+ after !== undefined ? this.parseFeedTimestamp(after) : 0n
2368
+ const epochKeyBytes = encryptionKey
2369
+ ? hexToUint8Array(encryptionKey)
2370
+ : undefined
2371
+
2372
+ let reference: Uint8Array | undefined
2373
+ if (epochKeyBytes) {
2374
+ const encryptedFinder = createAsyncEpochFinder({
2375
+ bee: this.bee,
2376
+ topic: topicObj,
2377
+ owner: ownerObj,
2378
+ encryptionKey: epochKeyBytes,
2379
+ })
2380
+ reference = await encryptedFinder.findAt(atValue, afterValue)
2381
+ } else {
2382
+ const plainFinder = createAsyncEpochFinder({
2383
+ bee: this.bee,
2384
+ topic: topicObj,
2385
+ owner: ownerObj,
2386
+ })
2387
+ reference = await plainFinder.findAt(atValue, afterValue)
2388
+ }
2389
+
2390
+ if (event.source) {
2391
+ ;(event.source as WindowProxy).postMessage(
2392
+ {
2393
+ type: "epochFeedDownloadReferenceResponse",
2394
+ requestId,
2395
+ reference: reference ? uint8ArrayToHex(reference) : undefined,
2396
+ } satisfies IframeToParentMessage,
2397
+ { targetOrigin: event.origin },
2398
+ )
2399
+ }
2400
+ } catch (error) {
2401
+ this.sendErrorToParent(
2402
+ event,
2403
+ requestId,
2404
+ error instanceof Error
2405
+ ? error.message
2406
+ : "Epoch feed download reference failed",
2407
+ )
2408
+ }
2409
+ }
2410
+
2411
+ private async handleEpochFeedUploadReference(
2412
+ message: EpochFeedUploadReferenceMessage,
2413
+ event: MessageEvent,
2414
+ ): Promise<void> {
2415
+ const { requestId, topic, signer, at, reference, encryptionKey, hints } =
2416
+ message
2417
+
2418
+ try {
2419
+ if (!this.authenticated || !this.appSecret) {
2420
+ throw new Error("Not authenticated. Please login first.")
2421
+ }
2422
+
2423
+ this.ensureCanUpload()
2424
+
2425
+ if (!this.postageBatchId || !this.stamper) {
2426
+ throw new Error(
2427
+ "Postage batch ID and stamper required. Please login first.",
2428
+ )
2429
+ }
2430
+
2431
+ const signerKey = signer ?? this.appSecret
2432
+ const signerKeyObj = new PrivateKey(signerKey)
2433
+ const topicObj = new Topic(hexToUint8Array(topic))
2434
+ const ownerHex = signerKeyObj.publicKey().address().toHex()
2435
+ const ownerAddress = new EthAddress(ownerHex)
2436
+ const updater = createEpochUpdater({
2437
+ bee: this.bee,
2438
+ topic: topicObj,
2439
+ owner: ownerAddress,
2440
+ signer: signerKeyObj,
2441
+ })
2442
+ const atValue = this.parseFeedTimestamp(at)
2443
+ const epochEncryptionKey = encryptionKey
2444
+ ? hexToUint8Array(encryptionKey)
2445
+ : undefined
2446
+
2447
+ // Convert hints from message format to updater format
2448
+ const epochHints = hints?.lastEpoch
2449
+ ? {
2450
+ lastEpoch: {
2451
+ start: BigInt(hints.lastEpoch.start),
2452
+ level: hints.lastEpoch.level,
2453
+ },
2454
+ lastTimestamp: hints.lastTimestamp
2455
+ ? BigInt(hints.lastTimestamp)
2456
+ : undefined,
2457
+ }
2458
+ : undefined
2459
+
2460
+ // Serialize write through Web Locks API to prevent concurrent uploads
2461
+ const updateResult = await this.withWriteLock(async () => {
2462
+ const referenceBytes = hexToUint8Array(reference)
2463
+ const result = await updater.update(
2464
+ atValue,
2465
+ referenceBytes,
2466
+ this.stamper!,
2467
+ epochEncryptionKey,
2468
+ epochHints,
2469
+ )
2470
+
2471
+ const readBackFinder = createAsyncEpochFinder({
2472
+ bee: this.bee,
2473
+ topic: topicObj,
2474
+ owner: ownerAddress,
2475
+ encryptionKey: epochEncryptionKey,
2476
+ })
2477
+ // Upload read-back should verify the exact timestamp write and avoid
2478
+ // broad fallback scans over historical leaves on poisoned networks.
2479
+ await readBackFinder.findAt(atValue, atValue)
2480
+
2481
+ await this.saveStamperState()
2482
+
2483
+ return result
2484
+ })
2485
+
2486
+ if (event.source) {
2487
+ ;(event.source as WindowProxy).postMessage(
2488
+ {
2489
+ type: "epochFeedUploadReferenceResponse",
2490
+ requestId,
2491
+ socAddress: uint8ArrayToHex(updateResult.socAddress),
2492
+ encryptionKey: encryptionKey ? encryptionKey : undefined,
2493
+ epoch: {
2494
+ start: updateResult.epoch.start.toString(),
2495
+ level: updateResult.epoch.level,
2496
+ },
2497
+ timestamp: updateResult.timestamp.toString(),
2498
+ } satisfies IframeToParentMessage,
2499
+ { targetOrigin: event.origin },
2500
+ )
2501
+ }
2502
+ } catch (error) {
2503
+ this.sendErrorToParent(
2504
+ event,
2505
+ requestId,
2506
+ error instanceof Error
2507
+ ? error.message
2508
+ : "Epoch feed upload reference failed",
2509
+ )
2510
+ }
2511
+ }
2512
+
2513
+ private async handleSequentialFeedGetOwner(
2514
+ message: SequentialFeedGetOwnerMessage,
2515
+ event: MessageEvent,
2516
+ ): Promise<void> {
2517
+ const { requestId } = message
2518
+
2519
+ try {
2520
+ if (!this.appSecret) {
2521
+ throw new Error("Not authenticated. Please login first.")
2522
+ }
2523
+
2524
+ const owner = new PrivateKey(this.appSecret).publicKey().address().toHex()
2525
+
2526
+ if (event.source) {
2527
+ ;(event.source as WindowProxy).postMessage(
2528
+ {
2529
+ type: "seqFeedGetOwnerResponse",
2530
+ requestId,
2531
+ owner,
2532
+ } satisfies IframeToParentMessage,
2533
+ { targetOrigin: event.origin },
2534
+ )
2535
+ }
2536
+ } catch (error) {
2537
+ this.sendErrorToParent(
2538
+ event,
2539
+ requestId,
2540
+ error instanceof Error
2541
+ ? error.message
2542
+ : "Sequential feed get owner failed",
2543
+ )
2544
+ }
2545
+ }
2546
+
2547
+ private async resolveSequentialOwner(owner?: string): Promise<string> {
2548
+ if (owner) {
2549
+ return owner
2550
+ }
2551
+ if (!this.appSecret) {
2552
+ throw new Error("Not authenticated. Please login first.")
2553
+ }
2554
+ return new PrivateKey(this.appSecret).publicKey().address().toHex()
2555
+ }
2556
+
2557
+ private parseSequentialPayload(
2558
+ payload: Uint8Array,
2559
+ hasTimestamp: boolean,
2560
+ ): { payload: Uint8Array; timestamp?: number } {
2561
+ if (!hasTimestamp) {
2562
+ return { payload }
2563
+ }
2564
+
2565
+ if (payload.length < 8) {
2566
+ return { payload, timestamp: undefined }
2567
+ }
2568
+
2569
+ const view = new DataView(
2570
+ payload.buffer,
2571
+ payload.byteOffset,
2572
+ payload.byteLength,
2573
+ )
2574
+ const timestamp = Number(view.getBigUint64(0, false))
2575
+ return { payload: payload.slice(8), timestamp }
2576
+ }
2577
+
2578
+ private async resolveSequentialIndex(
2579
+ topicBytes: Uint8Array,
2580
+ ownerAddress: EthAddress,
2581
+ index?: string | number,
2582
+ at?: string | number,
2583
+ hasTimestamp: boolean = true,
2584
+ requestOptions?: BeeRequestOptions,
2585
+ encryptionKey?: string,
2586
+ raw: boolean = false,
2587
+ lookupTimeoutMs?: number,
2588
+ ): Promise<bigint> {
2589
+ if (!raw && !encryptionKey) {
2590
+ throw new Error("Encryption key is required for encrypted feed lookup")
2591
+ }
2592
+ if (index !== undefined) {
2593
+ return this.parseFeedIndex(index)
2594
+ }
2595
+
2596
+ const latest = await this.findLatestSequentialIndex(
2597
+ topicBytes,
2598
+ ownerAddress,
2599
+ requestOptions,
2600
+ lookupTimeoutMs,
2601
+ )
2602
+ if (latest === undefined) {
2603
+ throw new Error("Sequential feed has no updates")
2604
+ }
2605
+
2606
+ if (at === undefined) {
2607
+ return latest
2608
+ }
2609
+
2610
+ if (!hasTimestamp) {
2611
+ throw new Error("Cannot use 'at' without timestamps")
2612
+ }
2613
+
2614
+ const atValue = this.parseFeedTimestamp(at)
2615
+ for (let current = latest; current >= 0n; current--) {
2616
+ const identifierBytes = this.makeSequentialFeedIdentifier(
2617
+ topicBytes,
2618
+ current,
2619
+ )
2620
+ const identifier = new Identifier(identifierBytes)
2621
+ const soc = raw
2622
+ ? encryptionKey
2623
+ ? await downloadEncryptedSOC(
2624
+ this.bee,
2625
+ ownerAddress,
2626
+ identifier,
2627
+ encryptionKey,
2628
+ requestOptions,
2629
+ )
2630
+ : await downloadSOC(
2631
+ this.bee,
2632
+ ownerAddress,
2633
+ identifier,
2634
+ requestOptions,
2635
+ )
2636
+ : await downloadEncryptedSOC(
2637
+ this.bee,
2638
+ ownerAddress,
2639
+ identifier,
2640
+ encryptionKey ?? "",
2641
+ requestOptions,
2642
+ )
2643
+
2644
+ const parsed = this.parseSequentialPayload(soc.payload, true)
2645
+ if (
2646
+ parsed.timestamp !== undefined &&
2647
+ BigInt(parsed.timestamp) <= atValue
2648
+ ) {
2649
+ return current
2650
+ }
2651
+ if (current === 0n) {
2652
+ break
2653
+ }
2654
+ }
2655
+
2656
+ // If no update matches the timestamp, fall back to latest for sequential feeds.
2657
+ return latest
2658
+ }
2659
+
2660
+ private async handleSequentialFeedDownloadPayload(
2661
+ message: SequentialFeedDownloadPayloadMessage,
2662
+ event: MessageEvent,
2663
+ ): Promise<void> {
2664
+ const {
2665
+ requestId,
2666
+ topic,
2667
+ owner,
2668
+ index,
2669
+ at,
2670
+ hasTimestamp,
2671
+ encryptionKey,
2672
+ lookupTimeoutMs,
2673
+ requestOptions,
2674
+ } = message
2675
+
2676
+ try {
2677
+ if (!encryptionKey) {
2678
+ throw new Error("Encryption key is required for downloadPayload")
2679
+ }
2680
+
2681
+ const resolvedOwner = await this.resolveSequentialOwner(owner)
2682
+ const ownerAddress = new EthAddress(resolvedOwner)
2683
+ const topicBytes = hexToUint8Array(topic)
2684
+ const useTimestamp = hasTimestamp !== false
2685
+ const resolvedIndex = await this.resolveSequentialIndex(
2686
+ topicBytes,
2687
+ ownerAddress,
2688
+ index,
2689
+ at,
2690
+ useTimestamp,
2691
+ requestOptions,
2692
+ encryptionKey,
2693
+ false,
2694
+ lookupTimeoutMs,
2695
+ )
2696
+
2697
+ const identifierBytes = this.makeSequentialFeedIdentifier(
2698
+ topicBytes,
2699
+ resolvedIndex,
2700
+ )
2701
+ const identifier = new Identifier(identifierBytes)
2702
+ const soc = await downloadEncryptedSOC(
2703
+ this.bee,
2704
+ ownerAddress,
2705
+ identifier,
2706
+ encryptionKey,
2707
+ requestOptions,
2708
+ )
2709
+
2710
+ const parsed = this.parseSequentialPayload(soc.payload, useTimestamp)
2711
+ const nextIndex = this.sequentialNextIndex(resolvedIndex)
2712
+
2713
+ if (event.source) {
2714
+ ;(event.source as WindowProxy).postMessage(
2715
+ {
2716
+ type: "seqFeedDownloadPayloadResponse",
2717
+ requestId,
2718
+ payload: parsed.payload,
2719
+ timestamp: parsed.timestamp,
2720
+ feedIndex: resolvedIndex.toString(),
2721
+ feedIndexNext: nextIndex.toString(),
2722
+ } satisfies IframeToParentMessage,
2723
+ { targetOrigin: event.origin },
2724
+ )
2725
+ }
2726
+ } catch (error) {
2727
+ this.sendErrorToParent(
2728
+ event,
2729
+ requestId,
2730
+ error instanceof Error
2731
+ ? error.message
2732
+ : "Sequential feed download payload failed",
2733
+ )
2734
+ }
2735
+ }
2736
+
2737
+ private async handleSequentialFeedDownloadRawPayload(
2738
+ message: SequentialFeedDownloadRawPayloadMessage,
2739
+ event: MessageEvent,
2740
+ ): Promise<void> {
2741
+ const {
2742
+ requestId,
2743
+ topic,
2744
+ owner,
2745
+ index,
2746
+ at,
2747
+ hasTimestamp,
2748
+ encryptionKey,
2749
+ lookupTimeoutMs,
2750
+ requestOptions,
2751
+ } = message
2752
+
2753
+ try {
2754
+ const resolvedOwner = await this.resolveSequentialOwner(owner)
2755
+ const ownerAddress = new EthAddress(resolvedOwner)
2756
+ const topicBytes = hexToUint8Array(topic)
2757
+ const useTimestamp = hasTimestamp !== false
2758
+ const resolvedIndex = await this.resolveSequentialIndex(
2759
+ topicBytes,
2760
+ ownerAddress,
2761
+ index,
2762
+ at,
2763
+ useTimestamp,
2764
+ requestOptions,
2765
+ encryptionKey,
2766
+ true,
2767
+ lookupTimeoutMs,
2768
+ )
2769
+
2770
+ const identifierBytes = this.makeSequentialFeedIdentifier(
2771
+ topicBytes,
2772
+ resolvedIndex,
2773
+ )
2774
+ const identifier = new Identifier(identifierBytes)
2775
+ const soc = encryptionKey
2776
+ ? await downloadEncryptedSOC(
2777
+ this.bee,
2778
+ ownerAddress,
2779
+ identifier,
2780
+ encryptionKey,
2781
+ requestOptions,
2782
+ )
2783
+ : await downloadSOC(this.bee, ownerAddress, identifier, requestOptions)
2784
+
2785
+ const parsed = this.parseSequentialPayload(soc.payload, useTimestamp)
2786
+ const nextIndex = this.sequentialNextIndex(resolvedIndex)
2787
+
2788
+ if (event.source) {
2789
+ ;(event.source as WindowProxy).postMessage(
2790
+ {
2791
+ type: "seqFeedDownloadRawPayloadResponse",
2792
+ requestId,
2793
+ payload: parsed.payload,
2794
+ timestamp: parsed.timestamp,
2795
+ feedIndex: resolvedIndex.toString(),
2796
+ feedIndexNext: nextIndex.toString(),
2797
+ } satisfies IframeToParentMessage,
2798
+ { targetOrigin: event.origin },
2799
+ )
2800
+ }
2801
+ } catch (error) {
2802
+ this.sendErrorToParent(
2803
+ event,
2804
+ requestId,
2805
+ error instanceof Error
2806
+ ? error.message
2807
+ : "Sequential feed download raw payload failed",
2808
+ )
2809
+ }
2810
+ }
2811
+
2812
+ private async handleSequentialFeedDownloadReference(
2813
+ message: SequentialFeedDownloadReferenceMessage,
2814
+ event: MessageEvent,
2815
+ ): Promise<void> {
2816
+ const {
2817
+ requestId,
2818
+ topic,
2819
+ owner,
2820
+ index,
2821
+ at,
2822
+ hasTimestamp,
2823
+ encryptionKey,
2824
+ lookupTimeoutMs,
2825
+ requestOptions,
2826
+ } = message
2827
+
2828
+ try {
2829
+ if (!encryptionKey) {
2830
+ throw new Error("Encryption key is required for downloadReference")
2831
+ }
2832
+
2833
+ const resolvedOwner = await this.resolveSequentialOwner(owner)
2834
+ const ownerAddress = new EthAddress(resolvedOwner)
2835
+ const topicBytes = hexToUint8Array(topic)
2836
+ const useTimestamp = hasTimestamp !== false
2837
+ const resolvedIndex = await this.resolveSequentialIndex(
2838
+ topicBytes,
2839
+ ownerAddress,
2840
+ index,
2841
+ at,
2842
+ useTimestamp,
2843
+ requestOptions,
2844
+ encryptionKey,
2845
+ false,
2846
+ lookupTimeoutMs,
2847
+ )
2848
+
2849
+ const identifierBytes = this.makeSequentialFeedIdentifier(
2850
+ topicBytes,
2851
+ resolvedIndex,
2852
+ )
2853
+ const identifier = new Identifier(identifierBytes)
2854
+ const soc = await downloadEncryptedSOC(
2855
+ this.bee,
2856
+ ownerAddress,
2857
+ identifier,
2858
+ encryptionKey,
2859
+ requestOptions,
2860
+ )
2861
+
2862
+ const parsed = this.parseSequentialPayload(soc.payload, useTimestamp)
2863
+ if (parsed.payload.length !== 32 && parsed.payload.length !== 64) {
2864
+ throw new Error(
2865
+ "Sequential feed update does not contain a reference; use downloadPayload",
2866
+ )
2867
+ }
2868
+ const referenceHex = uint8ArrayToHex(parsed.payload)
2869
+ const nextIndex = this.sequentialNextIndex(resolvedIndex)
2870
+
2871
+ if (event.source) {
2872
+ ;(event.source as WindowProxy).postMessage(
2873
+ {
2874
+ type: "seqFeedDownloadReferenceResponse",
2875
+ requestId,
2876
+ reference: referenceHex,
2877
+ feedIndex: resolvedIndex.toString(),
2878
+ feedIndexNext: nextIndex.toString(),
2879
+ } satisfies IframeToParentMessage,
2880
+ { targetOrigin: event.origin },
2881
+ )
2882
+ }
2883
+ } catch (error) {
2884
+ this.sendErrorToParent(
2885
+ event,
2886
+ requestId,
2887
+ error instanceof Error
2888
+ ? error.message
2889
+ : "Sequential feed download reference failed",
2890
+ )
2891
+ }
2892
+ }
2893
+
2894
+ private buildSequentialPayload(
2895
+ data: Uint8Array,
2896
+ hasTimestamp: boolean,
2897
+ at: bigint,
2898
+ ): Uint8Array {
2899
+ if (!hasTimestamp) {
2900
+ return data
2901
+ }
2902
+ const timestamp = new Uint8Array(8)
2903
+ const view = new DataView(timestamp.buffer)
2904
+ view.setBigUint64(0, at, false)
2905
+ return Binary.concatBytes(timestamp, data)
2906
+ }
2907
+
2908
+ private async handleSequentialFeedUploadPayload(
2909
+ message: SequentialFeedUploadPayloadMessage,
2910
+ event: MessageEvent,
2911
+ ): Promise<void> {
2912
+ const {
2913
+ requestId,
2914
+ topic,
2915
+ signer,
2916
+ data,
2917
+ index,
2918
+ at,
2919
+ hasTimestamp,
2920
+ lookupTimeoutMs,
2921
+ options,
2922
+ requestOptions,
2923
+ } = message
2924
+
2925
+ try {
2926
+ if (!this.authenticated || !this.appSecret) {
2927
+ throw new Error("Not authenticated. Please login first.")
2928
+ }
2929
+ this.ensureCanUpload()
2930
+ if (!this.postageBatchId || !this.stamper) {
2931
+ throw new Error(
2932
+ "Postage batch ID and stamper required. Please login first.",
2933
+ )
2934
+ }
2935
+
2936
+ const signerKey = signer ?? this.appSecret
2937
+ const signerKeyObj = new PrivateKey(signerKey)
2938
+ const ownerAddress = signerKeyObj.publicKey().address()
2939
+ const topicBytes = hexToUint8Array(topic)
2940
+
2941
+ const useTimestamp = hasTimestamp !== false
2942
+ const atValue =
2943
+ at !== undefined
2944
+ ? this.parseFeedTimestamp(at)
2945
+ : BigInt(Math.floor(Date.now() / 1000))
2946
+ let resolvedIndex: bigint
2947
+ if (index !== undefined) {
2948
+ resolvedIndex = this.parseFeedIndex(index)
2949
+ } else {
2950
+ const latest = await this.findLatestSequentialIndex(
2951
+ topicBytes,
2952
+ ownerAddress,
2953
+ requestOptions,
2954
+ lookupTimeoutMs,
2955
+ )
2956
+ resolvedIndex =
2957
+ latest === undefined ? 0n : this.sequentialNextIndex(latest)
2958
+ }
2959
+
2960
+ const payload = this.buildSequentialPayload(data, useTimestamp, atValue)
2961
+ if (payload.length < 1 || payload.length > 4096) {
2962
+ throw new Error(
2963
+ `Invalid payload length: ${payload.length} (expected 1-4096)`,
2964
+ )
2965
+ }
2966
+
2967
+ // Serialize write through Web Locks API to prevent concurrent uploads
2968
+ const result = await this.withWriteLock(async () => {
2969
+ const identifierBytes = this.makeSequentialFeedIdentifier(
2970
+ topicBytes,
2971
+ resolvedIndex,
2972
+ )
2973
+ const identifier = new Identifier(identifierBytes)
2974
+ const uploadResult = await uploadEncryptedSOC(
2975
+ this.bee,
2976
+ this.stamper!,
2977
+ signerKeyObj,
2978
+ identifier,
2979
+ payload,
2980
+ undefined,
2981
+ options,
2982
+ )
2983
+
2984
+ await this.saveStamperState()
2985
+
2986
+ return uploadResult
2987
+ })
2988
+
2989
+ if (event.source) {
2990
+ ;(event.source as WindowProxy).postMessage(
2991
+ {
2992
+ type: "seqFeedUploadPayloadResponse",
2993
+ requestId,
2994
+ reference: uint8ArrayToHex(result.socAddress),
2995
+ feedIndex: resolvedIndex.toString(),
2996
+ owner: ownerAddress.toHex(),
2997
+ encryptionKey: uint8ArrayToHex(result.encryptionKey),
2998
+ tagUid: result.tagUid,
2999
+ } satisfies IframeToParentMessage,
3000
+ { targetOrigin: event.origin },
3001
+ )
3002
+ }
3003
+ } catch (error) {
3004
+ this.sendErrorToParent(
3005
+ event,
3006
+ requestId,
3007
+ error instanceof Error
3008
+ ? error.message
3009
+ : "Sequential feed upload payload failed",
3010
+ )
3011
+ }
3012
+ }
3013
+
3014
+ private async handleSequentialFeedUploadRawPayload(
3015
+ message: SequentialFeedUploadRawPayloadMessage,
3016
+ event: MessageEvent,
3017
+ ): Promise<void> {
3018
+ const {
3019
+ requestId,
3020
+ topic,
3021
+ signer,
3022
+ data,
3023
+ index,
3024
+ at,
3025
+ hasTimestamp,
3026
+ encryptionKey,
3027
+ lookupTimeoutMs,
3028
+ options,
3029
+ requestOptions,
3030
+ } = message
3031
+
3032
+ try {
3033
+ if (!this.authenticated || !this.appSecret) {
3034
+ throw new Error("Not authenticated. Please login first.")
3035
+ }
3036
+ this.ensureCanUpload()
3037
+ if (!this.postageBatchId || !this.stamper) {
3038
+ throw new Error(
3039
+ "Postage batch ID and stamper required. Please login first.",
3040
+ )
3041
+ }
3042
+
3043
+ const signerKey = signer ?? this.appSecret
3044
+ const signerKeyObj = new PrivateKey(signerKey)
3045
+ const ownerAddress = signerKeyObj.publicKey().address()
3046
+ const topicBytes = hexToUint8Array(topic)
3047
+
3048
+ const useTimestamp = hasTimestamp !== false
3049
+ const atValue =
3050
+ at !== undefined
3051
+ ? this.parseFeedTimestamp(at)
3052
+ : BigInt(Math.floor(Date.now() / 1000))
3053
+ let resolvedIndex: bigint
3054
+ if (index !== undefined) {
3055
+ resolvedIndex = this.parseFeedIndex(index)
3056
+ } else {
3057
+ const latest = await this.findLatestSequentialIndex(
3058
+ topicBytes,
3059
+ ownerAddress,
3060
+ requestOptions,
3061
+ lookupTimeoutMs,
3062
+ )
3063
+ resolvedIndex =
3064
+ latest === undefined ? 0n : this.sequentialNextIndex(latest)
3065
+ }
3066
+
3067
+ const payload = this.buildSequentialPayload(data, useTimestamp, atValue)
3068
+ if (payload.length < 1 || payload.length > 4096) {
3069
+ throw new Error(
3070
+ `Invalid payload length: ${payload.length} (expected 1-4096)`,
3071
+ )
3072
+ }
3073
+
3074
+ const identifierBytes = this.makeSequentialFeedIdentifier(
3075
+ topicBytes,
3076
+ resolvedIndex,
3077
+ )
3078
+ const identifier = new Identifier(identifierBytes)
3079
+
3080
+ // Debug: log the values used for SOC address computation
3081
+
3082
+ // DEBUG: Log payload details for /bzz/ compatibility analysis
3083
+
3084
+ // Serialize write through Web Locks API to prevent concurrent uploads
3085
+ // Upload SOC - use encryption if key provided, otherwise use /soc endpoint
3086
+ // The /soc endpoint is needed for non-encrypted uploads because:
3087
+ // - Small SOCs (< 4104 bytes) are misidentified as CAC by /chunks endpoint
3088
+ // - /soc endpoint explicitly handles SOC without size-based detection
3089
+ // - Preserves v1 format (48-byte CAC) required for /bzz/ compatibility
3090
+ const result = await this.withWriteLock(async () => {
3091
+ let uploadResult
3092
+ if (encryptionKey) {
3093
+ uploadResult = await uploadEncryptedSOC(
3094
+ this.bee,
3095
+ this.stamper!,
3096
+ signerKeyObj,
3097
+ identifier,
3098
+ payload,
3099
+ hexToUint8Array(encryptionKey),
3100
+ options,
3101
+ )
3102
+ } else {
3103
+ // Use /soc endpoint for non-encrypted uploads (v1 format for /bzz/ compat)
3104
+ uploadResult = await uploadSOCViaSocEndpoint(
3105
+ this.bee,
3106
+ this.stamper!,
3107
+ signerKeyObj,
3108
+ identifier,
3109
+ payload,
3110
+ options,
3111
+ )
3112
+ }
3113
+
3114
+ await this.saveStamperState()
3115
+
3116
+ return uploadResult
3117
+ })
3118
+
3119
+ if (event.source) {
3120
+ ;(event.source as WindowProxy).postMessage(
3121
+ {
3122
+ type: "seqFeedUploadRawPayloadResponse",
3123
+ requestId,
3124
+ reference: uint8ArrayToHex(result.socAddress),
3125
+ feedIndex: resolvedIndex.toString(),
3126
+ owner: ownerAddress.toHex(),
3127
+ encryptionKey: encryptionKey ? encryptionKey : undefined,
3128
+ tagUid: result.tagUid,
3129
+ } satisfies IframeToParentMessage,
3130
+ { targetOrigin: event.origin },
3131
+ )
3132
+ }
3133
+ } catch (error) {
3134
+ this.sendErrorToParent(
3135
+ event,
3136
+ requestId,
3137
+ error instanceof Error
3138
+ ? error.message
3139
+ : "Sequential feed upload raw payload failed",
3140
+ )
3141
+ }
3142
+ }
3143
+
3144
+ private async handleSequentialFeedUploadReference(
3145
+ message: SequentialFeedUploadReferenceMessage,
3146
+ event: MessageEvent,
3147
+ ): Promise<void> {
3148
+ const {
3149
+ requestId,
3150
+ topic,
3151
+ signer,
3152
+ reference,
3153
+ index,
3154
+ at,
3155
+ hasTimestamp,
3156
+ lookupTimeoutMs,
3157
+ options,
3158
+ requestOptions,
3159
+ } = message
3160
+
3161
+ try {
3162
+ if (!this.authenticated || !this.appSecret) {
3163
+ throw new Error("Not authenticated. Please login first.")
3164
+ }
3165
+ this.ensureCanUpload()
3166
+ if (!this.postageBatchId || !this.stamper) {
3167
+ throw new Error(
3168
+ "Postage batch ID and stamper required. Please login first.",
3169
+ )
3170
+ }
3171
+
3172
+ const signerKey = signer ?? this.appSecret
3173
+ const signerKeyObj = new PrivateKey(signerKey)
3174
+ const ownerAddress = signerKeyObj.publicKey().address()
3175
+ const topicBytes = hexToUint8Array(topic)
3176
+
3177
+ const useTimestamp = hasTimestamp !== false
3178
+ const atValue =
3179
+ at !== undefined
3180
+ ? this.parseFeedTimestamp(at)
3181
+ : BigInt(Math.floor(Date.now() / 1000))
3182
+ let resolvedIndex: bigint
3183
+ if (index !== undefined) {
3184
+ resolvedIndex = this.parseFeedIndex(index)
3185
+ } else {
3186
+ const latest = await this.findLatestSequentialIndex(
3187
+ topicBytes,
3188
+ ownerAddress,
3189
+ requestOptions,
3190
+ lookupTimeoutMs,
3191
+ )
3192
+ resolvedIndex =
3193
+ latest === undefined ? 0n : this.sequentialNextIndex(latest)
3194
+ }
3195
+
3196
+ const referenceBytes = hexToUint8Array(reference)
3197
+ const payload = this.buildSequentialPayload(
3198
+ referenceBytes,
3199
+ useTimestamp,
3200
+ atValue,
3201
+ )
3202
+ if (payload.length < 1 || payload.length > 4096) {
3203
+ throw new Error(
3204
+ `Invalid payload length: ${payload.length} (expected 1-4096)`,
3205
+ )
3206
+ }
3207
+
3208
+ // Serialize write through Web Locks API to prevent concurrent uploads
3209
+ const { result, encryptionKeyResult } = await this.withWriteLock(
3210
+ async () => {
3211
+ const identifierBytes = this.makeSequentialFeedIdentifier(
3212
+ topicBytes,
3213
+ resolvedIndex,
3214
+ )
3215
+ const identifier = new Identifier(identifierBytes)
3216
+
3217
+ // uploadReference always uses encryption
3218
+ const encResult = await uploadEncryptedSOC(
3219
+ this.bee,
3220
+ this.stamper!,
3221
+ signerKeyObj,
3222
+ identifier,
3223
+ payload,
3224
+ undefined,
3225
+ options,
3226
+ )
3227
+
3228
+ await this.saveStamperState()
3229
+
3230
+ return {
3231
+ result: encResult,
3232
+ encryptionKeyResult: uint8ArrayToHex(encResult.encryptionKey),
3233
+ }
3234
+ },
3235
+ )
3236
+
3237
+ if (event.source) {
3238
+ ;(event.source as WindowProxy).postMessage(
3239
+ {
3240
+ type: "seqFeedUploadReferenceResponse",
3241
+ requestId,
3242
+ reference: uint8ArrayToHex(result.socAddress),
3243
+ feedIndex: resolvedIndex.toString(),
3244
+ owner: ownerAddress.toHex(),
3245
+ encryptionKey: encryptionKeyResult,
3246
+ tagUid: result.tagUid,
3247
+ } satisfies IframeToParentMessage,
3248
+ { targetOrigin: event.origin },
3249
+ )
3250
+ }
3251
+ } catch (error) {
3252
+ this.sendErrorToParent(
3253
+ event,
3254
+ requestId,
3255
+ error instanceof Error
3256
+ ? error.message
3257
+ : "Sequential feed upload reference failed",
3258
+ )
3259
+ }
3260
+ }
3261
+
3262
+ // ============================================================================
3263
+ // ACT (Access Control Tries) Handlers
3264
+ // ============================================================================
3265
+
3266
+ private async handleActUploadData(
3267
+ message: ActUploadDataMessage,
3268
+ event: MessageEvent,
3269
+ ): Promise<void> {
3270
+ const {
3271
+ requestId,
3272
+ data,
3273
+ grantees,
3274
+ options,
3275
+ requestOptions,
3276
+ enableProgress,
3277
+ } = message
3278
+
3279
+ try {
3280
+ if (!this.authenticated || !this.appSecret) {
3281
+ throw new Error("Not authenticated. Please login first.")
3282
+ }
3283
+
3284
+ this.ensureCanUpload()
3285
+
3286
+ if (!this.signerKey || !this.postageBatchId) {
3287
+ throw new Error(
3288
+ "Signer key and postage batch ID required. Please login first.",
3289
+ )
3290
+ }
3291
+
3292
+ if (!this.stamper) {
3293
+ throw new Error("Stamper not initialized. Please login first.")
3294
+ }
3295
+
3296
+ // Prepare upload context
3297
+ const context: UploadContext = {
3298
+ bee: this.bee,
3299
+ stamper: this.stamper,
3300
+ }
3301
+
3302
+ // Parse grantee public keys from compressed hex
3303
+ const granteePublicKeys = grantees.map((hex) =>
3304
+ parseCompressedPublicKey(hex),
3305
+ )
3306
+
3307
+ // Progress callback (if enabled)
3308
+ const onProgress = enableProgress
3309
+ ? (progress: UploadProgress) => {
3310
+ if (event.source) {
3311
+ ;(event.source as WindowProxy).postMessage(
3312
+ {
3313
+ type: "uploadProgress",
3314
+ requestId,
3315
+ total: progress.total,
3316
+ processed: progress.processed,
3317
+ } satisfies IframeToParentMessage,
3318
+ { targetOrigin: event.origin },
3319
+ )
3320
+ }
3321
+ }
3322
+ : undefined
3323
+
3324
+ // Use appSecret as publisher private key (user's identity key for this app)
3325
+ const publisherPrivateKey = hexToUint8Array(this.appSecret)
3326
+
3327
+ // Serialize write through Web Locks API to prevent concurrent uploads
3328
+ const { actResult, contentUpload } = await this.withWriteLock(
3329
+ async () => {
3330
+ // Step 1: Upload raw content data - ENCRYPTED (64-byte reference)
3331
+ const contentUploadResult = await uploadEncryptedDataWithSigning(
3332
+ context,
3333
+ data,
3334
+ undefined, // generate random encryption key
3335
+ options,
3336
+ onProgress,
3337
+ requestOptions,
3338
+ )
3339
+
3340
+ // Step 2: Create Mantaray manifest wrapping the content
3341
+ // Content reference is now 64 bytes (encrypted reference: address + encryption key)
3342
+ // This is needed because Bee's /bzz/ endpoint expects a default (Mantaray) manifest
3343
+ const manifest = new MantarayNode()
3344
+ const contentReferenceBytes = hexToUint8Array(
3345
+ contentUploadResult.reference,
3346
+ ) // 64 bytes
3347
+ manifest.addFork(DEFAULT_ACT_FILENAME, contentReferenceBytes, {
3348
+ "Content-Type": DEFAULT_ACT_CONTENT_TYPE,
3349
+ Filename: DEFAULT_ACT_FILENAME,
3350
+ })
3351
+ manifest.addFork("/", NULL_ADDRESS, {
3352
+ "website-index-document": DEFAULT_ACT_FILENAME,
3353
+ })
3354
+
3355
+ // Create a tag for the manifest uploads (required for dev mode)
3356
+ let manifestTag = options?.tag
3357
+ if (!manifestTag) {
3358
+ const tagResponse = await context.bee.createTag()
3359
+ manifestTag = tagResponse.uid
3360
+ }
3361
+
3362
+ const beeCompatible = options?.beeCompatible === true
3363
+
3364
+ // Step 3: Upload the Mantaray manifest
3365
+ const manifestResult = beeCompatible
3366
+ ? await saveMantarayTreeRecursively(
3367
+ manifest,
3368
+ async (chunkData, isRoot) => {
3369
+ const chunk = makeContentAddressedChunk(chunkData)
3370
+ const envelope = context.stamper.stamp({
3371
+ hash: () => chunk.address.toUint8Array(),
3372
+ build: () => chunk.data,
3373
+ span: 0n,
3374
+ writer: undefined as any,
3375
+ })
3376
+ await context.bee.uploadChunk(
3377
+ envelope,
3378
+ chunk.data,
3379
+ { ...options, tag: manifestTag, deferred: false },
3380
+ requestOptions,
3381
+ )
3382
+ return {
3383
+ reference: chunk.address.toHex(),
3384
+ tagUid: isRoot ? manifestTag : undefined,
3385
+ }
3386
+ },
3387
+ )
3388
+ : await saveMantarayTreeRecursivelyEncrypted(
3389
+ manifest,
3390
+ async (encryptedData, address, isRoot) => {
3391
+ const envelope = context.stamper.stamp({
3392
+ hash: () => address,
3393
+ build: () => encryptedData,
3394
+ span: 0n,
3395
+ writer: undefined as any,
3396
+ })
3397
+ await context.bee.uploadChunk(
3398
+ envelope,
3399
+ encryptedData,
3400
+ { ...options, tag: manifestTag, deferred: false },
3401
+ requestOptions,
3402
+ )
3403
+ return {
3404
+ tagUid: isRoot ? manifestTag : undefined,
3405
+ }
3406
+ },
3407
+ )
3408
+
3409
+ // Step 4: Use manifest reference for ACT encryption
3410
+ const manifestReferenceBytes = hexToUint8Array(
3411
+ manifestResult.rootReference,
3412
+ )
3413
+
3414
+ // Create ACT for the manifest (which points to the content)
3415
+ const actResultValue = await createActForContent(
3416
+ context,
3417
+ manifestReferenceBytes,
3418
+ publisherPrivateKey,
3419
+ granteePublicKeys,
3420
+ options,
3421
+ requestOptions,
3422
+ )
3423
+
3424
+ // Save stamper state after successful upload
3425
+ await this.saveStamperState()
3426
+
3427
+ return {
3428
+ actResult: actResultValue,
3429
+ contentUpload: contentUploadResult,
3430
+ }
3431
+ },
3432
+ )
3433
+
3434
+ // Send final response
3435
+ if (event.source) {
3436
+ ;(event.source as WindowProxy).postMessage(
3437
+ {
3438
+ type: "actUploadDataResponse",
3439
+ requestId,
3440
+ encryptedReference: actResult.encryptedReference,
3441
+ historyReference: actResult.historyReference,
3442
+ granteeListReference: actResult.granteeListReference,
3443
+ publisherPubKey: actResult.publisherPubKey,
3444
+ actReference: actResult.actReference,
3445
+ tagUid: contentUpload.tagUid,
3446
+ } satisfies IframeToParentMessage,
3447
+ { targetOrigin: event.origin },
3448
+ )
3449
+ }
3450
+ } catch (error) {
3451
+ this.sendErrorToParent(
3452
+ event,
3453
+ requestId,
3454
+ error instanceof Error ? error.message : "ACT upload failed",
3455
+ )
3456
+ }
3457
+ }
3458
+
3459
+ private async handleActDownloadData(
3460
+ message: ActDownloadDataMessage,
3461
+ event: MessageEvent,
3462
+ ): Promise<void> {
3463
+ const {
3464
+ requestId,
3465
+ encryptedReference,
3466
+ historyReference,
3467
+ publisherPubKey,
3468
+ timestamp,
3469
+ requestOptions,
3470
+ } = message
3471
+
3472
+ try {
3473
+ if (!this.authenticated || !this.appSecret) {
3474
+ throw new Error("Not authenticated. Please login first.")
3475
+ }
3476
+
3477
+ // appSecret is already checked by authenticated check above
3478
+ // Use appSecret as reader private key (user's identity key for this app)
3479
+ const readerPrivateKey = hexToUint8Array(this.appSecret)
3480
+
3481
+ // Decrypt the ACT reference to get the content reference
3482
+ const contentReference = await decryptActReference(
3483
+ this.bee,
3484
+ encryptedReference,
3485
+ historyReference,
3486
+ publisherPubKey,
3487
+ readerPrivateKey,
3488
+ timestamp,
3489
+ requestOptions,
3490
+ )
3491
+
3492
+ // Step 1: Download and unmarshal the Mantaray manifest (chunk API only)
3493
+ const manifest = await loadMantarayTreeWithChunkAPI(
3494
+ this.bee,
3495
+ contentReference,
3496
+ requestOptions,
3497
+ )
3498
+
3499
+ // Step 2: Get the index document path from manifest metadata
3500
+ const { indexDocument } = manifest.getDocsMetadata()
3501
+ if (!indexDocument) {
3502
+ throw new Error("Manifest does not contain an index document reference")
3503
+ }
3504
+
3505
+ // Step 3: Find the node at the index document path
3506
+ const contentNode = manifest.find(indexDocument)
3507
+ if (!contentNode) {
3508
+ throw new Error(`Content node "${indexDocument}" not found in manifest`)
3509
+ }
3510
+
3511
+ if (!contentNode.targetAddress) {
3512
+ throw new Error(
3513
+ `Content node "${indexDocument}" does not have a target address`,
3514
+ )
3515
+ }
3516
+
3517
+ const actualContentRef = uint8ArrayToHex(contentNode.targetAddress)
3518
+
3519
+ // Step 4: Download the actual content
3520
+ const data = await downloadDataWithChunkAPI(
3521
+ this.bee,
3522
+ actualContentRef,
3523
+ undefined,
3524
+ undefined,
3525
+ requestOptions,
3526
+ )
3527
+
3528
+ if (event.source) {
3529
+ ;(event.source as WindowProxy).postMessage(
3530
+ {
3531
+ type: "actDownloadDataResponse",
3532
+ requestId,
3533
+ data: data as Uint8Array,
3534
+ } satisfies IframeToParentMessage,
3535
+ { targetOrigin: event.origin },
3536
+ )
3537
+ }
3538
+ } catch (error) {
3539
+ this.sendErrorToParent(
3540
+ event,
3541
+ requestId,
3542
+ error instanceof Error ? error.message : "ACT download failed",
3543
+ )
3544
+ }
3545
+ }
3546
+
3547
+ private async handleActAddGrantees(
3548
+ message: ActAddGranteesMessage,
3549
+ event: MessageEvent,
3550
+ ): Promise<void> {
3551
+ const { requestId, historyReference, grantees, requestOptions } = message
3552
+
3553
+ try {
3554
+ if (!this.authenticated || !this.appSecret) {
3555
+ throw new Error("Not authenticated. Please login first.")
3556
+ }
3557
+
3558
+ this.ensureCanUpload()
3559
+
3560
+ if (!this.signerKey || !this.postageBatchId) {
3561
+ throw new Error(
3562
+ "Signer key and postage batch ID required. Please login first.",
3563
+ )
3564
+ }
3565
+
3566
+ if (!this.stamper) {
3567
+ throw new Error("Stamper not initialized. Please login first.")
3568
+ }
3569
+
3570
+ // Prepare upload context
3571
+ const context: UploadContext = {
3572
+ bee: this.bee,
3573
+ stamper: this.stamper,
3574
+ }
3575
+
3576
+ // Use appSecret as publisher private key (user's identity key for this app)
3577
+ const publisherPrivateKey = hexToUint8Array(this.appSecret)
3578
+
3579
+ // Parse grantee public keys from compressed hex
3580
+ const newGranteePublicKeys = grantees.map((hex) =>
3581
+ parseCompressedPublicKey(hex),
3582
+ )
3583
+
3584
+ // Serialize write through Web Locks API to prevent concurrent uploads
3585
+ const result = await this.withWriteLock(async () => {
3586
+ // Add grantees to ACT
3587
+ const addResult = await addGranteesToAct(
3588
+ context,
3589
+ historyReference,
3590
+ publisherPrivateKey,
3591
+ newGranteePublicKeys,
3592
+ undefined,
3593
+ requestOptions,
3594
+ )
3595
+
3596
+ // Save stamper state after successful upload
3597
+ await this.saveStamperState()
3598
+
3599
+ return addResult
3600
+ })
3601
+
3602
+ if (event.source) {
3603
+ ;(event.source as WindowProxy).postMessage(
3604
+ {
3605
+ type: "actAddGranteesResponse",
3606
+ requestId,
3607
+ historyReference: result.historyReference,
3608
+ granteeListReference: result.granteeListReference,
3609
+ actReference: result.actReference,
3610
+ } satisfies IframeToParentMessage,
3611
+ { targetOrigin: event.origin },
3612
+ )
3613
+ }
3614
+ } catch (error) {
3615
+ this.sendErrorToParent(
3616
+ event,
3617
+ requestId,
3618
+ error instanceof Error ? error.message : "ACT add grantees failed",
3619
+ )
3620
+ }
3621
+ }
3622
+
3623
+ private async handleActRevokeGrantees(
3624
+ message: ActRevokeGranteesMessage,
3625
+ event: MessageEvent,
3626
+ ): Promise<void> {
3627
+ const {
3628
+ requestId,
3629
+ historyReference,
3630
+ encryptedReference,
3631
+ revokeGrantees,
3632
+ requestOptions,
3633
+ } = message
3634
+
3635
+ try {
3636
+ if (!this.authenticated || !this.appSecret) {
3637
+ throw new Error("Not authenticated. Please login first.")
3638
+ }
3639
+
3640
+ this.ensureCanUpload()
3641
+
3642
+ if (!this.signerKey || !this.postageBatchId) {
3643
+ throw new Error(
3644
+ "Signer key and postage batch ID required. Please login first.",
3645
+ )
3646
+ }
3647
+
3648
+ if (!this.stamper) {
3649
+ throw new Error("Stamper not initialized. Please login first.")
3650
+ }
3651
+
3652
+ // Prepare upload context
3653
+ const context: UploadContext = {
3654
+ bee: this.bee,
3655
+ stamper: this.stamper,
3656
+ }
3657
+
3658
+ // Use appSecret as publisher private key (user's identity key for this app)
3659
+ const publisherPrivateKey = hexToUint8Array(this.appSecret)
3660
+
3661
+ // Parse grantee public keys from compressed hex
3662
+ const revokePublicKeys = revokeGrantees.map((hex) =>
3663
+ parseCompressedPublicKey(hex),
3664
+ )
3665
+
3666
+ // Serialize write through Web Locks API to prevent concurrent uploads
3667
+ const result = await this.withWriteLock(async () => {
3668
+ // Revoke grantees from ACT (performs key rotation)
3669
+ const revokeResult = await revokeGranteesFromAct(
3670
+ context,
3671
+ historyReference,
3672
+ encryptedReference,
3673
+ publisherPrivateKey,
3674
+ revokePublicKeys,
3675
+ undefined,
3676
+ requestOptions,
3677
+ )
3678
+
3679
+ // Save stamper state after successful upload
3680
+ await this.saveStamperState()
3681
+
3682
+ return revokeResult
3683
+ })
3684
+
3685
+ if (event.source) {
3686
+ ;(event.source as WindowProxy).postMessage(
3687
+ {
3688
+ type: "actRevokeGranteesResponse",
3689
+ requestId,
3690
+ encryptedReference: result.encryptedReference,
3691
+ historyReference: result.historyReference,
3692
+ granteeListReference: result.granteeListReference,
3693
+ actReference: result.actReference,
3694
+ } satisfies IframeToParentMessage,
3695
+ { targetOrigin: event.origin },
3696
+ )
3697
+ }
3698
+ } catch (error) {
3699
+ this.sendErrorToParent(
3700
+ event,
3701
+ requestId,
3702
+ error instanceof Error ? error.message : "ACT revoke grantees failed",
3703
+ )
3704
+ }
3705
+ }
3706
+
3707
+ private async handleActGetGrantees(
3708
+ message: ActGetGranteesMessage,
3709
+ event: MessageEvent,
3710
+ ): Promise<void> {
3711
+ const { requestId, historyReference, requestOptions } = message
3712
+
3713
+ try {
3714
+ if (!this.authenticated || !this.appSecret) {
3715
+ throw new Error("Not authenticated. Please login first.")
3716
+ }
3717
+
3718
+ // appSecret is already checked by authenticated check above
3719
+ // Use appSecret as publisher private key (user's identity key for this app)
3720
+ const publisherPrivateKey = hexToUint8Array(this.appSecret)
3721
+
3722
+ // Get grantees from ACT
3723
+ const grantees = await getGranteesFromAct(
3724
+ this.bee,
3725
+ historyReference,
3726
+ publisherPrivateKey,
3727
+ requestOptions,
3728
+ )
3729
+
3730
+ if (event.source) {
3731
+ ;(event.source as WindowProxy).postMessage(
3732
+ {
3733
+ type: "actGetGranteesResponse",
3734
+ requestId,
3735
+ grantees,
3736
+ } satisfies IframeToParentMessage,
3737
+ { targetOrigin: event.origin },
3738
+ )
3739
+ }
3740
+ } catch (error) {
3741
+ this.sendErrorToParent(
3742
+ event,
3743
+ requestId,
3744
+ error instanceof Error ? error.message : "ACT get grantees failed",
3745
+ )
3746
+ }
3747
+ }
3748
+
3749
+ private async handleGetPostageBatch(
3750
+ message: GetPostageBatchMessage,
3751
+ event: MessageEvent,
3752
+ ): Promise<void> {
3753
+ const stamp = this.lookupPostageStampForApp()
3754
+
3755
+ if (!stamp) {
3756
+ if (event.source) {
3757
+ ;(event.source as WindowProxy).postMessage(
3758
+ {
3759
+ type: "getPostageBatchResponse",
3760
+ requestId: message.requestId,
3761
+ postageBatch: undefined,
3762
+ } satisfies IframeToParentMessage,
3763
+ { targetOrigin: event.origin },
3764
+ )
3765
+ }
3766
+ return
3767
+ }
3768
+
3769
+ // Fetch current price from Swarmscan to calculate TTL
3770
+ let batchTTL: number | undefined = stamp.batchTTL
3771
+ try {
3772
+ const pricePerGBPerMonth = await fetchSwarmPrice()
3773
+ batchTTL = calculateTTLSeconds(stamp.amount, pricePerGBPerMonth)
3774
+ } catch (error) {
3775
+ console.warn("[Proxy] Failed to calculate TTL:", error)
3776
+ }
3777
+
3778
+ // Map PostageStamp to public PostageBatch (exclude signerKey, accountId)
3779
+ const postageBatch: PostageBatch = {
3780
+ batchID: stamp.batchID.toHex(),
3781
+ utilization: stamp.utilization,
3782
+ usable: stamp.usable,
3783
+ label: "", // PostageStamp doesn't store label
3784
+ depth: stamp.depth,
3785
+ amount: stamp.amount.toString(),
3786
+ bucketDepth: stamp.bucketDepth,
3787
+ blockNumber: stamp.blockNumber,
3788
+ immutableFlag: stamp.immutableFlag,
3789
+ exists: stamp.exists,
3790
+ batchTTL,
3791
+ }
3792
+
3793
+ if (event.source) {
3794
+ ;(event.source as WindowProxy).postMessage(
3795
+ {
3796
+ type: "getPostageBatchResponse",
3797
+ requestId: message.requestId,
3798
+ postageBatch,
3799
+ } satisfies IframeToParentMessage,
3800
+ { targetOrigin: event.origin },
3801
+ )
3802
+ }
3803
+ }
3804
+
3805
+ /**
3806
+ * Handle createFeedManifest request
3807
+ * Creates a feed manifest for accessing feed content via URL
3808
+ */
3809
+ private async handleCreateFeedManifest(
3810
+ message: CreateFeedManifestMessage,
3811
+ event: MessageEvent,
3812
+ ): Promise<void> {
3813
+ const { topic, owner, feedType, uploadOptions, requestOptions } = message
3814
+
3815
+ // Resolve owner - use provided or fall back to app signer
3816
+ let resolvedOwner = owner
3817
+ if (!resolvedOwner && this.appSecret) {
3818
+ const signerKeyObj = new PrivateKey(this.appSecret)
3819
+ resolvedOwner = signerKeyObj.publicKey().address().toHex()
3820
+ }
3821
+
3822
+ if (!resolvedOwner) {
3823
+ this.sendErrorToParent(
3824
+ event,
3825
+ message.requestId,
3826
+ "No owner provided and no app signer available",
3827
+ )
3828
+ return
3829
+ }
3830
+
3831
+ try {
3832
+ this.ensureCanUpload()
3833
+
3834
+ if (!this.postageBatchId) {
3835
+ throw new Error("No postage batch configured")
3836
+ }
3837
+
3838
+ if (!this.stamper) {
3839
+ throw new Error("Stamper not initialized. Please login first.")
3840
+ }
3841
+ // DEBUG: Log manifest creation details for /bzz/ compatibility analysis
3842
+
3843
+ // Serialize write through Web Locks API to prevent concurrent uploads
3844
+ const result = await this.withWriteLock(async () => {
3845
+ // Use createFeedManifestDirect to build and upload the manifest locally
3846
+ // instead of calling bee.createFeedManifest (which uses /feeds endpoint)
3847
+ const createResult = await createFeedManifestDirect(
3848
+ this.bee,
3849
+ this.stamper!,
3850
+ topic,
3851
+ resolvedOwner,
3852
+ {
3853
+ encrypt: uploadOptions?.encrypt !== false, // Default encrypted
3854
+ feedType: feedType, // "Sequence" or "Epoch"
3855
+ },
3856
+ uploadOptions,
3857
+ requestOptions,
3858
+ )
3859
+
3860
+ // Save stamper state after successful upload
3861
+ await this.saveStamperState()
3862
+
3863
+ return createResult
3864
+ })
3865
+
3866
+ if (event.source) {
3867
+ ;(event.source as WindowProxy).postMessage(
3868
+ {
3869
+ type: "createFeedManifestResponse",
3870
+ requestId: message.requestId,
3871
+ reference: result.reference,
3872
+ } satisfies IframeToParentMessage,
3873
+ { targetOrigin: event.origin },
3874
+ )
3875
+ }
3876
+ } catch (error) {
3877
+ this.sendErrorToParent(
3878
+ event,
3879
+ message.requestId,
3880
+ error instanceof Error ? error.message : "Create feed manifest failed",
3881
+ )
3882
+ }
3883
+ }
3884
+ }
3885
+
3886
+ /**
3887
+ * Initialize the proxy (called from HTML page)
3888
+ */
3889
+ export function initProxy(): SwarmIdProxy {
3890
+ return new SwarmIdProxy()
3891
+ }