-
Notifications
You must be signed in to change notification settings - Fork 168
Expand file tree
/
Copy pathapi.js
More file actions
281 lines (255 loc) · 8.33 KB
/
api.js
File metadata and controls
281 lines (255 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import { getMagicUserToken, logoutMagicSession } from './magic'
import constants from './constants'
import { NFTStorage } from 'nft.storage'
const API = constants.API
const REQUEST_TOKEN_STORAGE_KEY = 'client-request-token'
/**
* TODO(maybe): define a "common types" package, so we can share definitions with the api?
*
* @typedef {object} APITokenInfo an object describing an nft.storage API token
* @property {number} id
* @property {string} name
* @property {string} secret
* @property {number} user_id
* @property {string} inserted_at
* @property {string} updated_at
* @property {string} [deleted_at]
*
* @typedef {'queued' | 'pinning' | 'pinned' | 'failed'} PinStatus
* @typedef {object} Pin an object describing a remote "pin" of an NFT.
* @property {string} cid
* @property {PinStatus} status
* @property {string} created
* @property {string} [name]
* @property {number} [size]
* @property {Record<string, string>} [meta]
*
* @typedef {'queued' | 'active' | 'published' | 'terminated'} DealStatus
* @typedef {object} Deal an object describing a Filecoin deal
* @property {DealStatus} status
* @property {string} datamodelSelector
* @property {string} pieceCid
* @property {string} batchRootCid
* @property {string} [lastChanged]
* @property {number} [chainDealID]
* @property {string} [statusText]
* @property {string} [dealActivation]
* @property {string} [dealExpiration]
* @property {string} [miner]
*
* @typedef {object} NFTResponse an object describing an uploaded NFT, including pinning and deal info
* @property {string} cid - content identifier for the NFT data
* @property {string} type - either "directory" or the value of Blob.type (mime type)
* @property {Array<{ name?: string, type?: string }>} files - files in the directory (only if this NFT is a directory).
* @property {string} [name] - optional name of the file(s) uploaded as NFT.
* @property {string} scope - name of the JWT token used to create this NFT.
* @property {string} created - date this NFT was created in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format: YYYY-MM-DDTHH:MM:SSZ.
* @property {number} size
* @property {Pin} pin
* @property {Deal[]} deals
*
*
* @typedef {object} VersionInfo an object with version info for the nft.storage service
* @property {string} version - semver version number
* @property {string} commit - git commit hash
* @property {string} branch - git branch name
* @property {string} mode - maintenance mode state
*
* @typedef {object} StatsData an object with global stats about the nft.storage service
* @property {number} deals_total
* @property {number} deals_size_total
* @property {number} uploads_past_7_total
* @property {number} uploads_blob_total
* @property {number} uploads_car_total
* @property {number} uploads_nft_total
* @property {number} uploads_remote_total
* @property {number} uploads_multipart_total
*/
export async function logoutUserSession() {
deleteSavedRequestToken()
return logoutMagicSession()
}
/**
* @returns {Promise<NFTStorage>} an NFTStorage client instance, authenticated with the current user's auth token.
*/
export async function getStorageClient() {
const token = await getClientRequestToken()
if (!token) {
throw new Error(
`can't get storage client: not logged in / no request token available`
)
}
return new NFTStorage({
token,
endpoint: new URL(API + '/'),
})
}
export async function getClientRequestToken() {
let token = tokenFromLocationHash()
if (token) {
saveRequestToken(token)
window.location.hash = ''
return token
}
token = getSavedRequestToken()
if (token) {
return token
}
return getMagicUserToken()
}
/**
* @param {string} token
*/
function saveRequestToken(token) {
localStorage.setItem(REQUEST_TOKEN_STORAGE_KEY, token)
}
/**
* @returns {string|null}
*/
function getSavedRequestToken() {
return localStorage.getItem(REQUEST_TOKEN_STORAGE_KEY)
}
function deleteSavedRequestToken() {
localStorage.removeItem(REQUEST_TOKEN_STORAGE_KEY)
}
function tokenFromLocationHash() {
const fragment = window.location.hash.replace(/^#/, '')
const params = new URLSearchParams(fragment)
return params.get('authToken')
}
/**
* Get a list of objects describing the user's API tokens.
*
* @returns {Promise<APITokenInfo[]>} (async) a list of APITokenInfo objects for each of the user's API tokens
*/
export async function getTokens() {
return (await fetchAuthenticated('/internal/tokens')).value
}
/**
* Delete one of the user's API tokens with the given name
*
* @param {string} name
*/
export async function deleteToken(name) {
return fetchAuthenticated('/internal/tokens', {
method: 'DELETE',
body: JSON.stringify({ id: name }),
})
}
/**
* Create an API token with the given name.
*
* @param {string} name
*/
export async function createToken(name) {
return fetchAuthenticated('/internal/tokens', {
method: 'POST',
body: JSON.stringify({ name }),
})
}
/**
* Get a list of the user's stored NFTs.
*
* @param {object} query
* @param {number} query.limit - maximum number of NFTs to return
* @param {string} query.before - only return NFTs uploaded before this date (ISO-8601 datetime string)
*
* @returns {Promise<NFTResponse[]>}
*/
export async function getNfts({ limit, before }) {
const params = new URLSearchParams({ before, limit: String(limit) })
const result = await fetchAuthenticated(`/?${params}`)
return result.value.filter(Boolean)
}
/**
* Get the set of tags applied to this user account.
*
* See `packages/api/src/routes/user-tags.js` for tag definitions.
*
* @returns {Promise<Record<string, boolean>>} (async) object whose keys are tag names, with boolean values for tag state.
*/
export async function getUserTags() {
return (await fetchAuthenticated('/user/tags')).value
}
/**
* @returns {Promise<VersionInfo>} (async) version information for API service
*/
export async function getVersion() {
// the '/version' route doesn't wrap its response in `{ ok, value }` like `fetchRoute` expects
const res = await fetch(API + '/version')
if (!res.ok) {
throw new Error(`HTTP error: [${res.status}] ${res.statusText}`)
}
return res.json()
}
/**
* @returns {Promise<StatsData>} (async) global service stats
*/
export async function getStats() {
// @ts-expect-error the stats route is an odd duck... it returns `{ ok, data }` instead of `{ ok, value }`
return (await fetchRoute('/stats')).data
}
export async function getUserMetadata() {
const token = await getClientRequestToken()
if (!token) {
return null
}
return (await fetchAuthenticated('/user/meta')).value
}
/**
* Sends a `fetch` request to an API route, using the current user's authentiation token.
*
* See {@link fetchRoute}.
*
* @param {string} route api route (path + query portion of URL)
* @param {Record<string, any>} fetchOptions options to pass through to `fetch`
* @returns {Promise<{ok: boolean, value: any}>} JSON response body.
*/
async function fetchAuthenticated(route, fetchOptions = {}) {
fetchOptions.headers = {
...fetchOptions.headers,
Authorization: 'Bearer ' + (await getClientRequestToken()), // TODO: handle null here
}
return fetchRoute(route, fetchOptions)
}
/**
* Sends a `fetch` request to an API route and unpacks the JSON response body.
*
* Note that it does not unpack the `.value` field from the body, so
* you get a response like: `{"ok": true, "value": "thing you care about"}`
*
* Defaults to GET requests, but you can pass in whatever `method` you want to the `fetchOptions` param.
*
* @param {string} route
* @param {Record<string, any>} fetchOptions
* @returns {Promise<{ok: boolean, value: any}>} JSON response body.
*/
async function fetchRoute(route, fetchOptions = {}) {
if (!route.startsWith('/')) {
route = '/' + route
}
const url = API + route
const defaultHeaders = {
'Content-Type': 'application/json',
}
const options = {
method: 'GET',
...fetchOptions,
headers: { ...defaultHeaders, ...fetchOptions.headers },
}
const res = await fetch(url, options)
if (!res.ok) {
throw new Error(`HTTP error: [${res.status}] ${res.statusText}`)
}
const body = await res.json()
if (body.ok) {
return body
} else {
if (body.error && body.error.message) {
throw new Error(body.error.message)
}
throw new Error(
`unexpected response: ok != true, but body is missing error message`
)
}
}