@unavatar/core 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 +407 -0
- package/bin/index.js +163 -0
- package/bin/unavatar +3 -0
- package/bin/unavatar-dev +3 -0
- package/package.json +192 -0
- package/src/avatar/auto.js +94 -0
- package/src/constant.js +18 -0
- package/src/index.js +51 -0
- package/src/providers/apple-music.js +97 -0
- package/src/providers/bluesky.js +10 -0
- package/src/providers/deviantart.js +8 -0
- package/src/providers/dribbble.js +8 -0
- package/src/providers/duckduckgo.js +6 -0
- package/src/providers/github.js +10 -0
- package/src/providers/gitlab.js +8 -0
- package/src/providers/google.js +6 -0
- package/src/providers/gravatar.js +15 -0
- package/src/providers/index.js +60 -0
- package/src/providers/instagram.js +8 -0
- package/src/providers/microlink.js +15 -0
- package/src/providers/onlyfans.js +20 -0
- package/src/providers/openstreetmap.js +20 -0
- package/src/providers/patreon.js +10 -0
- package/src/providers/reddit.js +9 -0
- package/src/providers/soundcloud.js +17 -0
- package/src/providers/spotify.js +18 -0
- package/src/providers/substack.js +35 -0
- package/src/providers/telegram.js +8 -0
- package/src/providers/tiktok.js +26 -0
- package/src/providers/twitch.js +8 -0
- package/src/providers/vimeo.js +8 -0
- package/src/providers/whatsapp.js +31 -0
- package/src/providers/x.js +35 -0
- package/src/providers/youtube.js +8 -0
- package/src/util/browserless.js +38 -0
- package/src/util/cacheable-lookup.js +22 -0
- package/src/util/error.js +6 -0
- package/src/util/got.js +32 -0
- package/src/util/html-get.js +25 -0
- package/src/util/html-provider.js +190 -0
- package/src/util/http-status.js +17 -0
- package/src/util/is-iterable.js +9 -0
- package/src/util/keyv.js +27 -0
- package/src/util/reachable-url.js +21 -0
- package/src/util/stringify.js +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
- [Quick start](#quick-start)
|
|
4
|
+
- [Query parameters](#query-parameters)
|
|
5
|
+
- [TTL](#ttl)
|
|
6
|
+
- [Fallback](#fallback)
|
|
7
|
+
- [JSON](#json)
|
|
8
|
+
- [Pricing](#pricing)
|
|
9
|
+
- [Providers](#providers)
|
|
10
|
+
- [Apple Music](#apple-music)
|
|
11
|
+
- [Bluesky](#bluesky)
|
|
12
|
+
- [DeviantArt](#deviantart)
|
|
13
|
+
- [Dribbble](#dribbble)
|
|
14
|
+
- [DuckDuckGo](#duckduckgo)
|
|
15
|
+
- [GitHub](#github)
|
|
16
|
+
- [GitLab](#gitlab)
|
|
17
|
+
- [Google](#google)
|
|
18
|
+
- [Gravatar](#gravatar)
|
|
19
|
+
- [Instagram](#instagram)
|
|
20
|
+
- [Microlink](#microlink)
|
|
21
|
+
- [OnlyFans](#onlyfans)
|
|
22
|
+
- [OpenStreetMap](#openstreetmap)
|
|
23
|
+
- [Patreon](#patreon)
|
|
24
|
+
- [Reddit](#reddit)
|
|
25
|
+
- [SoundCloud](#soundcloud)
|
|
26
|
+
- [Spotify](#spotify)
|
|
27
|
+
- [Substack](#substack)
|
|
28
|
+
- [Telegram](#telegram)
|
|
29
|
+
- [TikTok](#tiktok)
|
|
30
|
+
- [Twitch](#twitch)
|
|
31
|
+
- [Vimeo](#vimeo)
|
|
32
|
+
- [WhatsApp](#whatsapp)
|
|
33
|
+
- [X/Twitter](#xtwitter)
|
|
34
|
+
- [YouTube](#youtube)
|
|
35
|
+
- [Response Format](#response-format)
|
|
36
|
+
- [Response Headers](#response-headers)
|
|
37
|
+
- [Response Errors](#response-errors)
|
|
38
|
+
- [Contact](#contact)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Welcome to **unavatar.io**, the ultimate avatar service that offers everything you need to easily retrieve user avatars:
|
|
43
|
+
|
|
44
|
+
- **Versatile**: A wide range of platforms and services including [TikTok](#tiktok), [Instagram](#instagram), [YouTube](#youtube), [X/Twitter](#xtwitter), [Gravatar](#gravatar), etc., meaning you can rule all of them just querying against unavatar.
|
|
45
|
+
|
|
46
|
+
- **Speed**: Designed to be fast and efficient, all requests are being cached and delivered +200 global datacenters, allowing you to consume avatars instantly, counting more than 20 millions requests per month.
|
|
47
|
+
|
|
48
|
+
- **Optimize**: All the images are not only compressed on-the-fly to reduce their size and save bandwith, but also optimized to maintain a high-quality ratio. They are ready for immediate use, enhancing the overall optimization of your website or application.
|
|
49
|
+
|
|
50
|
+
- **Integration**: The service seamlessly incorporates into your current applications or websites with ease. We offer straightforward documentation and comprehensive support to ensure a quick and effortless onboarding experience.
|
|
51
|
+
|
|
52
|
+
It's proudly powered by [microlink.io](https://microlink.io), the headless browser API that handles all the heavy lifting behind the scenes to ensure your avatars are always ready.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
The service is exposed in **unavatar.io** via provider endpoints:
|
|
57
|
+
|
|
58
|
+
- an **email**: [unavatar.io/gravatar/sindresorhus@gmail.com](https://unavatar.io/gravatar/sindresorhus@gmail.com)
|
|
59
|
+
- an **username**: [unavatar.io/github/kikobeats](https://unavatar.io/github/kikobeats)
|
|
60
|
+
- a **domain**: [unavatar.io/google/reddit.com](https://unavatar.io/google/reddit.com)
|
|
61
|
+
|
|
62
|
+
Use the `/:provider/:key` format for all lookups. You can read more about available providers in [providers](#providers).
|
|
63
|
+
|
|
64
|
+
## Query parameters
|
|
65
|
+
|
|
66
|
+
### TTL
|
|
67
|
+
|
|
68
|
+
Type: `number`|`string`<br/>
|
|
69
|
+
Default: `'24h'`<br/>
|
|
70
|
+
Range: from `'1h'` to `'28d'`
|
|
71
|
+
|
|
72
|
+
It determines the maximum quantity of time an avatar is considered fresh.
|
|
73
|
+
|
|
74
|
+
e.g., [unavatar.io/github/kikobeats?ttl=1h](https://unavatar.io/github/kikobeats?ttl=1h)
|
|
75
|
+
|
|
76
|
+
When you look up for a user avatar for the very first time, the service will determine it and cache it respecting TTL value.
|
|
77
|
+
|
|
78
|
+
The same resource will continue to be used until reach TTL expiration. After that, the resource will be computed, and cache as fresh, starting the cycle.
|
|
79
|
+
|
|
80
|
+
### Fallback
|
|
81
|
+
|
|
82
|
+
Type: `string`|`boolean`
|
|
83
|
+
|
|
84
|
+
When it can't be possible to get a user avatar, a fallback image is returned instead, and it can be personalized to fit better with your website or application style.
|
|
85
|
+
|
|
86
|
+
You can get one from **boringavatars.com**:
|
|
87
|
+
|
|
88
|
+
e.g., [unavatar.io/github/37t?fallback=https://source.boringavatars.com/marble/120/1337_user?colors=264653r,2a9d8f,e9c46a,f4a261,e76f51](https://unavatar.io/github/37t?fallback=https://source.boringavatars.com/marble/120/1337_user?colors=264653r,2a9d8f,e9c46a,f4a261,e76f51)
|
|
89
|
+
|
|
90
|
+
or **avatar.vercel.sh**:
|
|
91
|
+
|
|
92
|
+
e.g., [unavatar.io/github/37t?fallback=https://avatar.vercel.sh/37t?size=400](https://unavatar.io/github/37t?fallback=https://avatar.vercel.sh/37t?size=400)
|
|
93
|
+
|
|
94
|
+
or a static image:
|
|
95
|
+
|
|
96
|
+
e.g., [unavatar.io/github/37t?fallback=https://avatars.githubusercontent.com/u/66378906?v=4](https://unavatar.io/github/37t?fallback=https://avatars.githubusercontent.com/u/66378906?v=4)
|
|
97
|
+
|
|
98
|
+
or even a base64 encoded image. This allows you to return a transparent, base64 encoded 1x1 pixel GIF, which can be useful when you want to use your own background colour or image as a fallback.
|
|
99
|
+
|
|
100
|
+
e.g., [unavatar.io/github/37t?fallback=data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==](https://unavatar.io/github/37t?fallback=data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)
|
|
101
|
+
|
|
102
|
+
You can pass `fallback=false` to explicitly disable this behavior. In this case, a *404 Not Found* HTTP status code will returned when is not possible to get the user avatar.
|
|
103
|
+
|
|
104
|
+
### JSON
|
|
105
|
+
|
|
106
|
+
The service returns media content by default.
|
|
107
|
+
|
|
108
|
+
This is in this way to make easier consume the service from HTML markup.
|
|
109
|
+
|
|
110
|
+
In case you want to get a JSON payload as response, just pass `json=true`:
|
|
111
|
+
|
|
112
|
+
e.g., [unavatar.io/github/kikobeats?json](https://unavatar.io/github/kikobeats?json)
|
|
113
|
+
|
|
114
|
+
## Pricing
|
|
115
|
+
|
|
116
|
+
The service is **FREE** for everyone, no registration required, with a daily rate limit of **50 requests** per IP address.
|
|
117
|
+
|
|
118
|
+
For preventing abusive usage, the service has associated a daily rate limit based on requests IP address.
|
|
119
|
+
|
|
120
|
+
You can verify for your rate limit state checking the following headers in the response:
|
|
121
|
+
|
|
122
|
+
- `x-rate-limit-limit`: The maximum number of requests that the consumer is permitted to make per minute.
|
|
123
|
+
- `x-rate-limit-remaining`: The number of requests remaining in the current rate limit window.
|
|
124
|
+
- `x-rate-limit-reset`: The time at which the current rate limit window resets in UTC epoch seconds.
|
|
125
|
+
|
|
126
|
+
For higher usage, the **[PRO](https://unavatar.io/checkout)** plan is a usage-based plan billed monthly that removes rate limits and unlocks custom TTL.
|
|
127
|
+
|
|
128
|
+
Every request has a cost in tokens (**$0.001 per token**) based on the proxy tier needed to resolve the avatar:
|
|
129
|
+
|
|
130
|
+
| Proxy tier | Tokens | Cost |
|
|
131
|
+
| ----------- | :----: | :----: |
|
|
132
|
+
| Origin | 1 | $0.001 |
|
|
133
|
+
| Datacenter | +2 | $0.003 |
|
|
134
|
+
| Residential | +4 | $0.007 |
|
|
135
|
+
|
|
136
|
+
The proxy tier used is returned in the `x-proxy-tier` response header, and the total cost in the `x-unavatar-cost` header.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
$ curl -I -H "x-api-key: YOUR_API_KEY" https://unavatar.io/instagram/kikobeats
|
|
140
|
+
|
|
141
|
+
x-pricing-tier: pro
|
|
142
|
+
x-proxy-tier: origin
|
|
143
|
+
x-unavatar-cost: 1
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To upgrade, visit [unavatar.io/checkout](https://unavatar.io/checkout). After completing the payment, you'll receive an API key.
|
|
147
|
+
|
|
148
|
+
## Providers
|
|
149
|
+
|
|
150
|
+
### Apple Music
|
|
151
|
+
|
|
152
|
+
It resolves user avatar against **music.apple.com**.
|
|
153
|
+
|
|
154
|
+
e.g., [unavatar.io/apple-music/daft%20punk](https://unavatar.io/apple-music/daft%20punk)
|
|
155
|
+
|
|
156
|
+
The endpoint supports explictiy type as part of the input.
|
|
157
|
+
|
|
158
|
+
If explicit type is not provided, it searches `artist` and `song` (in that order).
|
|
159
|
+
|
|
160
|
+
Available types:
|
|
161
|
+
|
|
162
|
+
- artist
|
|
163
|
+
- by artist name: [unavatar.io/apple-music/artist:daft%20punk](https://unavatar.io/apple-music/artist:daft%20punk)
|
|
164
|
+
- by numeric artist ID: [unavatar.io/apple-music/artist:5468295](https://unavatar.io/apple-music/artist:5468295)
|
|
165
|
+
- album
|
|
166
|
+
- by album name: [unavatar.io/apple-music/album:discovery](https://unavatar.io/apple-music/album:discovery)
|
|
167
|
+
- by album ID: [unavatar.io/apple-music/album:78691923](https://unavatar.io/apple-music/album:78691923)
|
|
168
|
+
- song
|
|
169
|
+
- by song name: [unavatar.io/apple-music/song:harder%20better%20faster%20stronger](https://unavatar.io/apple-music/song:harder%20better%20faster%20stronger)
|
|
170
|
+
- by song ID: [unavatar.io/apple-music/song:697195787](https://unavatar.io/apple-music/song:697195787)
|
|
171
|
+
|
|
172
|
+
### Bluesky
|
|
173
|
+
|
|
174
|
+
It resolves user avatar against **bsky.app**.
|
|
175
|
+
|
|
176
|
+
e.g., [unavatar.io/bluesky/pfrazee.com](https://unavatar.io/bluesky/pfrazee.com)
|
|
177
|
+
|
|
178
|
+
### DeviantArt
|
|
179
|
+
|
|
180
|
+
It resolves user avatar against **deviantart.com**.
|
|
181
|
+
|
|
182
|
+
e.g., [unavatar.io/deviantart/spyed](https://unavatar.io/deviantart/spyed)
|
|
183
|
+
|
|
184
|
+
### Dribbble
|
|
185
|
+
|
|
186
|
+
It resolves user avatar against **dribbble.com**.
|
|
187
|
+
|
|
188
|
+
e.g., [unavatar.io/dribbble/omidnikrah](https://unavatar.io/dribbble/omidnikrah)
|
|
189
|
+
|
|
190
|
+
### DuckDuckGo
|
|
191
|
+
|
|
192
|
+
It resolves user avatar using **duckduckgo.com**.
|
|
193
|
+
|
|
194
|
+
e.g., [unavatar.io/duckduckgo/gummibeer.dev](https://unavatar.io/duckduckgo/gummibeer.dev)
|
|
195
|
+
|
|
196
|
+
### GitHub
|
|
197
|
+
|
|
198
|
+
It resolves user avatar against **github.com**.
|
|
199
|
+
|
|
200
|
+
e.g., [unavatar.io/github/mdo](https://unavatar.io/github/mdo)
|
|
201
|
+
|
|
202
|
+
### GitLab
|
|
203
|
+
|
|
204
|
+
It resolves user avatar against **gitlab.com**.
|
|
205
|
+
|
|
206
|
+
e.g., [unavatar.io/gitlab/inkscape](https://unavatar.io/gitlab/inkscape)
|
|
207
|
+
|
|
208
|
+
### Google
|
|
209
|
+
|
|
210
|
+
It resolves user avatar using **google.com**.
|
|
211
|
+
|
|
212
|
+
e.g., [unavatar.io/google/netflix.com](https://unavatar.io/google/netflix.com)
|
|
213
|
+
|
|
214
|
+
### Gravatar
|
|
215
|
+
|
|
216
|
+
It resolves user avatar against **gravatar.com**.
|
|
217
|
+
|
|
218
|
+
e.g., [unavatar.io/gravatar/sindresorhus@gmail.com](https://unavatar.io/gravatar/sindresorhus@gmail.com)
|
|
219
|
+
|
|
220
|
+
### Instagram
|
|
221
|
+
|
|
222
|
+
It resolves user avatar against **instagram.com**.
|
|
223
|
+
|
|
224
|
+
e.g., [unavatar.io/instagram/willsmith](https://unavatar.io/instagram/willsmith)
|
|
225
|
+
|
|
226
|
+
### Microlink
|
|
227
|
+
|
|
228
|
+
It resolves user avatar using **microlink.io**.
|
|
229
|
+
|
|
230
|
+
e.g., [unavatar.io/microlink/microlink.io](https://unavatar.io/microlink/microlink.io)
|
|
231
|
+
|
|
232
|
+
### OnlyFans
|
|
233
|
+
|
|
234
|
+
It resolves user avatar using **onlyfans.com**.
|
|
235
|
+
|
|
236
|
+
e.g., [unavatar.io/onlyfans/amandaribas](https://unavatar.io/onlyfans/amandaribas)
|
|
237
|
+
|
|
238
|
+
### OpenStreetMap
|
|
239
|
+
|
|
240
|
+
It resolves user avatar using **openstreetmap.org**.
|
|
241
|
+
|
|
242
|
+
The input accepts:
|
|
243
|
+
|
|
244
|
+
- Numeric user ID, e.g., [unavatar.io/openstreetmap/98672](https://unavatar.io/openstreetmap/98672)
|
|
245
|
+
- Username e.g., [unavatar.io/openstreetmap/Terence%20Eden](https://unavatar.io/openstreetmap/Terence%20Eden)
|
|
246
|
+
|
|
247
|
+
### Patreon
|
|
248
|
+
|
|
249
|
+
It resolves user avatar against **patreon.com**.
|
|
250
|
+
|
|
251
|
+
e.g., [unavatar.io/patreon/kikobeats](https://unavatar.io/patreon/kikobeats)
|
|
252
|
+
|
|
253
|
+
### Reddit
|
|
254
|
+
|
|
255
|
+
It resolves user avatar against **reddit.com**.
|
|
256
|
+
|
|
257
|
+
e.g., [unavatar.io/reddit/kikobeats](https://unavatar.io/reddit/kikobeats)
|
|
258
|
+
|
|
259
|
+
### SoundCloud
|
|
260
|
+
|
|
261
|
+
It resolves user avatar against **soundcloud.com**.
|
|
262
|
+
|
|
263
|
+
e.g., [unavatar.io/soundcloud/gorillaz](https://unavatar.io/soundcloud/gorillaz)
|
|
264
|
+
|
|
265
|
+
### Spotify
|
|
266
|
+
|
|
267
|
+
It resolves user avatar against **open.spotify.com**.
|
|
268
|
+
|
|
269
|
+
e.g., [unavatar.io/spotify/kikobeats](https://unavatar.io/spotify/kikobeats)
|
|
270
|
+
|
|
271
|
+
The endpoint supports explictiy type as part of the input.
|
|
272
|
+
|
|
273
|
+
If explicit type is not provided, it defaults to `user`.
|
|
274
|
+
|
|
275
|
+
Available types:
|
|
276
|
+
|
|
277
|
+
- `user`: [unavatar.io/spotify/kikobeats](https://unavatar.io/spotify/kikobeats)
|
|
278
|
+
- `artist`: [unavatar.io/spotify/artist:6sFIWsNpZYqbRiDnNOkZCA](https://unavatar.io/spotify/artist:6sFIWsNpZYqbRiDnNOkZCA)
|
|
279
|
+
- `playlist`: [unavatar.io/spotify/playlist:37i9dQZF1DXcBWIGoYBM5M](https://unavatar.io/spotify/playlist:37i9dQZF1DXcBWIGoYBM5M)
|
|
280
|
+
- `album`: [unavatar.io/spotify/album:4aawyAB9vmqN3uQ7FjRGTy](https://unavatar.io/spotify/album:4aawyAB9vmqN3uQ7FjRGTy)
|
|
281
|
+
- `show`: [unavatar.io/spotify/show:6UCtBYL29hRg064d4i5W2i](https://unavatar.io/spotify/show:6UCtBYL29hRg064d4i5W2i)
|
|
282
|
+
- `episode`: [unavatar.io/spotify/episode:512ojhOuo1ktJprKbVcKyQ](https://unavatar.io/spotify/episode:512ojhOuo1ktJprKbVcKyQ)
|
|
283
|
+
- `track`: [unavatar.io/spotify/track:11dFghVXANMlKmJXsNCbNl](https://unavatar.io/spotify/track:11dFghVXANMlKmJXsNCbNl)
|
|
284
|
+
|
|
285
|
+
### Substack
|
|
286
|
+
|
|
287
|
+
It resolves user avatar against **substack.com**.
|
|
288
|
+
|
|
289
|
+
e.g., [unavatar.io/substack/bankless](https://unavatar.io/substack/bankless)
|
|
290
|
+
|
|
291
|
+
### Telegram
|
|
292
|
+
|
|
293
|
+
It resolves user avatar against **telegram.com**.
|
|
294
|
+
|
|
295
|
+
e.g., [unavatar.io/telegram/drsdavidsoft](https://unavatar.io/telegram/drsdavidsoft)
|
|
296
|
+
|
|
297
|
+
### TikTok
|
|
298
|
+
|
|
299
|
+
It resolves user avatar against **tiktok.com**.
|
|
300
|
+
|
|
301
|
+
e.g., [unavatar.io/tiktok/carlosazaustre](https://unavatar.io/tiktok/carlosazaustre)
|
|
302
|
+
|
|
303
|
+
### Twitch
|
|
304
|
+
|
|
305
|
+
It resolves user avatar against **twitch.tv**.
|
|
306
|
+
|
|
307
|
+
e.g., [unavatar.io/twitch/midudev](https://unavatar.io/twitch/midudev)
|
|
308
|
+
|
|
309
|
+
### Vimeo
|
|
310
|
+
|
|
311
|
+
It resolves user avatar against **vimeo.com**.
|
|
312
|
+
|
|
313
|
+
e.g., [unavatar.io/vimeo/staff](https://unavatar.io/vimeo/staff)
|
|
314
|
+
|
|
315
|
+
### WhatsApp
|
|
316
|
+
|
|
317
|
+
It resolves user avatar against **whatsapp.com**.
|
|
318
|
+
|
|
319
|
+
The input supports a URI format `type:id`. When no type is provided, it defaults to `phone`.
|
|
320
|
+
|
|
321
|
+
- `phone` (default): [unavatar.io/whatsapp/34612345678](https://unavatar.io/whatsapp/34612345678)
|
|
322
|
+
- `channel`: [unavatar.io/whatsapp/channel:0029VaABC1234abcDEF56789](https://unavatar.io/whatsapp/channel:0029VaABC1234abcDEF56789)
|
|
323
|
+
- `chat`: [unavatar.io/whatsapp/chat:ABC1234DEFghi](https://unavatar.io/whatsapp/chat:ABC1234DEFghi)
|
|
324
|
+
- `group`: [unavatar.io/whatsapp/group:ABC1234DEFghi](https://unavatar.io/whatsapp/group:ABC1234DEFghi)
|
|
325
|
+
|
|
326
|
+
### X/Twitter
|
|
327
|
+
|
|
328
|
+
It resolves user avatar against **x.com**.
|
|
329
|
+
|
|
330
|
+
e.g., [unavatar.io/x/kikobeats](https://unavatar.io/x/kikobeats)
|
|
331
|
+
|
|
332
|
+
### YouTube
|
|
333
|
+
|
|
334
|
+
It resolves user avatar against **youtube.com**.
|
|
335
|
+
|
|
336
|
+
e.g., [unavatar.io/youtube/casey](https://unavatar.io/youtube/casey)
|
|
337
|
+
|
|
338
|
+
## Response Format
|
|
339
|
+
|
|
340
|
+
A response is returning the user avatar by default.
|
|
341
|
+
|
|
342
|
+
However, you can get a [json](#json) as response payload.
|
|
343
|
+
|
|
344
|
+
When an endpoint returns JSON, the shape is predictable so you can parse it reliably in your app:
|
|
345
|
+
|
|
346
|
+
| Field | Type | Present in | Description |
|
|
347
|
+
| --------- | -------------- | ----------------------------- | ------------------------------------------------ |
|
|
348
|
+
| `status` | `string` | all JSON responses | One of: `success`, `fail`, `error`. |
|
|
349
|
+
| `message` | `string` | all JSON responses | Human-readable summary for display/logging. |
|
|
350
|
+
| `data` | `object` | `success` | Response payload for successful requests. |
|
|
351
|
+
| `code` | `string` | `fail`, `error` | Stable machine-readable error code. |
|
|
352
|
+
| `more` | `string (URL)` | most `fail`/`error` responses | Documentation URL with troubleshooting details. |
|
|
353
|
+
| `report` | `string` | some `error` responses | Support contact channel (for example `mailto:`). |
|
|
354
|
+
|
|
355
|
+
## Response Headers
|
|
356
|
+
|
|
357
|
+
These headers help you understand pricing, limits, and request diagnostics.
|
|
358
|
+
|
|
359
|
+
| Header | Purpose |
|
|
360
|
+
| ------------------------ | --------------------------------------------------------- |
|
|
361
|
+
| `x-pricing-tier` | `free` or `pro` — the plan used for this request |
|
|
362
|
+
| `x-timestamp` | Server timestamp when request was received |
|
|
363
|
+
| `x-unavatar-cost` | Token cost of the request (avatar routes only) |
|
|
364
|
+
| `x-proxy-tier` | Proxy tier used: `origin`, `datacenter`, or `residential` |
|
|
365
|
+
| `x-rate-limit-limit` | Maximum requests allowed per window (free tier only) |
|
|
366
|
+
| `x-rate-limit-remaining` | Remaining requests in current window (free tier only) |
|
|
367
|
+
| `x-rate-limit-reset` | UTC epoch seconds when window resets (free tier only) |
|
|
368
|
+
| `retry-after` | Seconds until rate limit resets (only on 429 responses) |
|
|
369
|
+
|
|
370
|
+
## Response Errors
|
|
371
|
+
|
|
372
|
+
Expected errors are known operational cases returned with stable codes.
|
|
373
|
+
|
|
374
|
+
- **Client-side issues** return `status: "fail"` (HTTP `4xx`).
|
|
375
|
+
- **Service-side issues** return `status: "error"` (HTTP `5xx`).
|
|
376
|
+
- Unknown failures return `EINTERNAL` (HTTP `500`).
|
|
377
|
+
- Use the `code` for programmatic handling in clients.
|
|
378
|
+
- Use the `message` to show user-facing feedback.
|
|
379
|
+
- `more` links to documentation for common fixes.
|
|
380
|
+
- `report` (when present) indicates how to contact support for server errors.
|
|
381
|
+
|
|
382
|
+
| HTTP | Code | Typical trigger |
|
|
383
|
+
| ---- | -------------------- | ------------------------------------------- |
|
|
384
|
+
| 400 | `ESESSIONID` | Missing `session_id` in `/checkout/success` |
|
|
385
|
+
| 400 | `ESESSION` | Checkout session not paid or not found |
|
|
386
|
+
| 400 | `ESIGNATURE` | Missing `stripe-signature` header |
|
|
387
|
+
| 400 | `EWEBHOOK` | Invalid/failed Stripe webhook processing |
|
|
388
|
+
| 400 | `EAPIKEYVALUE` | Missing `apiKey` query parameter |
|
|
389
|
+
| 400 | `EAPIKEYLABEL` | Missing `label` query parameter |
|
|
390
|
+
| 401 | `EEMAIL` | Invalid or missing authenticated email |
|
|
391
|
+
| 401 | `EUSERUNAUTHORIZED` | Missing/invalid auth for protected routes |
|
|
392
|
+
| 401 | `EAPIKEY` | Invalid `x-api-key` |
|
|
393
|
+
| 403 | `ETTL` | Custom `ttl` requested without pro plan |
|
|
394
|
+
| 403 | `EPRO` | Provider restricted to pro plan |
|
|
395
|
+
| 404 | `ENOTFOUND` | Route not found |
|
|
396
|
+
| 404 | `EAPIKEYNOTFOUND` | API key not found |
|
|
397
|
+
| 409 | `EAPIKEYEXISTS` | Custom API key already exists |
|
|
398
|
+
| 409 | `EAPIKEYLABELEXISTS` | API key label already exists |
|
|
399
|
+
| 409 | `EAPIKEYMIN` | Attempt to remove last remaining key |
|
|
400
|
+
| 429 | `ERATE` | Free-tier daily rate limit exceeded |
|
|
401
|
+
| 500 | `ECHECKOUT` | Stripe checkout session creation failed |
|
|
402
|
+
| 500 | `EAPIKEYFAILED` | API key retrieval after checkout failed |
|
|
403
|
+
| 500 | `EINTERNAL` | Unexpected internal server failure |
|
|
404
|
+
|
|
405
|
+
## Contact
|
|
406
|
+
|
|
407
|
+
If you have any suggestion or bug to report, please contact to ust mailing to hello@unavatar.io.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { default: termImg } = require('term-img')
|
|
4
|
+
const { STATUS_CODES } = require('http')
|
|
5
|
+
const got = require('got')
|
|
6
|
+
const mri = require('mri')
|
|
7
|
+
|
|
8
|
+
module.exports = ({ baseUrl }) => {
|
|
9
|
+
const { _, ...flags } = mri(process.argv.slice(2), {
|
|
10
|
+
default: {}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const [input] = _
|
|
14
|
+
|
|
15
|
+
if (!input) {
|
|
16
|
+
console.error('Usage: unavatar <input> | unavatar <provider>/<key> | unavatar ping')
|
|
17
|
+
console.error(
|
|
18
|
+
'Examples: unavatar reddit.com | unavatar sindresorhus@gmail.com | unavatar x/kikobeats | unavatar ping'
|
|
19
|
+
)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isPing = input.toLowerCase() === 'ping'
|
|
24
|
+
const normalizeInput = value => {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(value)
|
|
27
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') return url.hostname
|
|
28
|
+
} catch (_) {}
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const resolvedInput = isPing ? input : normalizeInput(input)
|
|
33
|
+
|
|
34
|
+
const apiUrl = isPing
|
|
35
|
+
? new URL('/ping', baseUrl)
|
|
36
|
+
: (() => {
|
|
37
|
+
const hasProviderFormat = resolvedInput.includes('/')
|
|
38
|
+
let url
|
|
39
|
+
|
|
40
|
+
if (hasProviderFormat) {
|
|
41
|
+
const [provider, ...keyParts] = resolvedInput.split('/')
|
|
42
|
+
const key = keyParts.join('/')
|
|
43
|
+
|
|
44
|
+
if (!provider || !key) {
|
|
45
|
+
console.error('Invalid input format. Expected: <input>, <provider>/<key>, or "ping"')
|
|
46
|
+
console.error(
|
|
47
|
+
'Examples: unavatar reddit.com | unavatar sindresorhus@gmail.com | unavatar x/kikobeats | unavatar ping'
|
|
48
|
+
)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
url = new URL(`/${provider}/${key}`, baseUrl)
|
|
53
|
+
} else {
|
|
54
|
+
url = new URL(`/${resolvedInput}`, baseUrl)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
url.searchParams.set('json', 'true')
|
|
58
|
+
return url
|
|
59
|
+
})()
|
|
60
|
+
|
|
61
|
+
const startTime = Date.now()
|
|
62
|
+
|
|
63
|
+
let durationInfo = null
|
|
64
|
+
let apiHeaders = null
|
|
65
|
+
let apiStatusCode = null
|
|
66
|
+
|
|
67
|
+
const logMeta = () => {
|
|
68
|
+
if (apiHeaders) {
|
|
69
|
+
const headerEntries = Object.entries(apiHeaders)
|
|
70
|
+
if (headerEntries.length > 0) {
|
|
71
|
+
const maxHeaderLength = Math.max(...headerEntries.map(([key]) => key.toLowerCase().length))
|
|
72
|
+
|
|
73
|
+
console.error()
|
|
74
|
+
console.error(`HTTP/1.1 ${apiStatusCode} ${STATUS_CODES[apiStatusCode]}`)
|
|
75
|
+
headerEntries
|
|
76
|
+
.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
|
77
|
+
.forEach(([key, value]) => {
|
|
78
|
+
const paddedKey = key.toLowerCase().padEnd(maxHeaderLength)
|
|
79
|
+
console.error(`${paddedKey}: ${value}`)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (durationInfo) {
|
|
85
|
+
console.error()
|
|
86
|
+
console.error(durationInfo)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
got(apiUrl.toString(), { headers: { 'x-api-key': flags.apiKey }, responseType: 'json' })
|
|
91
|
+
.then(({ body, headers, statusCode, timings }) => {
|
|
92
|
+
apiHeaders = headers
|
|
93
|
+
apiStatusCode = statusCode
|
|
94
|
+
|
|
95
|
+
const duration = Date.now() - startTime
|
|
96
|
+
const timeToFirstByte =
|
|
97
|
+
timings?.response && timings?.start ? Math.round(timings.response - timings.start) : null
|
|
98
|
+
|
|
99
|
+
durationInfo = timeToFirstByte
|
|
100
|
+
? `Duration: ${duration}ms (TTFB: ${timeToFirstByte}ms)`
|
|
101
|
+
: `Duration: ${duration}ms`
|
|
102
|
+
|
|
103
|
+
if (isPing) {
|
|
104
|
+
console.error()
|
|
105
|
+
console.error(`${apiUrl.toString()}\n`)
|
|
106
|
+
console.error(body)
|
|
107
|
+
logMeta()
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!body || !body.url) {
|
|
112
|
+
console.error('No avatar URL found')
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return got(body.url, { responseType: 'buffer' }).then(result => ({
|
|
117
|
+
buffer: result.body,
|
|
118
|
+
imageUrl: body.url
|
|
119
|
+
}))
|
|
120
|
+
})
|
|
121
|
+
.then(({ buffer, imageUrl }) => {
|
|
122
|
+
console.error()
|
|
123
|
+
console.error(termImg(buffer, { width: '15%' }))
|
|
124
|
+
|
|
125
|
+
console.error(`
|
|
126
|
+
input: ${apiUrl.toString()}
|
|
127
|
+
output: ${imageUrl}
|
|
128
|
+
`)
|
|
129
|
+
|
|
130
|
+
logMeta()
|
|
131
|
+
})
|
|
132
|
+
.catch(error => {
|
|
133
|
+
const response = error?.response
|
|
134
|
+
if (response) {
|
|
135
|
+
apiHeaders = response.headers
|
|
136
|
+
apiStatusCode = response.statusCode
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!durationInfo) {
|
|
140
|
+
const duration = Date.now() - startTime
|
|
141
|
+
durationInfo = `Duration: ${duration}ms`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const body = response?.body
|
|
145
|
+
if (body !== undefined) {
|
|
146
|
+
console.error()
|
|
147
|
+
|
|
148
|
+
if (typeof body === 'object') {
|
|
149
|
+
console.error(JSON.stringify(body, null, 2))
|
|
150
|
+
} else {
|
|
151
|
+
const rawBody = Buffer.isBuffer(body) ? body.toString('utf8') : String(body)
|
|
152
|
+
try {
|
|
153
|
+
console.error(JSON.stringify(JSON.parse(rawBody), null, 2))
|
|
154
|
+
} catch (_) {
|
|
155
|
+
console.error(rawBody)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logMeta()
|
|
161
|
+
process.exit(1)
|
|
162
|
+
})
|
|
163
|
+
}
|
package/bin/unavatar
ADDED
package/bin/unavatar-dev
ADDED