@snaha/swarm-id 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +431 -0
  2. package/dist/chunk/bmt.d.ts +17 -0
  3. package/dist/chunk/bmt.d.ts.map +1 -0
  4. package/dist/chunk/cac.d.ts +18 -0
  5. package/dist/chunk/cac.d.ts.map +1 -0
  6. package/dist/chunk/constants.d.ts +10 -0
  7. package/dist/chunk/constants.d.ts.map +1 -0
  8. package/dist/chunk/encrypted-cac.d.ts +48 -0
  9. package/dist/chunk/encrypted-cac.d.ts.map +1 -0
  10. package/dist/chunk/encryption.d.ts +86 -0
  11. package/dist/chunk/encryption.d.ts.map +1 -0
  12. package/dist/chunk/index.d.ts +6 -0
  13. package/dist/chunk/index.d.ts.map +1 -0
  14. package/dist/index.d.ts +46 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/proxy/act/act.d.ts +78 -0
  17. package/dist/proxy/act/act.d.ts.map +1 -0
  18. package/dist/proxy/act/crypto.d.ts +44 -0
  19. package/dist/proxy/act/crypto.d.ts.map +1 -0
  20. package/dist/proxy/act/grantee-list.d.ts +82 -0
  21. package/dist/proxy/act/grantee-list.d.ts.map +1 -0
  22. package/dist/proxy/act/history.d.ts +183 -0
  23. package/dist/proxy/act/history.d.ts.map +1 -0
  24. package/dist/proxy/act/index.d.ts +104 -0
  25. package/dist/proxy/act/index.d.ts.map +1 -0
  26. package/dist/proxy/chunking-encrypted.d.ts +14 -0
  27. package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
  28. package/dist/proxy/chunking.d.ts +15 -0
  29. package/dist/proxy/chunking.d.ts.map +1 -0
  30. package/dist/proxy/download-data.d.ts +16 -0
  31. package/dist/proxy/download-data.d.ts.map +1 -0
  32. package/dist/proxy/feed-manifest.d.ts +62 -0
  33. package/dist/proxy/feed-manifest.d.ts.map +1 -0
  34. package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
  35. package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
  36. package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
  37. package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
  38. package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
  39. package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
  40. package/dist/proxy/feeds/epochs/index.d.ts +35 -0
  41. package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
  42. package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
  43. package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
  44. package/dist/proxy/feeds/epochs/types.d.ts +109 -0
  45. package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
  46. package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
  47. package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
  48. package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
  49. package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
  50. package/dist/proxy/feeds/index.d.ts +5 -0
  51. package/dist/proxy/feeds/index.d.ts.map +1 -0
  52. package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
  53. package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
  54. package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
  55. package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
  56. package/dist/proxy/feeds/sequence/index.d.ts +23 -0
  57. package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
  58. package/dist/proxy/feeds/sequence/types.d.ts +80 -0
  59. package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
  60. package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
  61. package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
  62. package/dist/proxy/index.d.ts +6 -0
  63. package/dist/proxy/index.d.ts.map +1 -0
  64. package/dist/proxy/manifest-builder.d.ts +183 -0
  65. package/dist/proxy/manifest-builder.d.ts.map +1 -0
  66. package/dist/proxy/mantaray-encrypted.d.ts +27 -0
  67. package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
  68. package/dist/proxy/mantaray.d.ts +26 -0
  69. package/dist/proxy/mantaray.d.ts.map +1 -0
  70. package/dist/proxy/types.d.ts +29 -0
  71. package/dist/proxy/types.d.ts.map +1 -0
  72. package/dist/proxy/upload-data.d.ts +17 -0
  73. package/dist/proxy/upload-data.d.ts.map +1 -0
  74. package/dist/proxy/upload-encrypted-data.d.ts +103 -0
  75. package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
  76. package/dist/schemas.d.ts +240 -0
  77. package/dist/schemas.d.ts.map +1 -0
  78. package/dist/storage/debounced-uploader.d.ts +62 -0
  79. package/dist/storage/debounced-uploader.d.ts.map +1 -0
  80. package/dist/storage/utilization-store.d.ts +108 -0
  81. package/dist/storage/utilization-store.d.ts.map +1 -0
  82. package/dist/swarm-id-auth.d.ts +74 -0
  83. package/dist/swarm-id-auth.d.ts.map +1 -0
  84. package/dist/swarm-id-auth.js +2 -0
  85. package/dist/swarm-id-auth.js.map +1 -0
  86. package/dist/swarm-id-client.d.ts +878 -0
  87. package/dist/swarm-id-client.d.ts.map +1 -0
  88. package/dist/swarm-id-client.js +2 -0
  89. package/dist/swarm-id-client.js.map +1 -0
  90. package/dist/swarm-id-proxy.d.ts +236 -0
  91. package/dist/swarm-id-proxy.d.ts.map +1 -0
  92. package/dist/swarm-id-proxy.js +2 -0
  93. package/dist/swarm-id-proxy.js.map +1 -0
  94. package/dist/swarm-id.esm.js +2 -0
  95. package/dist/swarm-id.esm.js.map +1 -0
  96. package/dist/swarm-id.umd.js +2 -0
  97. package/dist/swarm-id.umd.js.map +1 -0
  98. package/dist/sync/index.d.ts +9 -0
  99. package/dist/sync/index.d.ts.map +1 -0
  100. package/dist/sync/key-derivation.d.ts +25 -0
  101. package/dist/sync/key-derivation.d.ts.map +1 -0
  102. package/dist/sync/restore-account.d.ts +28 -0
  103. package/dist/sync/restore-account.d.ts.map +1 -0
  104. package/dist/sync/serialization.d.ts +16 -0
  105. package/dist/sync/serialization.d.ts.map +1 -0
  106. package/dist/sync/store-interfaces.d.ts +53 -0
  107. package/dist/sync/store-interfaces.d.ts.map +1 -0
  108. package/dist/sync/sync-account.d.ts +44 -0
  109. package/dist/sync/sync-account.d.ts.map +1 -0
  110. package/dist/sync/types.d.ts +13 -0
  111. package/dist/sync/types.d.ts.map +1 -0
  112. package/dist/test-fixtures.d.ts +17 -0
  113. package/dist/test-fixtures.d.ts.map +1 -0
  114. package/dist/types-BD_VkNn0.js +2 -0
  115. package/dist/types-BD_VkNn0.js.map +1 -0
  116. package/dist/types-lJCaT-50.js +2 -0
  117. package/dist/types-lJCaT-50.js.map +1 -0
  118. package/dist/types.d.ts +2157 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/utils/account-payload.d.ts +94 -0
  121. package/dist/utils/account-payload.d.ts.map +1 -0
  122. package/dist/utils/account-state-snapshot.d.ts +38 -0
  123. package/dist/utils/account-state-snapshot.d.ts.map +1 -0
  124. package/dist/utils/backup-encryption.d.ts +127 -0
  125. package/dist/utils/backup-encryption.d.ts.map +1 -0
  126. package/dist/utils/batch-utilization.d.ts +432 -0
  127. package/dist/utils/batch-utilization.d.ts.map +1 -0
  128. package/dist/utils/constants.d.ts +11 -0
  129. package/dist/utils/constants.d.ts.map +1 -0
  130. package/dist/utils/hex.d.ts +17 -0
  131. package/dist/utils/hex.d.ts.map +1 -0
  132. package/dist/utils/key-derivation.d.ts +92 -0
  133. package/dist/utils/key-derivation.d.ts.map +1 -0
  134. package/dist/utils/storage-managers.d.ts +65 -0
  135. package/dist/utils/storage-managers.d.ts.map +1 -0
  136. package/dist/utils/swarm-id-export.d.ts +24 -0
  137. package/dist/utils/swarm-id-export.d.ts.map +1 -0
  138. package/dist/utils/ttl.d.ts +49 -0
  139. package/dist/utils/ttl.d.ts.map +1 -0
  140. package/dist/utils/url.d.ts +41 -0
  141. package/dist/utils/url.d.ts.map +1 -0
  142. package/dist/utils/versioned-storage.d.ts +131 -0
  143. package/dist/utils/versioned-storage.d.ts.map +1 -0
  144. package/package.json +78 -0
  145. package/src/chunk/bmt.test.ts +217 -0
  146. package/src/chunk/bmt.ts +57 -0
  147. package/src/chunk/cac.test.ts +214 -0
  148. package/src/chunk/cac.ts +65 -0
  149. package/src/chunk/constants.ts +18 -0
  150. package/src/chunk/encrypted-cac.test.ts +385 -0
  151. package/src/chunk/encrypted-cac.ts +131 -0
  152. package/src/chunk/encryption.test.ts +352 -0
  153. package/src/chunk/encryption.ts +300 -0
  154. package/src/chunk/index.ts +47 -0
  155. package/src/index.ts +430 -0
  156. package/src/proxy/act/act.test.ts +278 -0
  157. package/src/proxy/act/act.ts +158 -0
  158. package/src/proxy/act/bee-compat.test.ts +948 -0
  159. package/src/proxy/act/crypto.test.ts +436 -0
  160. package/src/proxy/act/crypto.ts +376 -0
  161. package/src/proxy/act/grantee-list.test.ts +393 -0
  162. package/src/proxy/act/grantee-list.ts +239 -0
  163. package/src/proxy/act/history.test.ts +360 -0
  164. package/src/proxy/act/history.ts +413 -0
  165. package/src/proxy/act/index.test.ts +748 -0
  166. package/src/proxy/act/index.ts +853 -0
  167. package/src/proxy/chunking-encrypted.ts +95 -0
  168. package/src/proxy/chunking.ts +65 -0
  169. package/src/proxy/download-data.ts +448 -0
  170. package/src/proxy/feed-manifest.ts +174 -0
  171. package/src/proxy/feeds/epochs/async-finder.ts +372 -0
  172. package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
  173. package/src/proxy/feeds/epochs/epoch.ts +181 -0
  174. package/src/proxy/feeds/epochs/finder.ts +282 -0
  175. package/src/proxy/feeds/epochs/index.ts +73 -0
  176. package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
  177. package/src/proxy/feeds/epochs/test-utils.ts +274 -0
  178. package/src/proxy/feeds/epochs/types.ts +128 -0
  179. package/src/proxy/feeds/epochs/updater.ts +192 -0
  180. package/src/proxy/feeds/epochs/utils.ts +62 -0
  181. package/src/proxy/feeds/index.ts +5 -0
  182. package/src/proxy/feeds/sequence/async-finder.ts +31 -0
  183. package/src/proxy/feeds/sequence/finder.ts +73 -0
  184. package/src/proxy/feeds/sequence/index.ts +54 -0
  185. package/src/proxy/feeds/sequence/integration.test.ts +966 -0
  186. package/src/proxy/feeds/sequence/types.ts +103 -0
  187. package/src/proxy/feeds/sequence/updater.ts +71 -0
  188. package/src/proxy/index.ts +5 -0
  189. package/src/proxy/manifest-builder.test.ts +427 -0
  190. package/src/proxy/manifest-builder.ts +679 -0
  191. package/src/proxy/mantaray-encrypted.ts +78 -0
  192. package/src/proxy/mantaray.ts +104 -0
  193. package/src/proxy/types.ts +32 -0
  194. package/src/proxy/upload-data.ts +189 -0
  195. package/src/proxy/upload-encrypted-data.ts +658 -0
  196. package/src/schemas.ts +299 -0
  197. package/src/storage/debounced-uploader.ts +192 -0
  198. package/src/storage/utilization-store.ts +397 -0
  199. package/src/swarm-id-client.test.ts +99 -0
  200. package/src/swarm-id-client.ts +3095 -0
  201. package/src/swarm-id-proxy.ts +3891 -0
  202. package/src/sync/index.ts +28 -0
  203. package/src/sync/restore-account.ts +90 -0
  204. package/src/sync/serialization.ts +39 -0
  205. package/src/sync/store-interfaces.ts +62 -0
  206. package/src/sync/sync-account.test.ts +302 -0
  207. package/src/sync/sync-account.ts +396 -0
  208. package/src/sync/types.ts +11 -0
  209. package/src/test-fixtures.ts +109 -0
  210. package/src/types.ts +1651 -0
  211. package/src/utils/account-state-snapshot.test.ts +595 -0
  212. package/src/utils/account-state-snapshot.ts +94 -0
  213. package/src/utils/backup-encryption.test.ts +442 -0
  214. package/src/utils/backup-encryption.ts +352 -0
  215. package/src/utils/batch-utilization.ts +1309 -0
  216. package/src/utils/constants.ts +20 -0
  217. package/src/utils/hex.ts +27 -0
  218. package/src/utils/key-derivation.ts +197 -0
  219. package/src/utils/storage-managers.ts +365 -0
  220. package/src/utils/ttl.ts +129 -0
  221. package/src/utils/url.test.ts +136 -0
  222. package/src/utils/url.ts +71 -0
  223. package/src/utils/versioned-storage.ts +323 -0
