@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.
- package/README.md +431 -0
- package/dist/chunk/bmt.d.ts +17 -0
- package/dist/chunk/bmt.d.ts.map +1 -0
- package/dist/chunk/cac.d.ts +18 -0
- package/dist/chunk/cac.d.ts.map +1 -0
- package/dist/chunk/constants.d.ts +10 -0
- package/dist/chunk/constants.d.ts.map +1 -0
- package/dist/chunk/encrypted-cac.d.ts +48 -0
- package/dist/chunk/encrypted-cac.d.ts.map +1 -0
- package/dist/chunk/encryption.d.ts +86 -0
- package/dist/chunk/encryption.d.ts.map +1 -0
- package/dist/chunk/index.d.ts +6 -0
- package/dist/chunk/index.d.ts.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/proxy/act/act.d.ts +78 -0
- package/dist/proxy/act/act.d.ts.map +1 -0
- package/dist/proxy/act/crypto.d.ts +44 -0
- package/dist/proxy/act/crypto.d.ts.map +1 -0
- package/dist/proxy/act/grantee-list.d.ts +82 -0
- package/dist/proxy/act/grantee-list.d.ts.map +1 -0
- package/dist/proxy/act/history.d.ts +183 -0
- package/dist/proxy/act/history.d.ts.map +1 -0
- package/dist/proxy/act/index.d.ts +104 -0
- package/dist/proxy/act/index.d.ts.map +1 -0
- package/dist/proxy/chunking-encrypted.d.ts +14 -0
- package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
- package/dist/proxy/chunking.d.ts +15 -0
- package/dist/proxy/chunking.d.ts.map +1 -0
- package/dist/proxy/download-data.d.ts +16 -0
- package/dist/proxy/download-data.d.ts.map +1 -0
- package/dist/proxy/feed-manifest.d.ts +62 -0
- package/dist/proxy/feed-manifest.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
- package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/index.d.ts +35 -0
- package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/types.d.ts +109 -0
- package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
- package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
- package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
- package/dist/proxy/feeds/index.d.ts +5 -0
- package/dist/proxy/feeds/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
- package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/index.d.ts +23 -0
- package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/types.d.ts +80 -0
- package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
- package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
- package/dist/proxy/index.d.ts +6 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/manifest-builder.d.ts +183 -0
- package/dist/proxy/manifest-builder.d.ts.map +1 -0
- package/dist/proxy/mantaray-encrypted.d.ts +27 -0
- package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
- package/dist/proxy/mantaray.d.ts +26 -0
- package/dist/proxy/mantaray.d.ts.map +1 -0
- package/dist/proxy/types.d.ts +29 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/upload-data.d.ts +17 -0
- package/dist/proxy/upload-data.d.ts.map +1 -0
- package/dist/proxy/upload-encrypted-data.d.ts +103 -0
- package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
- package/dist/schemas.d.ts +240 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/storage/debounced-uploader.d.ts +62 -0
- package/dist/storage/debounced-uploader.d.ts.map +1 -0
- package/dist/storage/utilization-store.d.ts +108 -0
- package/dist/storage/utilization-store.d.ts.map +1 -0
- package/dist/swarm-id-auth.d.ts +74 -0
- package/dist/swarm-id-auth.d.ts.map +1 -0
- package/dist/swarm-id-auth.js +2 -0
- package/dist/swarm-id-auth.js.map +1 -0
- package/dist/swarm-id-client.d.ts +878 -0
- package/dist/swarm-id-client.d.ts.map +1 -0
- package/dist/swarm-id-client.js +2 -0
- package/dist/swarm-id-client.js.map +1 -0
- package/dist/swarm-id-proxy.d.ts +236 -0
- package/dist/swarm-id-proxy.d.ts.map +1 -0
- package/dist/swarm-id-proxy.js +2 -0
- package/dist/swarm-id-proxy.js.map +1 -0
- package/dist/swarm-id.esm.js +2 -0
- package/dist/swarm-id.esm.js.map +1 -0
- package/dist/swarm-id.umd.js +2 -0
- package/dist/swarm-id.umd.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/key-derivation.d.ts +25 -0
- package/dist/sync/key-derivation.d.ts.map +1 -0
- package/dist/sync/restore-account.d.ts +28 -0
- package/dist/sync/restore-account.d.ts.map +1 -0
- package/dist/sync/serialization.d.ts +16 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/store-interfaces.d.ts +53 -0
- package/dist/sync/store-interfaces.d.ts.map +1 -0
- package/dist/sync/sync-account.d.ts +44 -0
- package/dist/sync/sync-account.d.ts.map +1 -0
- package/dist/sync/types.d.ts +13 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/test-fixtures.d.ts +17 -0
- package/dist/test-fixtures.d.ts.map +1 -0
- package/dist/types-BD_VkNn0.js +2 -0
- package/dist/types-BD_VkNn0.js.map +1 -0
- package/dist/types-lJCaT-50.js +2 -0
- package/dist/types-lJCaT-50.js.map +1 -0
- package/dist/types.d.ts +2157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/account-payload.d.ts +94 -0
- package/dist/utils/account-payload.d.ts.map +1 -0
- package/dist/utils/account-state-snapshot.d.ts +38 -0
- package/dist/utils/account-state-snapshot.d.ts.map +1 -0
- package/dist/utils/backup-encryption.d.ts +127 -0
- package/dist/utils/backup-encryption.d.ts.map +1 -0
- package/dist/utils/batch-utilization.d.ts +432 -0
- package/dist/utils/batch-utilization.d.ts.map +1 -0
- package/dist/utils/constants.d.ts +11 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/hex.d.ts +17 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/key-derivation.d.ts +92 -0
- package/dist/utils/key-derivation.d.ts.map +1 -0
- package/dist/utils/storage-managers.d.ts +65 -0
- package/dist/utils/storage-managers.d.ts.map +1 -0
- package/dist/utils/swarm-id-export.d.ts +24 -0
- package/dist/utils/swarm-id-export.d.ts.map +1 -0
- package/dist/utils/ttl.d.ts +49 -0
- package/dist/utils/ttl.d.ts.map +1 -0
- package/dist/utils/url.d.ts +41 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/versioned-storage.d.ts +131 -0
- package/dist/utils/versioned-storage.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/chunk/bmt.test.ts +217 -0
- package/src/chunk/bmt.ts +57 -0
- package/src/chunk/cac.test.ts +214 -0
- package/src/chunk/cac.ts +65 -0
- package/src/chunk/constants.ts +18 -0
- package/src/chunk/encrypted-cac.test.ts +385 -0
- package/src/chunk/encrypted-cac.ts +131 -0
- package/src/chunk/encryption.test.ts +352 -0
- package/src/chunk/encryption.ts +300 -0
- package/src/chunk/index.ts +47 -0
- package/src/index.ts +430 -0
- package/src/proxy/act/act.test.ts +278 -0
- package/src/proxy/act/act.ts +158 -0
- package/src/proxy/act/bee-compat.test.ts +948 -0
- package/src/proxy/act/crypto.test.ts +436 -0
- package/src/proxy/act/crypto.ts +376 -0
- package/src/proxy/act/grantee-list.test.ts +393 -0
- package/src/proxy/act/grantee-list.ts +239 -0
- package/src/proxy/act/history.test.ts +360 -0
- package/src/proxy/act/history.ts +413 -0
- package/src/proxy/act/index.test.ts +748 -0
- package/src/proxy/act/index.ts +853 -0
- package/src/proxy/chunking-encrypted.ts +95 -0
- package/src/proxy/chunking.ts +65 -0
- package/src/proxy/download-data.ts +448 -0
- package/src/proxy/feed-manifest.ts +174 -0
- package/src/proxy/feeds/epochs/async-finder.ts +372 -0
- package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
- package/src/proxy/feeds/epochs/epoch.ts +181 -0
- package/src/proxy/feeds/epochs/finder.ts +282 -0
- package/src/proxy/feeds/epochs/index.ts +73 -0
- package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
- package/src/proxy/feeds/epochs/test-utils.ts +274 -0
- package/src/proxy/feeds/epochs/types.ts +128 -0
- package/src/proxy/feeds/epochs/updater.ts +192 -0
- package/src/proxy/feeds/epochs/utils.ts +62 -0
- package/src/proxy/feeds/index.ts +5 -0
- package/src/proxy/feeds/sequence/async-finder.ts +31 -0
- package/src/proxy/feeds/sequence/finder.ts +73 -0
- package/src/proxy/feeds/sequence/index.ts +54 -0
- package/src/proxy/feeds/sequence/integration.test.ts +966 -0
- package/src/proxy/feeds/sequence/types.ts +103 -0
- package/src/proxy/feeds/sequence/updater.ts +71 -0
- package/src/proxy/index.ts +5 -0
- package/src/proxy/manifest-builder.test.ts +427 -0
- package/src/proxy/manifest-builder.ts +679 -0
- package/src/proxy/mantaray-encrypted.ts +78 -0
- package/src/proxy/mantaray.ts +104 -0
- package/src/proxy/types.ts +32 -0
- package/src/proxy/upload-data.ts +189 -0
- package/src/proxy/upload-encrypted-data.ts +658 -0
- package/src/schemas.ts +299 -0
- package/src/storage/debounced-uploader.ts +192 -0
- package/src/storage/utilization-store.ts +397 -0
- package/src/swarm-id-client.test.ts +99 -0
- package/src/swarm-id-client.ts +3095 -0
- package/src/swarm-id-proxy.ts +3891 -0
- package/src/sync/index.ts +28 -0
- package/src/sync/restore-account.ts +90 -0
- package/src/sync/serialization.ts +39 -0
- package/src/sync/store-interfaces.ts +62 -0
- package/src/sync/sync-account.test.ts +302 -0
- package/src/sync/sync-account.ts +396 -0
- package/src/sync/types.ts +11 -0
- package/src/test-fixtures.ts +109 -0
- package/src/types.ts +1651 -0
- package/src/utils/account-state-snapshot.test.ts +595 -0
- package/src/utils/account-state-snapshot.ts +94 -0
- package/src/utils/backup-encryption.test.ts +442 -0
- package/src/utils/backup-encryption.ts +352 -0
- package/src/utils/batch-utilization.ts +1309 -0
- package/src/utils/constants.ts +20 -0
- package/src/utils/hex.ts +27 -0
- package/src/utils/key-derivation.ts +197 -0
- package/src/utils/storage-managers.ts +365 -0
- package/src/utils/ttl.ts +129 -0
- package/src/utils/url.test.ts +136 -0
- package/src/utils/url.ts +71 -0
- 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
|
+
}
|