@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,129 @@
1
+ /**
2
+ * TTL (Time To Live) calculation and formatting utilities for postage stamps
3
+ */
4
+
5
+ /**
6
+ * Gnosis Chain block time in seconds
7
+ */
8
+ export const GNOSIS_BLOCK_TIME = 5
9
+
10
+ /**
11
+ * Time constants
12
+ */
13
+ const SECONDS_PER_MINUTE = 60
14
+ const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
15
+ const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
16
+ const SECONDS_PER_MONTH = 30 * SECONDS_PER_DAY // 2,592,000
17
+
18
+ /**
19
+ * Swarm constants
20
+ */
21
+ const PLUR_PER_BZZ = 1e16
22
+ const CHUNK_SIZE_BYTES = 4096
23
+ const BYTES_PER_GB = 1024 * 1024 * 1024
24
+ const CHUNKS_PER_GB = Math.floor(BYTES_PER_GB / CHUNK_SIZE_BYTES) // 262144
25
+
26
+ /**
27
+ * Swarmscan API URL for price data
28
+ */
29
+ export const SWARMSCAN_STATS_URL =
30
+ "https://api.swarmscan.io/v1/postage-stamps/stats"
31
+
32
+ /**
33
+ * Fetches current price from Swarmscan.
34
+ * @returns pricePerGBPerMonth in BZZ
35
+ */
36
+ export async function fetchSwarmPrice(): Promise<number> {
37
+ const response = await fetch(SWARMSCAN_STATS_URL)
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to fetch Swarmscan stats: ${response.status}`)
40
+ }
41
+ const data = await response.json()
42
+ return data.pricePerGBPerMonth
43
+ }
44
+
45
+ /**
46
+ * Calculates TTL in seconds from stamp amount and Swarmscan price.
47
+ *
48
+ * @param amount - Stamp amount in PLUR (smallest BZZ unit)
49
+ * @param pricePerGBPerMonth - Price from Swarmscan (in BZZ)
50
+ * @returns TTL in seconds
51
+ */
52
+ export function calculateTTLSeconds(
53
+ amount: bigint | number | string,
54
+ pricePerGBPerMonth: number,
55
+ ): number {
56
+ const amountBigInt = BigInt(amount)
57
+ // Cost per chunk per month in PLUR
58
+ const perChunkPerMonthCost =
59
+ (pricePerGBPerMonth * PLUR_PER_BZZ) / CHUNKS_PER_GB
60
+ // TTL in months
61
+ const ttlMonths = Number(amountBigInt) / perChunkPerMonthCost
62
+ // TTL in seconds
63
+ return ttlMonths * SECONDS_PER_MONTH
64
+ }
65
+
66
+ /**
67
+ * Formats a TTL value (in seconds) to a human-readable string.
68
+ * Returns "Xd Yh" format (e.g., "30d 14h").
69
+ *
70
+ * @param ttlSeconds - TTL in seconds
71
+ * @returns Human-readable TTL string, or "N/A" if undefined/invalid
72
+ */
73
+ export function formatTTL(ttlSeconds: number | undefined): string {
74
+ if (ttlSeconds === undefined || ttlSeconds <= 0) {
75
+ return "N/A"
76
+ }
77
+
78
+ const days = Math.floor(ttlSeconds / SECONDS_PER_DAY)
79
+ const hours = Math.floor((ttlSeconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR)
80
+
81
+ return `${days}d ${hours}h`
82
+ }
83
+
84
+ /**
85
+ * Fetches block timestamp from Gnosis RPC.
86
+ *
87
+ * @param rpcUrl - Gnosis RPC URL
88
+ * @param blockNumber - Block number to get timestamp for
89
+ * @returns Block timestamp in seconds (Unix timestamp)
90
+ */
91
+ export async function getBlockTimestamp(
92
+ rpcUrl: string,
93
+ blockNumber: number,
94
+ ): Promise<number> {
95
+ const response = await fetch(rpcUrl, {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ jsonrpc: "2.0",
100
+ method: "eth_getBlockByNumber",
101
+ params: [`0x${blockNumber.toString(16)}`, false],
102
+ id: 1,
103
+ }),
104
+ })
105
+
106
+ const data = await response.json()
107
+ if (data.error) {
108
+ throw new Error(`RPC error: ${data.error.message}`)
109
+ }
110
+ if (!data.result) {
111
+ throw new Error(`Block ${blockNumber} not found`)
112
+ }
113
+
114
+ return parseInt(data.result.timestamp, 16)
115
+ }
116
+
117
+ /**
118
+ * Calculates expiry timestamp for a postage stamp.
119
+ *
120
+ * @param blockTimestamp - Timestamp when stamp was created (from blockNumber)
121
+ * @param ttlSeconds - TTL in seconds
122
+ * @returns Expiry timestamp in seconds (Unix timestamp)
123
+ */
124
+ export function calculateExpiryTimestamp(
125
+ blockTimestamp: number,
126
+ ttlSeconds: number,
127
+ ): number {
128
+ return blockTimestamp + ttlSeconds
129
+ }
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { buildAuthUrl } from "./url"
3
+
4
+ describe("buildAuthUrl", () => {
5
+ it("should build URL with origin only", () => {
6
+ const url = buildAuthUrl(
7
+ "https://swarm-id.example.com",
8
+ "https://myapp.example.com",
9
+ )
10
+
11
+ expect(url).toBe(
12
+ "https://swarm-id.example.com/connect#origin=https%3A%2F%2Fmyapp.example.com",
13
+ )
14
+ })
15
+
16
+ it("should build URL with origin and minimal metadata", () => {
17
+ const url = buildAuthUrl(
18
+ "https://swarm-id.example.com",
19
+ "https://myapp.example.com",
20
+ {
21
+ name: "Test App",
22
+ },
23
+ )
24
+
25
+ expect(url).toBe(
26
+ "https://swarm-id.example.com/connect#origin=https%3A%2F%2Fmyapp.example.com&appName=Test+App",
27
+ )
28
+ })
29
+
30
+ it("should build URL with origin and full metadata", () => {
31
+ const url = buildAuthUrl(
32
+ "https://swarm-id.example.com",
33
+ "https://myapp.example.com",
34
+ {
35
+ name: "Test App",
36
+ description: "A test application",
37
+ icon: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiPjwvc3ZnPg==",
38
+ },
39
+ )
40
+
41
+ const expectedUrl =
42
+ "https://swarm-id.example.com/connect#origin=https%3A%2F%2Fmyapp.example.com&appName=Test+App&appDescription=A+test+application&appIcon=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiPjwvc3ZnPg%3D%3D"
43
+ expect(url).toBe(expectedUrl)
44
+ })
45
+
46
+ it("should properly encode URL parameters", () => {
47
+ const url = buildAuthUrl(
48
+ "https://swarm-id.example.com",
49
+ "https://my-app.example.com/path?query=value",
50
+ {
51
+ name: "My App & Co.",
52
+ description: "An app with special chars: äöü",
53
+ },
54
+ )
55
+
56
+ expect(url).toContain(
57
+ "origin=https%3A%2F%2Fmy-app.example.com%2Fpath%3Fquery%3Dvalue",
58
+ )
59
+ expect(url).toContain("appName=My+App+%26+Co.")
60
+ expect(url).toContain(
61
+ "appDescription=An+app+with+special+chars%3A+%C3%A4%C3%B6%C3%BC",
62
+ )
63
+ })
64
+
65
+ it("should handle empty description and icon", () => {
66
+ const url = buildAuthUrl(
67
+ "https://swarm-id.example.com",
68
+ "https://myapp.example.com",
69
+ {
70
+ name: "Test App",
71
+ description: "",
72
+ icon: "",
73
+ },
74
+ )
75
+
76
+ // URLSearchParams omits empty strings, so only origin and appName should be included
77
+ expect(url).toBe(
78
+ "https://swarm-id.example.com/connect#origin=https%3A%2F%2Fmyapp.example.com&appName=Test+App",
79
+ )
80
+ })
81
+
82
+ it("should include agent parameter when option is true", () => {
83
+ const url = buildAuthUrl(
84
+ "https://swarm-id.example.com",
85
+ "https://myapp.example.com",
86
+ { name: "Test App" },
87
+ { agent: true },
88
+ )
89
+
90
+ expect(url).toContain("agent=")
91
+ })
92
+
93
+ it("should not include agent parameter when option is false", () => {
94
+ const url = buildAuthUrl(
95
+ "https://swarm-id.example.com",
96
+ "https://myapp.example.com",
97
+ { name: "Test App" },
98
+ { agent: false },
99
+ )
100
+
101
+ expect(url).not.toContain("agent")
102
+ })
103
+
104
+ it("should include challenge and agent when both options are set", () => {
105
+ const url = buildAuthUrl(
106
+ "https://swarm-id.example.com",
107
+ "https://myapp.example.com",
108
+ { name: "Test App" },
109
+ { agent: true, challenge: "test-challenge-123" },
110
+ )
111
+
112
+ expect(url).toContain("challenge=test-challenge-123")
113
+ expect(url).toContain("agent=")
114
+ })
115
+
116
+ it("should include challenge when provided", () => {
117
+ const url = buildAuthUrl(
118
+ "https://swarm-id.example.com",
119
+ "https://myapp.example.com",
120
+ { name: "Test App" },
121
+ { challenge: "test-challenge-123" },
122
+ )
123
+
124
+ expect(url).toContain("challenge=test-challenge-123")
125
+ })
126
+
127
+ it("should not include challenge when not provided", () => {
128
+ const url = buildAuthUrl(
129
+ "https://swarm-id.example.com",
130
+ "https://myapp.example.com",
131
+ { name: "Test App" },
132
+ )
133
+
134
+ expect(url).not.toContain("challenge")
135
+ })
136
+ })
@@ -0,0 +1,71 @@
1
+ import type { AppMetadata } from "../types"
2
+
3
+ /**
4
+ * Configuration options for building the authentication URL.
5
+ */
6
+ export interface BuildAuthUrlOptions {
7
+ /**
8
+ * When true, shows the agent sign-up option on the connect page.
9
+ * Agents are automated services that can perform operations on behalf of users.
10
+ */
11
+ agent?: boolean
12
+ /**
13
+ * Challenge string for storage partitioning detection. When present, the popup checks
14
+ * if it can read this challenge from localStorage to determine whether
15
+ * storage is partitioned.
16
+ */
17
+ challenge?: string
18
+ }
19
+
20
+ /**
21
+ * Build the authentication URL for connecting to Swarm ID
22
+ *
23
+ * This function creates the same URL format as used by SwarmIdProxy.openAuthPopup()
24
+ * to ensure consistency across the library.
25
+ *
26
+ * @param baseUrl - The base URL where the authentication page is hosted
27
+ * @param origin - The origin of the parent application requesting authentication
28
+ * @param metadata - Optional application metadata to display during authentication
29
+ * @param options - Optional configuration for the auth URL
30
+ * @returns The complete authentication URL with hash parameters
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const url = buildAuthUrl(
35
+ * "https://swarm-id.example.com",
36
+ * "https://myapp.example.com",
37
+ * { name: "My App", description: "A decentralized application" }
38
+ * )
39
+ * // Returns: "https://swarm-id.example.com/connect#origin=https%3A%2F%2Fmyapp.example.com&appName=My+App&appDescription=A+decentralized+application"
40
+ * ```
41
+ */
42
+ export function buildAuthUrl(
43
+ baseUrl: string,
44
+ origin: string,
45
+ metadata?: AppMetadata,
46
+ options?: BuildAuthUrlOptions,
47
+ ): string {
48
+ // Build URL with hash parameters (avoids re-renders in SPA)
49
+ const params = new URLSearchParams()
50
+ params.set("origin", origin)
51
+
52
+ if (metadata) {
53
+ params.set("appName", metadata.name)
54
+ if (metadata.description) {
55
+ params.set("appDescription", metadata.description)
56
+ }
57
+ if (metadata.icon) {
58
+ params.set("appIcon", metadata.icon)
59
+ }
60
+ }
61
+
62
+ if (options?.challenge) {
63
+ params.set("challenge", options.challenge)
64
+ }
65
+
66
+ if (options?.agent) {
67
+ params.set("agent", "")
68
+ }
69
+
70
+ return `${baseUrl}/connect#${params.toString()}`
71
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Generic Versioned Storage Utility
3
+ *
4
+ * Provides a framework-agnostic way to store and retrieve versioned data
5
+ * with automatic migration support.
6
+ */
7
+
8
+ import { z } from "zod"
9
+
10
+ // ============================================================================
11
+ // Types & Schemas
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Versioned storage wrapper schema
16
+ * Used to check if data is in versioned format
17
+ */
18
+ export const VersionedStorageSchema = z.object({
19
+ version: z.number().int().nonnegative(),
20
+ data: z.unknown(),
21
+ })
22
+
23
+ export type VersionedStorage = z.infer<typeof VersionedStorageSchema>
24
+
25
+ /**
26
+ * Storage adapter interface - allows different storage backends
27
+ */
28
+ export interface StorageAdapter {
29
+ getItem(key: string): string | undefined
30
+ setItem(key: string, value: string): void
31
+ removeItem(key: string): void
32
+ }
33
+
34
+ /**
35
+ * Version parser function - handles migration from one version to another
36
+ */
37
+ export type VersionParser<T> = (data: unknown, version: number) => T[]
38
+
39
+ /**
40
+ * Serializer function - converts data to JSON-serializable format
41
+ */
42
+ export type Serializer<T> = (data: T) => Record<string, unknown>
43
+
44
+ /**
45
+ * Listener function for storage change events
46
+ */
47
+ export type StorageChangeListener<T> = (data: T[]) => void
48
+
49
+ /**
50
+ * Options for versioned storage
51
+ */
52
+ export interface VersionedStorageOptions<T> {
53
+ /** Storage key */
54
+ key: string
55
+
56
+ /** Current version number */
57
+ currentVersion: number
58
+
59
+ /** Storage adapter (e.g., localStorage) */
60
+ storage: StorageAdapter
61
+
62
+ /** Version parsers - map of version to parser function */
63
+ parsers: Record<number, VersionParser<T>>
64
+
65
+ /** Optional serializer for complex types */
66
+ serializer?: Serializer<T>
67
+
68
+ /** Logger name for error messages */
69
+ loggerName?: string
70
+ }
71
+
72
+ // ============================================================================
73
+ // Browser Storage Adapters
74
+ // ============================================================================
75
+
76
+ /**
77
+ * LocalStorage adapter for browser environments
78
+ */
79
+ export class LocalStorageAdapter implements StorageAdapter {
80
+ getItem(key: string): string | undefined {
81
+ if (typeof window === "undefined" || !window.localStorage) {
82
+ return undefined
83
+ }
84
+ return window.localStorage.getItem(key) ?? undefined
85
+ }
86
+
87
+ setItem(key: string, value: string): void {
88
+ if (typeof window === "undefined" || !window.localStorage) {
89
+ return
90
+ }
91
+ window.localStorage.setItem(key, value)
92
+ }
93
+
94
+ removeItem(key: string): void {
95
+ if (typeof window === "undefined" || !window.localStorage) {
96
+ return
97
+ }
98
+ window.localStorage.removeItem(key)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * In-memory storage adapter (for testing or non-browser environments)
104
+ */
105
+ export class MemoryStorageAdapter implements StorageAdapter {
106
+ private storage = new Map<string, string>()
107
+
108
+ getItem(key: string): string | undefined {
109
+ return this.storage.get(key)
110
+ }
111
+
112
+ setItem(key: string, value: string): void {
113
+ this.storage.set(key, value)
114
+ }
115
+
116
+ removeItem(key: string): void {
117
+ this.storage.delete(key)
118
+ }
119
+
120
+ clear(): void {
121
+ this.storage.clear()
122
+ }
123
+ }
124
+
125
+ // ============================================================================
126
+ // Core Versioned Storage Class
127
+ // ============================================================================
128
+
129
+ /**
130
+ * Generic versioned storage manager
131
+ */
132
+ export class VersionedStorageManager<T> {
133
+ private options: VersionedStorageOptions<T>
134
+ private listeners: Set<StorageChangeListener<T>> = new Set()
135
+ private boundStorageHandler: ((event: StorageEvent) => void) | undefined
136
+
137
+ constructor(options: VersionedStorageOptions<T>) {
138
+ this.options = options
139
+ }
140
+
141
+ /**
142
+ * Subscribe to storage change events from other windows/tabs
143
+ * The browser's storage event fires when localStorage changes in OTHER windows,
144
+ * making this useful for cross-window synchronization.
145
+ *
146
+ * @param listener - Callback function that receives the updated data
147
+ * @returns Unsubscribe function to remove the listener
148
+ */
149
+ subscribe(listener: StorageChangeListener<T>): () => void {
150
+ this.listeners.add(listener)
151
+
152
+ // Set up storage event listener on first subscription
153
+ if (!this.boundStorageHandler) {
154
+ this.setupStorageEventListener()
155
+ }
156
+
157
+ // Return unsubscribe function
158
+ return () => {
159
+ this.listeners.delete(listener)
160
+ if (this.listeners.size === 0) {
161
+ this.cleanupStorageEventListener()
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Set up the browser storage event listener
168
+ * Only listens for changes to this manager's specific key
169
+ */
170
+ private setupStorageEventListener(): void {
171
+ if (typeof window === "undefined") return
172
+
173
+ this.boundStorageHandler = (event: StorageEvent) => {
174
+ // Only handle events for our specific key
175
+ if (event.key !== this.options.key) return
176
+
177
+ // Reload data and notify listeners
178
+ const data = this.load()
179
+ this.notifyListeners(data)
180
+ }
181
+
182
+ window.addEventListener("storage", this.boundStorageHandler)
183
+ }
184
+
185
+ /**
186
+ * Clean up the storage event listener when no more subscribers
187
+ */
188
+ private cleanupStorageEventListener(): void {
189
+ if (this.boundStorageHandler && typeof window !== "undefined") {
190
+ window.removeEventListener("storage", this.boundStorageHandler)
191
+ this.boundStorageHandler = undefined
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Notify all listeners of data changes
197
+ */
198
+ private notifyListeners(data: T[]): void {
199
+ for (const listener of this.listeners) {
200
+ try {
201
+ listener(data)
202
+ } catch (e) {
203
+ console.error(
204
+ `[${this.options.loggerName ?? "Storage"}] Listener error:`,
205
+ e,
206
+ )
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Load data from storage
213
+ */
214
+ load(): T[] {
215
+ const stored = this.options.storage.getItem(this.options.key)
216
+
217
+ if (!stored) {
218
+ return []
219
+ }
220
+
221
+ try {
222
+ const parsed: unknown = JSON.parse(stored)
223
+ return this.parse(parsed)
224
+ } catch (e) {
225
+ console.error(`[${this.options.loggerName ?? "Storage"}] Load failed:`, e)
226
+ return []
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Parse versioned data and migrate if needed
232
+ */
233
+ private parse(parsed: unknown): T[] {
234
+ // Try to parse as versioned data
235
+ const versioned = VersionedStorageSchema.safeParse(parsed)
236
+ const version = versioned.success ? versioned.data.version : 0
237
+ const data = versioned.success ? versioned.data.data : parsed
238
+
239
+ // Find appropriate parser
240
+ const parser = this.options.parsers[version]
241
+
242
+ if (!parser) {
243
+ console.error(
244
+ `[${this.options.loggerName ?? "Storage"}] No parser for version ${version}`,
245
+ )
246
+ return []
247
+ }
248
+
249
+ return parser(data, version)
250
+ }
251
+
252
+ /**
253
+ * Save data to storage
254
+ */
255
+ save(data: T[]): void {
256
+ try {
257
+ // Apply serializer if provided
258
+ const serialized = this.options.serializer
259
+ ? data.map(this.options.serializer)
260
+ : data
261
+
262
+ const wrapped: VersionedStorage = {
263
+ version: this.options.currentVersion,
264
+ data: serialized,
265
+ }
266
+
267
+ this.options.storage.setItem(this.options.key, JSON.stringify(wrapped))
268
+ } catch (e) {
269
+ console.error(`[${this.options.loggerName ?? "Storage"}] Save failed:`, e)
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Clear data from storage
275
+ */
276
+ clear(): void {
277
+ this.options.storage.removeItem(this.options.key)
278
+ }
279
+ }
280
+
281
+ // ============================================================================
282
+ // Helper Functions
283
+ // ============================================================================
284
+
285
+ /**
286
+ * Create a versioned storage manager with localStorage
287
+ */
288
+ export function createLocalStorageManager<T>(
289
+ options: Omit<VersionedStorageOptions<T>, "storage">,
290
+ ): VersionedStorageManager<T> {
291
+ return new VersionedStorageManager({
292
+ ...options,
293
+ storage: new LocalStorageAdapter(),
294
+ })
295
+ }
296
+
297
+ /**
298
+ * Create a versioned storage manager with memory storage
299
+ */
300
+ export function createMemoryStorageManager<T>(
301
+ options: Omit<VersionedStorageOptions<T>, "storage">,
302
+ ): VersionedStorageManager<T> {
303
+ return new VersionedStorageManager({
304
+ ...options,
305
+ storage: new MemoryStorageAdapter(),
306
+ })
307
+ }
308
+
309
+ /**
310
+ * Create a simple Zod-based parser for a single version
311
+ */
312
+ export function createZodParser<T>(schema: z.ZodType<T[]>): VersionParser<T> {
313
+ return (data: unknown) => {
314
+ const result = schema.safeParse(data)
315
+
316
+ if (!result.success) {
317
+ console.error("Parse failed:", result.error.format())
318
+ return []
319
+ }
320
+
321
+ return result.data
322
+ }
323
+ }