@@ -0,0 +1,748 @@
1
+ /**
2
+ * Unit tests for high-level ACT operations (Bee-compatible API)
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from "vitest"
6
+ import type { Bee, Stamper } from "@ethersphere/bee-js"
7
+ import { MerkleTree } from "@ethersphere/bee-js"
8
+ import type { UploadContext } from "../types"
9
+ import {
10
+ createActForContent,
11
+ decryptActReference,
12
+ addGranteesToAct,
13
+ revokeGranteesFromAct,
14
+ getGranteesFromAct,
15
+ parseCompressedPublicKey,
16
+ } from "./index"
17
+ import {
18
+ publicKeyFromPrivate,
19
+ compressPublicKey,
20
+ deriveKeys,
21
+ counterModeDecrypt,
22
+ } from "./crypto"
23
+ import { collectActEntriesFromJson, findActEntryByKey } from "./act"
24
+ import { decryptAndDeserializeGranteeList } from "./grantee-list"
25
+
26
+ // Mock the upload/download functions
27
+ vi.mock("../upload-encrypted-data", () => ({
28
+ uploadEncryptedDataWithSigning: vi.fn(),
29
+ }))
30
+
31
+ vi.mock("../download-data", () => ({
32
+ downloadDataWithChunkAPI: vi.fn(),
33
+ }))
34
+
35
+ import { uploadEncryptedDataWithSigning } from "../upload-encrypted-data"
36
+ import { downloadDataWithChunkAPI } from "../download-data"
37
+
38
+ // Helper to create a random 32-byte array
39
+ function randomBytes(length: number): Uint8Array {
40
+ const bytes = new Uint8Array(length)
41
+ crypto.getRandomValues(bytes)
42
+ return bytes
43
+ }
44
+
45
+ // Helper to convert bytes to hex
46
+ function toHex(bytes: Uint8Array): string {
47
+ return Array.from(bytes)
48
+ .map((b) => b.toString(16).padStart(2, "0"))
49
+ .join("")
50
+ }
51
+
52
+ // Helper to convert hex to bytes
53
+ function fromHex(hex: string): Uint8Array {
54
+ const bytes = new Uint8Array(hex.length / 2)
55
+ for (let i = 0; i < hex.length; i += 2) {
56
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
57
+ }
58
+ return bytes
59
+ }
60
+
61
+ // Create mock context
62
+ function createMockContext(): UploadContext {
63
+ return {
64
+ bee: {} as Bee,
65
+ stamper: {} as Stamper,
66
+ }
67
+ }
68
+
69
+ // Create a valid 32-byte hex reference for mocking
70
+ function createMockReference(counter: number): string {
71
+ // Create a 32-byte array with the counter as the last byte
72
+ const bytes = new Uint8Array(32)
73
+ bytes[31] = counter
74
+ return toHex(bytes)
75
+ }
76
+
77
+ // Compute content hash using Swarm's BMT algorithm (same as MantarayNode)
78
+ async function computeContentHash(data: Uint8Array): Promise<string> {
79
+ const rootNode = await MerkleTree.root(data)
80
+ return toHex(rootNode.hash())
81
+ }
82
+
83
+ // Create a content-addressed storage mock for uploads
84
+ function createContentAddressedUploadMock() {
85
+ const storage: Map<string, Uint8Array> = new Map()
86
+
87
+ const uploadMock = async (
88
+ _ctx: UploadContext,
89
+ data: Uint8Array,
90
+ ): Promise<{
91
+ reference: string
92
+ tagUid?: number
93
+ chunkAddresses: Uint8Array[]
94
+ }> => {
95
+ const address = await computeContentHash(data)
96
+ // Fake 64-byte encrypted reference: address + address
97
+ const ref = `${address}${address}`
98
+ storage.set(ref, data)
99
+ return { reference: ref, chunkAddresses: [] }
100
+ }
101
+
102
+ const downloadMock = async (_bee: Bee, ref: string): Promise<Uint8Array> => {
103
+ const data = storage.get(ref)
104
+ if (!data) {
105
+ throw new Error(`Data not found for reference: ${ref}`)
106
+ }
107
+ return data
108
+ }
109
+
110
+ return { storage, uploadMock, downloadMock }
111
+ }
112
+
113
+ // Create test key pair
114
+ function createTestKeyPair(seed: number): {
115
+ privateKey: Uint8Array
116
+ publicKey: { x: Uint8Array; y: Uint8Array }
117
+ compressedPublicKey: string
118
+ } {
119
+ const privateKey = new Uint8Array(32)
120
+ privateKey[31] = seed
121
+ const publicKey = publicKeyFromPrivate(privateKey)
122
+ const compressed = compressPublicKey(publicKey.x, publicKey.y)
123
+ return {
124
+ privateKey,
125
+ publicKey,
126
+ compressedPublicKey: toHex(compressed),
127
+ }
128
+ }
129
+
130
+ // Helper to load ACT JSON data from storage
131
+ function loadActDataFromStorage(
132
+ storage: Map<string, Uint8Array>,
133
+ actReference: string,
134
+ ): Uint8Array {
135
+ const actData = storage.get(actReference)
136
+ if (!actData) {
137
+ throw new Error(`ACT data not found for reference: ${actReference}`)
138
+ }
139
+ return actData
140
+ }
141
+
142
+ describe("parseCompressedPublicKey", () => {
143
+ it("should parse compressed public key from hex string", () => {
144
+ const keyPair = createTestKeyPair(1)
145
+ const parsed = parseCompressedPublicKey(keyPair.compressedPublicKey)
146
+
147
+ expect(parsed.x).toEqual(keyPair.publicKey.x)
148
+ expect(parsed.y).toEqual(keyPair.publicKey.y)
149
+ })
150
+
151
+ it("should throw for invalid hex string", () => {
152
+ expect(() => parseCompressedPublicKey("invalid")).toThrow()
153
+ expect(() => parseCompressedPublicKey("0102")).toThrow() // Too short
154
+ })
155
+ })
156
+
157
+ describe("createActForContent", () => {
158
+ beforeEach(() => {
159
+ vi.clearAllMocks()
160
+ })
161
+
162
+ it("should create ACT with publisher and grantees", async () => {
163
+ const context = createMockContext()
164
+ const publisher = createTestKeyPair(10)
165
+ const grantee1 = createTestKeyPair(11)
166
+ const grantee2 = createTestKeyPair(12)
167
+
168
+ const contentRef = randomBytes(64)
169
+
170
+ // Use content-addressed storage mock
171
+ const { storage, uploadMock } = createContentAddressedUploadMock()
172
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
173
+
174
+ const result = await createActForContent(
175
+ context,
176
+ contentRef,
177
+ publisher.privateKey,
178
+ [grantee1.publicKey, grantee2.publicKey],
179
+ )
180
+
181
+ // Should have all references
182
+ expect(result.encryptedReference).toBeDefined()
183
+ expect(result.encryptedReference.length).toBe(128) // 64 bytes = 128 hex chars
184
+ expect(result.historyReference).toBeDefined()
185
+ expect(result.granteeListReference).toBeDefined()
186
+ expect(result.publisherPubKey).toBeDefined()
187
+ expect(result.actReference).toBeDefined()
188
+
189
+ // ACT manifest should have 3 entries (publisher + 2 grantees)
190
+ const actData = loadActDataFromStorage(storage, result.actReference)
191
+ const actEntries = collectActEntriesFromJson(actData)
192
+ expect(actEntries.length).toBe(3)
193
+
194
+ // Grantee list should have 2 grantees
195
+ const granteeListBlob = storage.get(result.granteeListReference)
196
+ expect(granteeListBlob).toBeDefined()
197
+ const grantees = decryptAndDeserializeGranteeList(
198
+ granteeListBlob!,
199
+ publisher.privateKey,
200
+ )
201
+ expect(grantees.length).toBe(2)
202
+ })
203
+
204
+ it("should create ACT that publisher can decrypt", async () => {
205
+ const context = createMockContext()
206
+ const publisher = createTestKeyPair(20)
207
+ const grantee = createTestKeyPair(21)
208
+
209
+ const contentRef = randomBytes(32)
210
+
211
+ // Use content-addressed storage mock
212
+ const { storage, uploadMock } = createContentAddressedUploadMock()
213
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
214
+
215
+ const result = await createActForContent(
216
+ context,
217
+ contentRef,
218
+ publisher.privateKey,
219
+ [grantee.publicKey],
220
+ )
221
+
222
+ // Publisher should be able to decrypt
223
+ const actData = loadActDataFromStorage(storage, result.actReference)
224
+
225
+ // Derive publisher's lookup key (publisher uses their own pub key)
226
+ const publisherKeys = deriveKeys(
227
+ publisher.privateKey,
228
+ publisher.publicKey.x,
229
+ publisher.publicKey.y,
230
+ )
231
+
232
+ // Find publisher's entry
233
+ const encryptedAccessKey = findActEntryByKey(
234
+ actData,
235
+ publisherKeys.lookupKey,
236
+ )
237
+ expect(encryptedAccessKey).toBeDefined()
238
+
239
+ // Decrypt access key
240
+ const accessKey = counterModeDecrypt(
241
+ encryptedAccessKey!,
242
+ publisherKeys.accessKeyDecryptionKey,
243
+ )
244
+
245
+ // Decrypt the encrypted reference
246
+ const encryptedRefBytes = fromHex(result.encryptedReference)
247
+ const decryptedRef = counterModeDecrypt(encryptedRefBytes, accessKey)
248
+
249
+ // First 32 bytes should match original content ref
250
+ expect(decryptedRef.slice(0, 32)).toEqual(contentRef)
251
+ })
252
+
253
+ it("should create ACT that grantee can decrypt", async () => {
254
+ const context = createMockContext()
255
+ const publisher = createTestKeyPair(30)
256
+ const grantee = createTestKeyPair(31)
257
+
258
+ const contentRef = randomBytes(32)
259
+
260
+ // Use content-addressed storage mock
261
+ const { storage, uploadMock } = createContentAddressedUploadMock()
262
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
263
+
264
+ const result = await createActForContent(
265
+ context,
266
+ contentRef,
267
+ publisher.privateKey,
268
+ [grantee.publicKey],
269
+ )
270
+
271
+ // Grantee should be able to decrypt
272
+ const actData = loadActDataFromStorage(storage, result.actReference)
273
+
274
+ // Grantee derives keys using publisher's public key
275
+ const granteeKeys = deriveKeys(
276
+ grantee.privateKey,
277
+ publisher.publicKey.x,
278
+ publisher.publicKey.y,
279
+ )
280
+
281
+ // Find grantee's entry
282
+ const encryptedAccessKey = findActEntryByKey(actData, granteeKeys.lookupKey)
283
+ expect(encryptedAccessKey).toBeDefined()
284
+
285
+ // Decrypt access key
286
+ const accessKey = counterModeDecrypt(
287
+ encryptedAccessKey!,
288
+ granteeKeys.accessKeyDecryptionKey,
289
+ )
290
+
291
+ // Decrypt the encrypted reference
292
+ const encryptedRefBytes = fromHex(result.encryptedReference)
293
+ const decryptedRef = counterModeDecrypt(encryptedRefBytes, accessKey)
294
+
295
+ // First 32 bytes should match original content ref
296
+ expect(decryptedRef.slice(0, 32)).toEqual(contentRef)
297
+ })
298
+ })
299
+
300
+ describe("decryptActReference", () => {
301
+ beforeEach(() => {
302
+ vi.clearAllMocks()
303
+ })
304
+
305
+ it("should decrypt reference when reader is a grantee", async () => {
306
+ const bee = {} as Bee
307
+ const publisher = createTestKeyPair(40)
308
+ const grantee = createTestKeyPair(41)
309
+
310
+ const originalContentRef = randomBytes(32)
311
+ const originalContentRefHex = toHex(originalContentRef)
312
+
313
+ // Use content-addressed storage mock
314
+ const { uploadMock, downloadMock } = createContentAddressedUploadMock()
315
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
316
+
317
+ const context = createMockContext()
318
+ const createResult = await createActForContent(
319
+ context,
320
+ originalContentRef,
321
+ publisher.privateKey,
322
+ [grantee.publicKey],
323
+ )
324
+
325
+ // Mock download to return the appropriate blobs
326
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
327
+
328
+ // Grantee should be able to decrypt
329
+ const decryptedRef = await decryptActReference(
330
+ bee,
331
+ createResult.encryptedReference,
332
+ createResult.historyReference,
333
+ createResult.publisherPubKey,
334
+ grantee.privateKey,
335
+ )
336
+
337
+ expect(decryptedRef).toBe(originalContentRefHex)
338
+ })
339
+
340
+ it("should decrypt reference when reader is the publisher", async () => {
341
+ const bee = {} as Bee
342
+ const publisher = createTestKeyPair(50)
343
+ const grantee = createTestKeyPair(51)
344
+
345
+ const originalContentRef = randomBytes(32)
346
+ const originalContentRefHex = toHex(originalContentRef)
347
+
348
+ // Use content-addressed storage mock
349
+ const { uploadMock, downloadMock } = createContentAddressedUploadMock()
350
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
351
+
352
+ const context = createMockContext()
353
+ const createResult = await createActForContent(
354
+ context,
355
+ originalContentRef,
356
+ publisher.privateKey,
357
+ [grantee.publicKey],
358
+ )
359
+
360
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
361
+
362
+ // Publisher should be able to decrypt their own content
363
+ const decryptedRef = await decryptActReference(
364
+ bee,
365
+ createResult.encryptedReference,
366
+ createResult.historyReference,
367
+ createResult.publisherPubKey,
368
+ publisher.privateKey,
369
+ )
370
+
371
+ expect(decryptedRef).toBe(originalContentRefHex)
372
+ })
373
+
374
+ it("should throw error when reader is not authorized", async () => {
375
+ const bee = {} as Bee
376
+ const publisher = createTestKeyPair(60)
377
+ const grantee = createTestKeyPair(61)
378
+ const unauthorized = createTestKeyPair(62)
379
+
380
+ const originalContentRef = randomBytes(32)
381
+
382
+ // Use content-addressed storage mock
383
+ const { uploadMock, downloadMock } = createContentAddressedUploadMock()
384
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
385
+
386
+ const context = createMockContext()
387
+ const createResult = await createActForContent(
388
+ context,
389
+ originalContentRef,
390
+ publisher.privateKey,
391
+ [grantee.publicKey],
392
+ )
393
+
394
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
395
+
396
+ // Unauthorized user should not be able to decrypt
397
+ await expect(
398
+ decryptActReference(
399
+ bee,
400
+ createResult.encryptedReference,
401
+ createResult.historyReference,
402
+ createResult.publisherPubKey,
403
+ unauthorized.privateKey,
404
+ ),
405
+ ).rejects.toThrow("Access denied")
406
+ })
407
+ })
408
+
409
+ describe("addGranteesToAct", () => {
410
+ beforeEach(() => {
411
+ vi.clearAllMocks()
412
+ })
413
+
414
+ it("should add new grantees to existing ACT", async () => {
415
+ const publisher = createTestKeyPair(70)
416
+ const grantee1 = createTestKeyPair(71)
417
+ const grantee2 = createTestKeyPair(72) // New grantee to add
418
+
419
+ const originalContentRef = randomBytes(32)
420
+
421
+ // Use content-addressed storage mock
422
+ const { storage, uploadMock, downloadMock } =
423
+ createContentAddressedUploadMock()
424
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
425
+
426
+ const context = createMockContext()
427
+ const createResult = await createActForContent(
428
+ context,
429
+ originalContentRef,
430
+ publisher.privateKey,
431
+ [grantee1.publicKey],
432
+ )
433
+
434
+ // Verify original has 2 entries (publisher + grantee1)
435
+ const originalActData = loadActDataFromStorage(
436
+ storage,
437
+ createResult.actReference,
438
+ )
439
+ const originalEntries = collectActEntriesFromJson(originalActData)
440
+ expect(originalEntries.length).toBe(2)
441
+
442
+ // Mock download to return the uploaded blobs
443
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
444
+
445
+ // Add new grantee
446
+ const result = await addGranteesToAct(
447
+ context,
448
+ createResult.historyReference,
449
+ publisher.privateKey,
450
+ [grantee2.publicKey],
451
+ )
452
+
453
+ expect(result.actReference).toBeDefined()
454
+ expect(result.historyReference).toBeDefined()
455
+ expect(result.granteeListReference).toBeDefined()
456
+
457
+ // Verify new ACT has 3 entries
458
+ const newActData = loadActDataFromStorage(storage, result.actReference)
459
+ const newEntries = collectActEntriesFromJson(newActData)
460
+ expect(newEntries.length).toBe(3)
461
+
462
+ // Verify grantee list has 2 grantees
463
+ const newGranteeListBlob = storage.get(result.granteeListReference)!
464
+ const grantees = decryptAndDeserializeGranteeList(
465
+ newGranteeListBlob,
466
+ publisher.privateKey,
467
+ )
468
+ expect(grantees.length).toBe(2)
469
+
470
+ // New grantee should be able to find their entry
471
+ const grantee2Keys = deriveKeys(
472
+ grantee2.privateKey,
473
+ publisher.publicKey.x,
474
+ publisher.publicKey.y,
475
+ )
476
+ const grantee2Entry = findActEntryByKey(newActData, grantee2Keys.lookupKey)
477
+ expect(grantee2Entry).toBeDefined()
478
+ })
479
+ })
480
+
481
+ describe("revokeGranteesFromAct", () => {
482
+ beforeEach(() => {
483
+ vi.clearAllMocks()
484
+ })
485
+
486
+ it("should revoke grantees and rotate keys", async () => {
487
+ const publisher = createTestKeyPair(80)
488
+ const grantee1 = createTestKeyPair(81)
489
+ const grantee2 = createTestKeyPair(82) // Will be revoked
490
+
491
+ const originalContentRef = randomBytes(32)
492
+
493
+ // Use content-addressed storage mock
494
+ const { storage, uploadMock, downloadMock } =
495
+ createContentAddressedUploadMock()
496
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
497
+
498
+ const context = createMockContext()
499
+ const createResult = await createActForContent(
500
+ context,
501
+ originalContentRef,
502
+ publisher.privateKey,
503
+ [grantee1.publicKey, grantee2.publicKey],
504
+ )
505
+
506
+ // Verify original has 3 entries
507
+ const originalActData = loadActDataFromStorage(
508
+ storage,
509
+ createResult.actReference,
510
+ )
511
+ const originalEntries = collectActEntriesFromJson(originalActData)
512
+ expect(originalEntries.length).toBe(3)
513
+
514
+ // Mock download to return the uploaded blobs
515
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
516
+
517
+ // Revoke grantee2
518
+ const result = await revokeGranteesFromAct(
519
+ context,
520
+ createResult.historyReference,
521
+ createResult.encryptedReference,
522
+ publisher.privateKey,
523
+ [grantee2.publicKey],
524
+ )
525
+
526
+ // Should have new encrypted reference (key rotation)
527
+ expect(result.encryptedReference).not.toBe(createResult.encryptedReference)
528
+ expect(result.actReference).toBeDefined()
529
+
530
+ // New ACT should have 2 entries (publisher + grantee1)
531
+ const newActData = loadActDataFromStorage(storage, result.actReference)
532
+ const newEntries = collectActEntriesFromJson(newActData)
533
+ expect(newEntries.length).toBe(2)
534
+
535
+ // Verify grantee list has 1 grantee
536
+ const newGranteeListBlob = storage.get(result.granteeListReference)!
537
+ const grantees = decryptAndDeserializeGranteeList(
538
+ newGranteeListBlob,
539
+ publisher.privateKey,
540
+ )
541
+ expect(grantees.length).toBe(1)
542
+
543
+ // Grantee2 should NOT be able to find their entry in new ACT
544
+ const grantee2Keys = deriveKeys(
545
+ grantee2.privateKey,
546
+ publisher.publicKey.x,
547
+ publisher.publicKey.y,
548
+ )
549
+ const grantee2Entry = findActEntryByKey(newActData, grantee2Keys.lookupKey)
550
+ expect(grantee2Entry).toBeUndefined()
551
+
552
+ // Grantee1 should still be able to find their entry
553
+ const grantee1Keys = deriveKeys(
554
+ grantee1.privateKey,
555
+ publisher.publicKey.x,
556
+ publisher.publicKey.y,
557
+ )
558
+ const grantee1Entry = findActEntryByKey(newActData, grantee1Keys.lookupKey)
559
+ expect(grantee1Entry).toBeDefined()
560
+ })
561
+ })
562
+
563
+ describe("getGranteesFromAct", () => {
564
+ beforeEach(() => {
565
+ vi.clearAllMocks()
566
+ })
567
+
568
+ it("should return list of grantees as compressed hex strings", async () => {
569
+ const bee = {} as Bee
570
+ const publisher = createTestKeyPair(90)
571
+ const grantee1 = createTestKeyPair(91)
572
+ const grantee2 = createTestKeyPair(92)
573
+
574
+ // Use content-addressed storage mock
575
+ const { uploadMock, downloadMock } = createContentAddressedUploadMock()
576
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
577
+
578
+ const context = createMockContext()
579
+ const createResult = await createActForContent(
580
+ context,
581
+ randomBytes(32),
582
+ publisher.privateKey,
583
+ [grantee1.publicKey, grantee2.publicKey],
584
+ )
585
+
586
+ // Mock download to return the uploaded blobs
587
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
588
+
589
+ // Get grantees
590
+ const grantees = await getGranteesFromAct(
591
+ bee,
592
+ createResult.historyReference,
593
+ publisher.privateKey,
594
+ )
595
+
596
+ expect(grantees.length).toBe(2)
597
+ expect(grantees).toContain(grantee1.compressedPublicKey)
598
+ expect(grantees).toContain(grantee2.compressedPublicKey)
599
+ })
600
+
601
+ it("should return empty array for ACT with no grantees", async () => {
602
+ const bee = {} as Bee
603
+ const publisher = createTestKeyPair(100)
604
+
605
+ // Use content-addressed storage mock
606
+ const { uploadMock, downloadMock } = createContentAddressedUploadMock()
607
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
608
+
609
+ const context = createMockContext()
610
+ const createResult = await createActForContent(
611
+ context,
612
+ randomBytes(32),
613
+ publisher.privateKey,
614
+ [],
615
+ )
616
+
617
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
618
+
619
+ const grantees = await getGranteesFromAct(
620
+ bee,
621
+ createResult.historyReference,
622
+ publisher.privateKey,
623
+ )
624
+
625
+ expect(grantees.length).toBe(0)
626
+ })
627
+ })
628
+
629
+ describe("ACT end-to-end flow", () => {
630
+ beforeEach(() => {
631
+ vi.clearAllMocks()
632
+ })
633
+
634
+ it("should support full upload/download/manage lifecycle", async () => {
635
+ const bee = {} as Bee
636
+ const context = createMockContext()
637
+
638
+ // Setup participants
639
+ const publisher = createTestKeyPair(110)
640
+ const alice = createTestKeyPair(111)
641
+ const bob = createTestKeyPair(112)
642
+ const charlie = createTestKeyPair(113) // Will be added later
643
+ const eve = createTestKeyPair(114) // Unauthorized
644
+
645
+ const secretData = new TextEncoder().encode("Top secret message!")
646
+
647
+ // Use content-addressed storage mock
648
+ const { uploadMock, downloadMock } = createContentAddressedUploadMock()
649
+ vi.mocked(uploadEncryptedDataWithSigning).mockImplementation(uploadMock)
650
+
651
+ // Step 1: Publisher creates ACT with Alice and Bob
652
+ const createResult = await createActForContent(
653
+ context,
654
+ secretData,
655
+ publisher.privateKey,
656
+ [alice.publicKey, bob.publicKey],
657
+ )
658
+
659
+ // Step 2: Verify Alice can decrypt
660
+ vi.mocked(downloadDataWithChunkAPI).mockImplementation(downloadMock)
661
+
662
+ const aliceDecrypted = await decryptActReference(
663
+ bee,
664
+ createResult.encryptedReference,
665
+ createResult.historyReference,
666
+ createResult.publisherPubKey,
667
+ alice.privateKey,
668
+ )
669
+ expect(fromHex(aliceDecrypted).slice(0, secretData.length)).toEqual(
670
+ secretData,
671
+ )
672
+
673
+ // Step 3: Verify Eve cannot decrypt
674
+ await expect(
675
+ decryptActReference(
676
+ bee,
677
+ createResult.encryptedReference,
678
+ createResult.historyReference,
679
+ createResult.publisherPubKey,
680
+ eve.privateKey,
681
+ ),
682
+ ).rejects.toThrow("Access denied")
683
+
684
+ // Step 4: Publisher adds Charlie
685
+ const addResult = await addGranteesToAct(
686
+ context,
687
+ createResult.historyReference,
688
+ publisher.privateKey,
689
+ [charlie.publicKey],
690
+ )
691
+
692
+ // Charlie should now be able to decrypt (using same encrypted ref)
693
+ const charlieDecrypted = await decryptActReference(
694
+ bee,
695
+ createResult.encryptedReference,
696
+ addResult.historyReference,
697
+ createResult.publisherPubKey,
698
+ charlie.privateKey,
699
+ )
700
+ expect(fromHex(charlieDecrypted).slice(0, secretData.length)).toEqual(
701
+ secretData,
702
+ )
703
+
704
+ // Step 5: Publisher revokes Bob
705
+ const revokeResult = await revokeGranteesFromAct(
706
+ context,
707
+ addResult.historyReference,
708
+ createResult.encryptedReference,
709
+ publisher.privateKey,
710
+ [bob.publicKey],
711
+ )
712
+
713
+ // Bob should NOT be able to decrypt the new ACT
714
+ await expect(
715
+ decryptActReference(
716
+ bee,
717
+ revokeResult.encryptedReference,
718
+ revokeResult.historyReference,
719
+ createResult.publisherPubKey,
720
+ bob.privateKey,
721
+ ),
722
+ ).rejects.toThrow("Access denied")
723
+
724
+ // Alice and Charlie should still be able to decrypt
725
+ const aliceStillDecrypted = await decryptActReference(
726
+ bee,
727
+ revokeResult.encryptedReference,
728
+ revokeResult.historyReference,
729
+ createResult.publisherPubKey,
730
+ alice.privateKey,
731
+ )
732
+ expect(fromHex(aliceStillDecrypted).slice(0, secretData.length)).toEqual(
733
+ secretData,
734
+ )
735
+
736
+ // Step 6: Verify grantee list
737
+ const finalGrantees = await getGranteesFromAct(
738
+ bee,
739
+ revokeResult.historyReference,
740
+ publisher.privateKey,
741
+ )
742
+
743
+ expect(finalGrantees.length).toBe(2)
744
+ expect(finalGrantees).toContain(alice.compressedPublicKey)
745
+ expect(finalGrantees).toContain(charlie.compressedPublicKey)
746
+ expect(finalGrantees).not.toContain(bob.compressedPublicKey)
747
+ })
748
+ })