@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
package/src/utils/ttl.ts
ADDED
|
@@ -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
|
+
})
|
package/src/utils/url.ts
ADDED
|
@@ -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
|
+
}
|