@bsv/sdk 1.4.18 → 1.4.20
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/dist/cjs/package.json +1 -1
- package/dist/cjs/src/storage/StorageUploader.js +122 -14
- package/dist/cjs/src/storage/StorageUploader.js.map +1 -1
- package/dist/cjs/src/storage/__test/StorageUploader.test.js +85 -14
- package/dist/cjs/src/storage/__test/StorageUploader.test.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/index.js +3 -1
- package/dist/cjs/src/wallet/substrates/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/storage/StorageUploader.js +119 -14
- package/dist/esm/src/storage/StorageUploader.js.map +1 -1
- package/dist/esm/src/storage/__test/StorageUploader.test.js +85 -14
- package/dist/esm/src/storage/__test/StorageUploader.test.js.map +1 -1
- package/dist/esm/src/wallet/substrates/index.js +1 -0
- package/dist/esm/src/wallet/substrates/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/storage/StorageUploader.d.ts +77 -14
- package/dist/types/src/storage/StorageUploader.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/index.d.ts +1 -0
- package/dist/types/src/wallet/substrates/index.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/kvstore.md +3 -3
- package/docs/storage.md +117 -7
- package/package.json +1 -1
- package/src/storage/StorageUploader.ts +156 -14
- package/src/storage/__test/StorageUploader.test.ts +134 -15
- package/src/wallet/substrates/index.ts +1 -0
package/docs/kvstore.md
CHANGED
|
@@ -14,7 +14,7 @@ Allows setting, getting, and removing key-value pairs, with optional encryption.
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
export default class LocalKVStore {
|
|
17
|
-
constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore
|
|
17
|
+
constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string)
|
|
18
18
|
async get(key: string, defaultValue: string | undefined = undefined): Promise<string | undefined>
|
|
19
19
|
async set(key: string, value: string): Promise<OutpointString>
|
|
20
20
|
async remove(key: string): Promise<string[]>
|
|
@@ -28,7 +28,7 @@ See also: [OutpointString](./wallet.md#type-outpointstring), [WalletClient](./wa
|
|
|
28
28
|
Creates an instance of the localKVStore.
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
|
-
constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore
|
|
31
|
+
constructor(wallet: WalletInterface = new WalletClient(), context = "kvstore default", encrypt = true, originator?: string)
|
|
32
32
|
```
|
|
33
33
|
See also: [WalletClient](./wallet.md#class-walletclient), [WalletInterface](./wallet.md#interface-walletinterface), [encrypt](./messages.md#variable-encrypt)
|
|
34
34
|
|
|
@@ -37,7 +37,7 @@ Argument Details
|
|
|
37
37
|
+ **wallet**
|
|
38
38
|
+ The wallet interface to use. Defaults to a new WalletClient instance.
|
|
39
39
|
+ **context**
|
|
40
|
-
+ The context (basket) for namespacing keys. Defaults to 'kvstore
|
|
40
|
+
+ The context (basket) for namespacing keys. Defaults to 'kvstore default'.
|
|
41
41
|
+ **encrypt**
|
|
42
42
|
+ Whether to encrypt values. Defaults to true.
|
|
43
43
|
+ **originator**
|
package/docs/storage.md
CHANGED
|
@@ -8,6 +8,8 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
8
8
|
| --- |
|
|
9
9
|
| [DownloadResult](#interface-downloadresult) |
|
|
10
10
|
| [DownloaderConfig](#interface-downloaderconfig) |
|
|
11
|
+
| [FindFileData](#interface-findfiledata) |
|
|
12
|
+
| [RenewFileResult](#interface-renewfileresult) |
|
|
11
13
|
| [UploadFileResult](#interface-uploadfileresult) |
|
|
12
14
|
| [UploadableFile](#interface-uploadablefile) |
|
|
13
15
|
| [UploaderConfig](#interface-uploaderconfig) |
|
|
@@ -38,6 +40,34 @@ export interface DownloaderConfig {
|
|
|
38
40
|
|
|
39
41
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
40
42
|
|
|
43
|
+
---
|
|
44
|
+
### Interface: FindFileData
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export interface FindFileData {
|
|
48
|
+
name: string;
|
|
49
|
+
size: string;
|
|
50
|
+
mimeType: string;
|
|
51
|
+
expiryTime: number;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
### Interface: RenewFileResult
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export interface RenewFileResult {
|
|
62
|
+
status: string;
|
|
63
|
+
prevExpiryTime?: number;
|
|
64
|
+
newExpiryTime?: number;
|
|
65
|
+
amount?: number;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
70
|
+
|
|
41
71
|
---
|
|
42
72
|
### Interface: UploadFileResult
|
|
43
73
|
|
|
@@ -107,6 +137,12 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
107
137
|
---
|
|
108
138
|
### Class: StorageUploader
|
|
109
139
|
|
|
140
|
+
The StorageUploader class provides client-side methods for:
|
|
141
|
+
- Uploading files with a specified retention period
|
|
142
|
+
- Finding file metadata by UHRP URL
|
|
143
|
+
- Listing all user uploads
|
|
144
|
+
- Renewing an existing advertisement's expiry time
|
|
145
|
+
|
|
110
146
|
```ts
|
|
111
147
|
export class StorageUploader {
|
|
112
148
|
constructor(config: UploaderConfig)
|
|
@@ -114,10 +150,65 @@ export class StorageUploader {
|
|
|
114
150
|
file: UploadableFile;
|
|
115
151
|
retentionPeriod: number;
|
|
116
152
|
}): Promise<UploadFileResult>
|
|
153
|
+
public async findFile(uhrpUrl: string): Promise<FindFileData>
|
|
154
|
+
public async listUploads(): Promise<any>
|
|
155
|
+
public async renewFile(uhrpUrl: string, additionalMinutes: number): Promise<RenewFileResult>
|
|
117
156
|
}
|
|
118
157
|
```
|
|
119
158
|
|
|
120
|
-
See also: [UploadFileResult](./storage.md#interface-uploadfileresult), [UploadableFile](./storage.md#interface-uploadablefile), [UploaderConfig](./storage.md#interface-uploaderconfig)
|
|
159
|
+
See also: [FindFileData](./storage.md#interface-findfiledata), [RenewFileResult](./storage.md#interface-renewfileresult), [UploadFileResult](./storage.md#interface-uploadfileresult), [UploadableFile](./storage.md#interface-uploadablefile), [UploaderConfig](./storage.md#interface-uploaderconfig)
|
|
160
|
+
|
|
161
|
+
#### Constructor
|
|
162
|
+
|
|
163
|
+
Creates a new StorageUploader instance.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
constructor(config: UploaderConfig)
|
|
167
|
+
```
|
|
168
|
+
See also: [UploaderConfig](./storage.md#interface-uploaderconfig)
|
|
169
|
+
|
|
170
|
+
Argument Details
|
|
171
|
+
|
|
172
|
+
+ **config**
|
|
173
|
+
+ An object containing the storage server's URL and a wallet interface
|
|
174
|
+
|
|
175
|
+
#### Method findFile
|
|
176
|
+
|
|
177
|
+
Retrieves metadata for a file matching the given UHRP URL from the `/find` route.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
public async findFile(uhrpUrl: string): Promise<FindFileData>
|
|
181
|
+
```
|
|
182
|
+
See also: [FindFileData](./storage.md#interface-findfiledata)
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
|
|
186
|
+
An object with file name, size, MIME type, and expiry time
|
|
187
|
+
|
|
188
|
+
Argument Details
|
|
189
|
+
|
|
190
|
+
+ **uhrpUrl**
|
|
191
|
+
+ The UHRP URL, e.g. "uhrp://abcd..."
|
|
192
|
+
|
|
193
|
+
Throws
|
|
194
|
+
|
|
195
|
+
If the server or the route returns an error
|
|
196
|
+
|
|
197
|
+
#### Method listUploads
|
|
198
|
+
|
|
199
|
+
Lists all advertisements belonging to the user from the `/list` route.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
public async listUploads(): Promise<any>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
|
|
207
|
+
The array of uploads returned by the server
|
|
208
|
+
|
|
209
|
+
Throws
|
|
210
|
+
|
|
211
|
+
If the server or the route returns an error
|
|
121
212
|
|
|
122
213
|
#### Method publishFile
|
|
123
214
|
|
|
@@ -138,18 +229,37 @@ See also: [UploadFileResult](./storage.md#interface-uploadfileresult), [Uploadab
|
|
|
138
229
|
|
|
139
230
|
Returns
|
|
140
231
|
|
|
141
|
-
An object
|
|
232
|
+
An object with the file's UHRP URL
|
|
233
|
+
|
|
234
|
+
Throws
|
|
235
|
+
|
|
236
|
+
If the server or upload step returns a non-OK response
|
|
237
|
+
|
|
238
|
+
#### Method renewFile
|
|
239
|
+
|
|
240
|
+
Renews the hosting time for an existing file advertisement identified by uhrpUrl.
|
|
241
|
+
Calls the `/renew` route to add `additionalMinutes` to the GCS customTime
|
|
242
|
+
and re-mint the advertisement token on-chain.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
public async renewFile(uhrpUrl: string, additionalMinutes: number): Promise<RenewFileResult>
|
|
246
|
+
```
|
|
247
|
+
See also: [RenewFileResult](./storage.md#interface-renewfileresult)
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
|
|
251
|
+
An object with the new and previous expiry times, plus any cost
|
|
142
252
|
|
|
143
253
|
Argument Details
|
|
144
254
|
|
|
145
|
-
+ **
|
|
146
|
-
+
|
|
147
|
-
+ **
|
|
148
|
-
+
|
|
255
|
+
+ **uhrpUrl**
|
|
256
|
+
+ The UHRP URL of the file (e.g., "uhrp://abcd1234...")
|
|
257
|
+
+ **additionalMinutes**
|
|
258
|
+
+ The number of minutes to extend
|
|
149
259
|
|
|
150
260
|
Throws
|
|
151
261
|
|
|
152
|
-
If
|
|
262
|
+
If the request fails or the server returns an error
|
|
153
263
|
|
|
154
264
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
155
265
|
|
package/package.json
CHANGED
|
@@ -17,15 +17,48 @@ export interface UploadFileResult {
|
|
|
17
17
|
uhrpURL: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface FindFileData {
|
|
21
|
+
name: string
|
|
22
|
+
size: string
|
|
23
|
+
mimeType: string
|
|
24
|
+
expiryTime: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RenewFileResult {
|
|
28
|
+
status: string
|
|
29
|
+
prevExpiryTime?: number
|
|
30
|
+
newExpiryTime?: number
|
|
31
|
+
amount?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The StorageUploader class provides client-side methods for:
|
|
36
|
+
* - Uploading files with a specified retention period
|
|
37
|
+
* - Finding file metadata by UHRP URL
|
|
38
|
+
* - Listing all user uploads
|
|
39
|
+
* - Renewing an existing advertisement's expiry time
|
|
40
|
+
*/
|
|
20
41
|
export class StorageUploader {
|
|
21
42
|
private readonly authFetch: AuthFetch
|
|
22
43
|
private readonly baseURL: string
|
|
23
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new StorageUploader instance.
|
|
47
|
+
* @param {UploaderConfig} config - An object containing the storage server's URL and a wallet interface
|
|
48
|
+
*/
|
|
24
49
|
constructor (config: UploaderConfig) {
|
|
25
50
|
this.baseURL = config.storageURL
|
|
26
51
|
this.authFetch = new AuthFetch(config.wallet)
|
|
27
52
|
}
|
|
28
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Requests information from the server to upload a file (including presigned URL and headers).
|
|
56
|
+
* @private
|
|
57
|
+
* @param {number} fileSize - The size of the file, in bytes
|
|
58
|
+
* @param {number} retentionPeriod - The desired hosting time, in minutes
|
|
59
|
+
* @returns {Promise<{ uploadURL: string; requiredHeaders: Record<string, string>; amount?: number }>}
|
|
60
|
+
* @throws {Error} If the server returns a non-OK response or an error status
|
|
61
|
+
*/
|
|
29
62
|
private async getUploadInfo (
|
|
30
63
|
fileSize: number,
|
|
31
64
|
retentionPeriod: number
|
|
@@ -61,6 +94,15 @@ export class StorageUploader {
|
|
|
61
94
|
}
|
|
62
95
|
}
|
|
63
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Performs the actual file upload (HTTP PUT) to the presigned URL returned by the server.
|
|
99
|
+
* @private
|
|
100
|
+
* @param {string} uploadURL - The presigned URL where the file is to be uploaded
|
|
101
|
+
* @param {UploadableFile} file - The file to upload, including its raw data and MIME type
|
|
102
|
+
* @param {Record<string, string>} requiredHeaders - Additional headers required by the server (e.g. content-length)
|
|
103
|
+
* @returns {Promise<UploadFileResult>} An object indicating whether publishing was successful and the resulting UHRP URL
|
|
104
|
+
* @throws {Error} If the server returns a non-OK response
|
|
105
|
+
*/
|
|
64
106
|
private async uploadFile (
|
|
65
107
|
uploadURL: string,
|
|
66
108
|
file: UploadableFile,
|
|
@@ -88,20 +130,19 @@ export class StorageUploader {
|
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*/
|
|
133
|
+
* Publishes a file to the storage server with the specified retention period.
|
|
134
|
+
*
|
|
135
|
+
* This will:
|
|
136
|
+
* 1. Request an upload URL from the server.
|
|
137
|
+
* 2. Perform an HTTP PUT to upload the file’s raw bytes.
|
|
138
|
+
* 3. Return a UHRP URL referencing the file once published.
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} params
|
|
141
|
+
* @param {UploadableFile} params.file - The file data + type
|
|
142
|
+
* @param {number} params.retentionPeriod - Number of minutes to host the file
|
|
143
|
+
* @returns {Promise<UploadFileResult>} An object with the file's UHRP URL
|
|
144
|
+
* @throws {Error} If the server or upload step returns a non-OK response
|
|
145
|
+
*/
|
|
105
146
|
public async publishFile (params: {
|
|
106
147
|
file: UploadableFile
|
|
107
148
|
retentionPeriod: number
|
|
@@ -112,4 +153,105 @@ export class StorageUploader {
|
|
|
112
153
|
const { uploadURL, requiredHeaders } = await this.getUploadInfo(fileSize, retentionPeriod)
|
|
113
154
|
return await this.uploadFile(uploadURL, file, requiredHeaders)
|
|
114
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Retrieves metadata for a file matching the given UHRP URL from the `/find` route.
|
|
159
|
+
* @param {string} uhrpUrl - The UHRP URL, e.g. "uhrp://abcd..."
|
|
160
|
+
* @returns {Promise<FindFileData>} An object with file name, size, MIME type, and expiry time
|
|
161
|
+
* @throws {Error} If the server or the route returns an error
|
|
162
|
+
*/
|
|
163
|
+
public async findFile (uhrpUrl: string): Promise<FindFileData> {
|
|
164
|
+
const url = new URL(`${this.baseURL}/find`)
|
|
165
|
+
url.searchParams.set('uhrpUrl', uhrpUrl)
|
|
166
|
+
|
|
167
|
+
const response = await this.authFetch.fetch(url.toString(), {
|
|
168
|
+
method: 'GET'
|
|
169
|
+
})
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(`findFile request failed: HTTP ${response.status}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const data = await response.json() as {
|
|
175
|
+
status: string
|
|
176
|
+
data: { name: string, size: string, mimeType: string, expiryTime: number }
|
|
177
|
+
code?: string
|
|
178
|
+
description?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (data.status === 'error') {
|
|
182
|
+
const errCode = data.code ?? 'unknown-code'
|
|
183
|
+
const errDesc = data.description ?? 'no-description'
|
|
184
|
+
throw new Error(`findFile returned an error: ${errCode} - ${errDesc}`)
|
|
185
|
+
}
|
|
186
|
+
return data.data
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Lists all advertisements belonging to the user from the `/list` route.
|
|
191
|
+
* @returns {Promise<any>} The array of uploads returned by the server
|
|
192
|
+
* @throws {Error} If the server or the route returns an error
|
|
193
|
+
*/
|
|
194
|
+
public async listUploads (): Promise<any> {
|
|
195
|
+
const url = `${this.baseURL}/list`
|
|
196
|
+
const response = await this.authFetch.fetch(url, {
|
|
197
|
+
method: 'GET'
|
|
198
|
+
})
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
throw new Error(`listUploads request failed: HTTP ${response.status}`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const data = await response.json()
|
|
204
|
+
if (data.status === 'error') {
|
|
205
|
+
const errCode = data.code as string ?? 'unknown-code'
|
|
206
|
+
const errDesc = data.description as string ?? 'no-description'
|
|
207
|
+
throw new Error(`listUploads returned an error: ${errCode} - ${errDesc}`)
|
|
208
|
+
}
|
|
209
|
+
return data.uploads
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Renews the hosting time for an existing file advertisement identified by uhrpUrl.
|
|
214
|
+
* Calls the `/renew` route to add `additionalMinutes` to the GCS customTime
|
|
215
|
+
* and re-mint the advertisement token on-chain.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} uhrpUrl - The UHRP URL of the file (e.g., "uhrp://abcd1234...")
|
|
218
|
+
* @param {number} additionalMinutes - The number of minutes to extend
|
|
219
|
+
* @returns {Promise<RenewFileResult>} An object with the new and previous expiry times, plus any cost
|
|
220
|
+
* @throws {Error} If the request fails or the server returns an error
|
|
221
|
+
*/
|
|
222
|
+
public async renewFile (uhrpUrl: string, additionalMinutes: number): Promise<RenewFileResult> {
|
|
223
|
+
const url = `${this.baseURL}/renew`
|
|
224
|
+
const body = { uhrpUrl, additionalMinutes }
|
|
225
|
+
|
|
226
|
+
const response = await this.authFetch.fetch(url, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify(body)
|
|
230
|
+
})
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`renewFile request failed: HTTP ${response.status}`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const data = await response.json() as {
|
|
236
|
+
status: string
|
|
237
|
+
prevExpiryTime?: number
|
|
238
|
+
newExpiryTime?: number
|
|
239
|
+
amount?: number
|
|
240
|
+
code?: string
|
|
241
|
+
description?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (data.status === 'error') {
|
|
245
|
+
const errCode = data.code ?? 'unknown-code'
|
|
246
|
+
const errDesc = data.description ?? 'no-description'
|
|
247
|
+
throw new Error(`renewFile returned an error: ${errCode} - ${errDesc}`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
status: data.status,
|
|
252
|
+
prevExpiryTime: data.prevExpiryTime,
|
|
253
|
+
newExpiryTime: data.newExpiryTime,
|
|
254
|
+
amount: data.amount
|
|
255
|
+
}
|
|
256
|
+
}
|
|
115
257
|
}
|
|
@@ -2,7 +2,9 @@ import { StorageUploader } from '../StorageUploader.js'
|
|
|
2
2
|
import * as StorageUtils from '../StorageUtils.js'
|
|
3
3
|
import WalletClient from '../../wallet/WalletClient.js'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A helper for converting a string to a number[] of UTF-8 bytes
|
|
7
|
+
*/
|
|
6
8
|
function stringToUtf8Array(str: string): number[] {
|
|
7
9
|
return Array.from(new TextEncoder().encode(str))
|
|
8
10
|
}
|
|
@@ -10,16 +12,24 @@ function stringToUtf8Array(str: string): number[] {
|
|
|
10
12
|
describe('StorageUploader Tests', () => {
|
|
11
13
|
let uploader: StorageUploader
|
|
12
14
|
let walletClient: WalletClient
|
|
15
|
+
|
|
16
|
+
// We'll have TWO spies:
|
|
17
|
+
let authFetchSpy: jest.SpiedFunction<typeof global.fetch>
|
|
13
18
|
let globalFetchSpy: jest.SpiedFunction<typeof global.fetch>
|
|
14
19
|
|
|
15
20
|
beforeEach(() => {
|
|
16
21
|
walletClient = new WalletClient('json-api', 'non-admin.com')
|
|
17
|
-
|
|
18
22
|
uploader = new StorageUploader({
|
|
19
23
|
storageURL: 'https://example.test.system',
|
|
20
24
|
wallet: walletClient
|
|
21
25
|
})
|
|
22
26
|
|
|
27
|
+
// 1) Spy on the "authFetch.fetch" calls for /find, /list, /renew
|
|
28
|
+
authFetchSpy = jest
|
|
29
|
+
.spyOn(uploader['authFetch'], 'fetch')
|
|
30
|
+
.mockResolvedValue(new Response(null, { status: 200 }))
|
|
31
|
+
|
|
32
|
+
// 2) Spy on the global "fetch" calls for file upload (uploadFile)
|
|
23
33
|
globalFetchSpy = jest
|
|
24
34
|
.spyOn(global, 'fetch')
|
|
25
35
|
.mockResolvedValue(new Response(null, { status: 200 }))
|
|
@@ -34,47 +44,156 @@ describe('StorageUploader Tests', () => {
|
|
|
34
44
|
|
|
35
45
|
// Mock out getUploadInfo so we can control the returned upload/public URLs
|
|
36
46
|
jest.spyOn(uploader as any, 'getUploadInfo').mockResolvedValue({
|
|
37
|
-
uploadURL: 'https://example-upload.com/put'
|
|
47
|
+
uploadURL: 'https://example-upload.com/put'
|
|
38
48
|
})
|
|
39
49
|
|
|
40
50
|
const result = await uploader.publishFile({
|
|
41
|
-
file: {
|
|
42
|
-
data,
|
|
43
|
-
type: 'text/plain'
|
|
44
|
-
},
|
|
51
|
+
file: { data, type: 'text/plain' },
|
|
45
52
|
retentionPeriod: 7
|
|
46
53
|
})
|
|
47
54
|
|
|
48
|
-
//
|
|
55
|
+
// This direct upload uses global.fetch, not authFetch
|
|
49
56
|
expect(globalFetchSpy).toHaveBeenCalledTimes(1)
|
|
57
|
+
|
|
50
58
|
// Check the result
|
|
51
59
|
expect(StorageUtils.isValidURL(result.uhrpURL)).toBe(true)
|
|
52
60
|
expect(result.published).toBe(true)
|
|
53
61
|
|
|
54
62
|
const url = StorageUtils.getHashFromURL(result.uhrpURL)
|
|
55
|
-
const firstFour = url.slice(0, 4)
|
|
63
|
+
const firstFour = url.slice(0, 4)
|
|
64
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
65
|
+
.join('')
|
|
56
66
|
expect(firstFour).toHaveLength(8)
|
|
57
67
|
})
|
|
58
68
|
|
|
59
69
|
it('should throw if the upload fails with HTTP 500', async () => {
|
|
60
|
-
// Force the fetch to fail
|
|
70
|
+
// Force the direct upload (global fetch) to fail
|
|
61
71
|
globalFetchSpy.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
|
62
72
|
|
|
63
73
|
// Also mock getUploadInfo
|
|
64
74
|
jest.spyOn(uploader as any, 'getUploadInfo').mockResolvedValue({
|
|
65
|
-
uploadURL: 'https://example-upload.com/put'
|
|
75
|
+
uploadURL: 'https://example-upload.com/put'
|
|
66
76
|
})
|
|
67
77
|
|
|
68
78
|
const failingData = stringToUtf8Array('failing data')
|
|
69
79
|
|
|
70
80
|
await expect(
|
|
71
81
|
uploader.publishFile({
|
|
72
|
-
file: {
|
|
73
|
-
data: failingData,
|
|
74
|
-
type: 'text/plain'
|
|
75
|
-
},
|
|
82
|
+
file: { data: failingData, type: 'text/plain' },
|
|
76
83
|
retentionPeriod: 30
|
|
77
84
|
})
|
|
78
85
|
).rejects.toThrow('File upload failed: HTTP 500')
|
|
79
86
|
})
|
|
87
|
+
|
|
88
|
+
it('should find a file and return metadata', async () => {
|
|
89
|
+
// This route goes through authFetch, not global fetch
|
|
90
|
+
authFetchSpy.mockResolvedValueOnce(
|
|
91
|
+
new Response(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
status: 'success',
|
|
94
|
+
data: {
|
|
95
|
+
name: 'cdn/abc123',
|
|
96
|
+
size: '1024',
|
|
97
|
+
mimeType: 'text/plain',
|
|
98
|
+
expiryTime: 123456
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
{ status: 200 }
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const fileData = await uploader.findFile('uhrp://some-hash')
|
|
106
|
+
expect(authFetchSpy).toHaveBeenCalledTimes(1)
|
|
107
|
+
expect(fileData.name).toBe('cdn/abc123')
|
|
108
|
+
expect(fileData.size).toBe('1024')
|
|
109
|
+
expect(fileData.mimeType).toBe('text/plain')
|
|
110
|
+
expect(fileData.expiryTime).toBe(123456)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should throw an error if findFile returns an error status', async () => {
|
|
114
|
+
authFetchSpy.mockResolvedValueOnce(
|
|
115
|
+
new Response(
|
|
116
|
+
JSON.stringify({ status: 'error', code: 'ERR_NOT_FOUND', description: 'File not found' }),
|
|
117
|
+
{ status: 200 }
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
await expect(uploader.findFile('uhrp://unknown-hash'))
|
|
122
|
+
.rejects
|
|
123
|
+
.toThrow('findFile returned an error: ERR_NOT_FOUND - File not found')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should list user uploads successfully', async () => {
|
|
127
|
+
// /list uses authFetch
|
|
128
|
+
const mockUploads = [
|
|
129
|
+
{ uhrpUrl: 'uhrp://hash1', expiryTime: 111111 },
|
|
130
|
+
{ uhrpUrl: 'uhrp://hash2', expiryTime: 222222 }
|
|
131
|
+
]
|
|
132
|
+
authFetchSpy.mockResolvedValueOnce(
|
|
133
|
+
new Response(
|
|
134
|
+
JSON.stringify({ status: 'success', uploads: mockUploads }),
|
|
135
|
+
{ status: 200 }
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const result = await uploader.listUploads()
|
|
140
|
+
expect(authFetchSpy).toHaveBeenCalledTimes(1)
|
|
141
|
+
expect(result).toEqual(mockUploads)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should throw an error if listUploads returns an error', async () => {
|
|
145
|
+
authFetchSpy.mockResolvedValueOnce(
|
|
146
|
+
new Response(
|
|
147
|
+
JSON.stringify({ status: 'error', code: 'ERR_INTERNAL', description: 'Something broke' }),
|
|
148
|
+
{ status: 200 }
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
await expect(uploader.listUploads()).rejects.toThrow(
|
|
153
|
+
'listUploads returned an error: ERR_INTERNAL - Something broke'
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should renew a file and return the new expiry info', async () => {
|
|
158
|
+
// /renew uses authFetch
|
|
159
|
+
authFetchSpy.mockResolvedValueOnce(
|
|
160
|
+
new Response(
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
status: 'success',
|
|
163
|
+
prevExpiryTime: 123,
|
|
164
|
+
newExpiryTime: 456,
|
|
165
|
+
amount: 99
|
|
166
|
+
}),
|
|
167
|
+
{ status: 200 }
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const renewal = await uploader.renewFile('uhrp://some-hash', 30)
|
|
172
|
+
expect(authFetchSpy).toHaveBeenCalledTimes(1)
|
|
173
|
+
expect(renewal.status).toBe('success')
|
|
174
|
+
expect(renewal.prevExpiryTime).toBe(123)
|
|
175
|
+
expect(renewal.newExpiryTime).toBe(456)
|
|
176
|
+
expect(renewal.amount).toBe(99)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should throw an error if renewFile returns error status JSON', async () => {
|
|
180
|
+
authFetchSpy.mockResolvedValueOnce(
|
|
181
|
+
new Response(
|
|
182
|
+
JSON.stringify({ status: 'error', code: 'ERR_CANT_RENEW', description: 'Failed to renew' }),
|
|
183
|
+
{ status: 200 }
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
await expect(uploader.renewFile('uhrp://some-other-hash', 15))
|
|
188
|
+
.rejects
|
|
189
|
+
.toThrow('renewFile returned an error: ERR_CANT_RENEW - Failed to renew')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should throw if renewFile request fails with non-200 status', async () => {
|
|
193
|
+
authFetchSpy.mockResolvedValueOnce(new Response(null, { status: 404 }))
|
|
194
|
+
|
|
195
|
+
await expect(uploader.renewFile('uhrp://ghost', 10))
|
|
196
|
+
.rejects
|
|
197
|
+
.toThrow('renewFile request failed: HTTP 404')
|
|
198
|
+
})
|
|
80
199
|
})
|
|
@@ -5,3 +5,4 @@ export * from './WalletWireCalls.js'
|
|
|
5
5
|
export { default as WalletWireTransceiver } from './WalletWireTransceiver.js'
|
|
6
6
|
export { default as WalletWireProcessor } from './WalletWireProcessor.js'
|
|
7
7
|
export { default as HTTPWalletWire } from './HTTPWalletWire.js'
|
|
8
|
+
export { default as HTTPWalletJSON } from './HTTPWalletJSON.js'
|