e621 3.5.0 → 3.5.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 +33 -0
- package/dist/browser/e621.js +237 -27
- package/dist/browser/e621.js.map +4 -4
- package/dist/lib/OAuthClient.d.ts +152 -0
- package/dist/lib/OAuthClient.js +223 -0
- package/dist/lib/client.d.ts +14 -1
- package/dist/lib/client.js +26 -27
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +24 -2
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/index.js +2 -1
- package/dist/lib/standalone.d.ts +1 -1
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -37,6 +37,39 @@ const e621 = new E621({
|
|
|
37
37
|
// e.g. use e621.posts.METHOD(), e621.users.METHOD()
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
### Bearer Tokens & OAuth
|
|
41
|
+
|
|
42
|
+
Instead of `authUser`/`authKey`, you can authenticate with a bearer token (e.g. an OAuth access token). It can be a string, or a function that returns one, which is called for every request. If both are given, the bearer token wins.
|
|
43
|
+
```typescript
|
|
44
|
+
const e621 = new E621({ authToken: "..." });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`OAuthClient` handles the OAuth 2.0 authorization code flow (with PKCE) for you, and keeps the access token refreshed. **OAuth has not been publicly released on e621 yet.**
|
|
48
|
+
```typescript
|
|
49
|
+
import E621, { OAuthClient } from "e621"; // or from "e621/oauth"
|
|
50
|
+
|
|
51
|
+
const oauth = new OAuthClient({
|
|
52
|
+
clientId: "",
|
|
53
|
+
clientSecret: "", // leave out for public clients
|
|
54
|
+
redirectURI: "https://example.com/callback",
|
|
55
|
+
scopes: ["openid", "full"],
|
|
56
|
+
token: savedToken, // optional, restore a previous token
|
|
57
|
+
onToken: (token) => saveToken(token), // called when the token is exchanged, refreshed, or revoked
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// keep state & codeVerifier (e.g. in the session), and send the user to url
|
|
61
|
+
const { url, state, codeVerifier } = await oauth.createAuthorizationURL();
|
|
62
|
+
|
|
63
|
+
// once they're redirected back, this checks the state and exchanges the code
|
|
64
|
+
await oauth.handleCallback(callbackURL, { state, codeVerifier });
|
|
65
|
+
|
|
66
|
+
// getAccessToken refreshes the token when it's about to expire
|
|
67
|
+
const e621 = new E621({ authToken: oauth.getAccessToken });
|
|
68
|
+
|
|
69
|
+
await oauth.userInfo();
|
|
70
|
+
await oauth.revoke();
|
|
71
|
+
```
|
|
72
|
+
|
|
40
73
|
### Default Post Format
|
|
41
74
|
|
|
42
75
|
Post-returning methods (`posts.get()`, `posts.search()`, `favorites.search()`, `popular.get()`, `iqdbQueries.get()`/`.post()`, and convenience methods on returned post models like `post.update()`) accept per-call `v2`/`mode` options and default to the legacy format when omitted. Set `defaultPostFormat` once to change that default everywhere instead of repeating the same options on every call:
|
package/dist/browser/e621.js
CHANGED
|
@@ -37,10 +37,12 @@ ${error.message}`;
|
|
|
37
37
|
if ("message" in error && typeof error.message === "string")
|
|
38
38
|
message += `
|
|
39
39
|
${error.message}`;
|
|
40
|
-
else if ("error" in error && typeof error.error === "string")
|
|
40
|
+
else if ("error" in error && typeof error.error === "string") {
|
|
41
41
|
message += `
|
|
42
42
|
${error.error}`;
|
|
43
|
-
|
|
43
|
+
if ("error_description" in error && typeof error.error_description === "string")
|
|
44
|
+
message += `: ${error.error_description}`;
|
|
45
|
+
} else if ("errors" in error && typeof error.errors === "object" && error.errors !== null) {
|
|
44
46
|
for (const [key, value] of Object.entries(error.errors)) {
|
|
45
47
|
if (typeof value === "string")
|
|
46
48
|
message += `
|
|
@@ -71,6 +73,19 @@ var RatelimitError = class extends UnexpectedResponseError {
|
|
|
71
73
|
Retry after ${this.retryAfter}s`);
|
|
72
74
|
}
|
|
73
75
|
};
|
|
76
|
+
var OAuthCallbackError = class extends Error {
|
|
77
|
+
/** The OAuth error code, e.g. `access_denied`. */
|
|
78
|
+
error;
|
|
79
|
+
errorDescription;
|
|
80
|
+
name = "OAuthCallbackError";
|
|
81
|
+
state;
|
|
82
|
+
constructor(error, errorDescription, state) {
|
|
83
|
+
super(errorDescription ? `${error}: ${errorDescription}` : error);
|
|
84
|
+
this.error = error;
|
|
85
|
+
this.errorDescription = errorDescription;
|
|
86
|
+
this.state = state;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
74
89
|
|
|
75
90
|
// dist/lib/generated/core/serverSentEvents.js
|
|
76
91
|
function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
|
|
@@ -851,12 +866,13 @@ var createClient = (config = {}) => {
|
|
|
851
866
|
};
|
|
852
867
|
|
|
853
868
|
// dist/lib/version.js
|
|
854
|
-
var VERSION = "3.5.
|
|
869
|
+
var VERSION = "3.5.1";
|
|
855
870
|
|
|
856
871
|
// dist/lib/client.js
|
|
857
872
|
function createE621Client(options) {
|
|
858
873
|
const resolved = {
|
|
859
874
|
authKey: options?.authKey ?? null,
|
|
875
|
+
authToken: options?.authToken ?? null,
|
|
860
876
|
authUser: options?.authUser ?? null,
|
|
861
877
|
baseURL: options?.baseURL ?? "https://e621.net",
|
|
862
878
|
ratelimit: {
|
|
@@ -871,8 +887,17 @@ function createE621Client(options) {
|
|
|
871
887
|
userAgent: options?.userAgent ?? `E621/${VERSION} (https://github.com/DonovanDMC/E621${options?.authUser ? `; "${options.authUser}"` : ""})`,
|
|
872
888
|
userAgentInQuery: options?.userAgentInQuery ?? "window" in globalThis
|
|
873
889
|
};
|
|
874
|
-
|
|
875
|
-
|
|
890
|
+
const getAuthorization = async () => {
|
|
891
|
+
if (resolved.authToken) {
|
|
892
|
+
const token = typeof resolved.authToken === "function" ? await resolved.authToken() : resolved.authToken;
|
|
893
|
+
if (token)
|
|
894
|
+
return `Bearer ${token}`;
|
|
895
|
+
}
|
|
896
|
+
if (resolved.authKey && resolved.authUser)
|
|
897
|
+
return `Basic ${btoa(`${resolved.authUser}:${resolved.authKey}`)}`;
|
|
898
|
+
return null;
|
|
899
|
+
};
|
|
900
|
+
const config = createConfig({
|
|
876
901
|
baseUrl: resolved.baseURL,
|
|
877
902
|
headers: resolved.userAgentInQuery ? void 0 : {
|
|
878
903
|
"User-Agent": resolved.userAgent
|
|
@@ -882,19 +907,21 @@ function createE621Client(options) {
|
|
|
882
907
|
if (input instanceof Request) {
|
|
883
908
|
url = input.url;
|
|
884
909
|
method = input.method.toLowerCase();
|
|
885
|
-
if (!input.headers.has("Authorization")
|
|
886
|
-
|
|
910
|
+
if (!input.headers.has("Authorization")) {
|
|
911
|
+
const authorization = await getAuthorization();
|
|
912
|
+
if (authorization)
|
|
913
|
+
input.headers.set("Authorization", authorization);
|
|
887
914
|
}
|
|
888
915
|
} else if (init) {
|
|
889
916
|
url = input.toString();
|
|
890
917
|
method = init.method?.toLowerCase() ?? "get";
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
init.headers = headers;
|
|
918
|
+
const headers = new Headers(init.headers);
|
|
919
|
+
if (!headers.has("Authorization")) {
|
|
920
|
+
const authorization = await getAuthorization();
|
|
921
|
+
if (authorization)
|
|
922
|
+
headers.set("Authorization", authorization);
|
|
897
923
|
}
|
|
924
|
+
init.headers = headers;
|
|
898
925
|
}
|
|
899
926
|
if (resolved.userAgentInQuery && input instanceof Request) {
|
|
900
927
|
const requestURL = new URL(input.url);
|
|
@@ -930,20 +957,7 @@ function createE621Client(options) {
|
|
|
930
957
|
}
|
|
931
958
|
return res;
|
|
932
959
|
}
|
|
933
|
-
};
|
|
934
|
-
if (resolved.authKey && resolved.authUser) {
|
|
935
|
-
config = createConfig({
|
|
936
|
-
...commonConfig,
|
|
937
|
-
auth: (auth) => {
|
|
938
|
-
if (auth.scheme === "basic") {
|
|
939
|
-
return `${resolved.authUser}:${resolved.authKey}`;
|
|
940
|
-
}
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
} else {
|
|
945
|
-
config = createConfig(commonConfig);
|
|
946
|
-
}
|
|
960
|
+
});
|
|
947
961
|
return { client: createClient(config), options: resolved };
|
|
948
962
|
}
|
|
949
963
|
|
|
@@ -22440,6 +22454,200 @@ var OAuthErrorSchema = {
|
|
|
22440
22454
|
}
|
|
22441
22455
|
};
|
|
22442
22456
|
|
|
22457
|
+
// dist/lib/OAuthClient.js
|
|
22458
|
+
function base64URL(bytes) {
|
|
22459
|
+
let str = "";
|
|
22460
|
+
for (const byte of bytes)
|
|
22461
|
+
str += String.fromCharCode(byte);
|
|
22462
|
+
return btoa(str).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
22463
|
+
}
|
|
22464
|
+
function randomString(byteLength = 32) {
|
|
22465
|
+
return base64URL(crypto.getRandomValues(new Uint8Array(byteLength)));
|
|
22466
|
+
}
|
|
22467
|
+
var OAuthClient = class _OAuthClient {
|
|
22468
|
+
_clientSecret;
|
|
22469
|
+
_discovery;
|
|
22470
|
+
_onToken;
|
|
22471
|
+
_refreshing;
|
|
22472
|
+
_token;
|
|
22473
|
+
_tokenInfo;
|
|
22474
|
+
_tokens;
|
|
22475
|
+
_userinfo;
|
|
22476
|
+
baseURL;
|
|
22477
|
+
clientId;
|
|
22478
|
+
redirectURI;
|
|
22479
|
+
refreshLeeway;
|
|
22480
|
+
scopes;
|
|
22481
|
+
constructor(options) {
|
|
22482
|
+
const { clientId, clientSecret, onToken, redirectURI, refreshLeeway, scopes, token, ...clientOptions } = options;
|
|
22483
|
+
this.clientId = clientId;
|
|
22484
|
+
this.redirectURI = redirectURI ?? null;
|
|
22485
|
+
this.refreshLeeway = refreshLeeway ?? 60;
|
|
22486
|
+
this.scopes = scopes ?? [];
|
|
22487
|
+
const { client: client2, options: resolved } = createE621Client(clientOptions);
|
|
22488
|
+
const { client: authedClient } = createE621Client({ ...clientOptions, authToken: () => this.getAccessToken() });
|
|
22489
|
+
this.baseURL = resolved.baseURL;
|
|
22490
|
+
Object.defineProperties(this, {
|
|
22491
|
+
_clientSecret: { value: clientSecret ?? null, enumerable: false },
|
|
22492
|
+
_discovery: { value: new DoorkeeperOpenidConnectDiscovery(void 0, client2), enumerable: false },
|
|
22493
|
+
_onToken: { value: onToken, enumerable: false },
|
|
22494
|
+
_refreshing: { value: null, writable: true, enumerable: false },
|
|
22495
|
+
_token: { value: token ?? null, writable: true, enumerable: false },
|
|
22496
|
+
_tokenInfo: { value: new DoorkeeperTokenInfo(void 0, authedClient), enumerable: false },
|
|
22497
|
+
_tokens: { value: new DoorkeeperTokens(void 0, client2), enumerable: false },
|
|
22498
|
+
_userinfo: { value: new DoorkeeperOpenidConnectUserinfo(void 0, authedClient), enumerable: false }
|
|
22499
|
+
});
|
|
22500
|
+
this.getAccessToken = this.getAccessToken.bind(this);
|
|
22501
|
+
}
|
|
22502
|
+
/** Creates a PKCE `code_challenge` (S256) from a code verifier. */
|
|
22503
|
+
static async createCodeChallenge(codeVerifier) {
|
|
22504
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
|
|
22505
|
+
return base64URL(new Uint8Array(digest));
|
|
22506
|
+
}
|
|
22507
|
+
/** Creates a random PKCE code verifier. */
|
|
22508
|
+
static createCodeVerifier() {
|
|
22509
|
+
return randomString(32);
|
|
22510
|
+
}
|
|
22511
|
+
/** Creates a random state value. */
|
|
22512
|
+
static createState() {
|
|
22513
|
+
return randomString(16);
|
|
22514
|
+
}
|
|
22515
|
+
/** When the current access token expires, as a unix timestamp in milliseconds - `null` if there's no token. */
|
|
22516
|
+
get expiresAt() {
|
|
22517
|
+
if (this._token === null)
|
|
22518
|
+
return null;
|
|
22519
|
+
return (this._token.created_at + this._token.expires_in) * 1e3;
|
|
22520
|
+
}
|
|
22521
|
+
/** Whether the current access token is expired (or within {@link OAuthClientOptions.refreshLeeway} of expiring). */
|
|
22522
|
+
get isExpired() {
|
|
22523
|
+
const expiresAt = this.expiresAt;
|
|
22524
|
+
return expiresAt === null || Date.now() >= expiresAt - this.refreshLeeway * 1e3;
|
|
22525
|
+
}
|
|
22526
|
+
/** The current token, if any. */
|
|
22527
|
+
get token() {
|
|
22528
|
+
return this._token;
|
|
22529
|
+
}
|
|
22530
|
+
_clientAuth() {
|
|
22531
|
+
return this._clientSecret === null ? { client_id: this.clientId } : { client_id: this.clientId, client_secret: this._clientSecret };
|
|
22532
|
+
}
|
|
22533
|
+
_requireToken() {
|
|
22534
|
+
if (this._token === null)
|
|
22535
|
+
throw new Error("No OAuth token is available, the user must authorize first.");
|
|
22536
|
+
return this._token;
|
|
22537
|
+
}
|
|
22538
|
+
/** Builds the url to send the user to, to authorize your application. */
|
|
22539
|
+
async createAuthorizationURL(options = {}) {
|
|
22540
|
+
const codeVerifier = options.codeVerifier ?? _OAuthClient.createCodeVerifier();
|
|
22541
|
+
const state = options.state ?? _OAuthClient.createState();
|
|
22542
|
+
const redirectURI = options.redirectURI ?? this.redirectURI;
|
|
22543
|
+
const scopes = options.scopes ?? this.scopes;
|
|
22544
|
+
const url = new URL("/oauth/authorize", this.baseURL);
|
|
22545
|
+
url.searchParams.set("response_type", "code");
|
|
22546
|
+
url.searchParams.set("client_id", this.clientId);
|
|
22547
|
+
if (redirectURI)
|
|
22548
|
+
url.searchParams.set("redirect_uri", redirectURI);
|
|
22549
|
+
if (scopes.length !== 0)
|
|
22550
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
22551
|
+
url.searchParams.set("state", state);
|
|
22552
|
+
url.searchParams.set("code_challenge", await _OAuthClient.createCodeChallenge(codeVerifier));
|
|
22553
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
22554
|
+
if (options.nonce)
|
|
22555
|
+
url.searchParams.set("nonce", options.nonce);
|
|
22556
|
+
if (options.prompt)
|
|
22557
|
+
url.searchParams.set("prompt", options.prompt);
|
|
22558
|
+
return { codeVerifier, redirectURI, state, url: url.toString() };
|
|
22559
|
+
}
|
|
22560
|
+
/** Gets e621's OpenID Connect discovery document. */
|
|
22561
|
+
async discovery() {
|
|
22562
|
+
return this._discovery.provider();
|
|
22563
|
+
}
|
|
22564
|
+
/** Exchanges an authorization code for a token, and stores it. */
|
|
22565
|
+
async exchangeCode(code, codeVerifier, redirectURI = this.redirectURI) {
|
|
22566
|
+
const token = await this._tokens.create({
|
|
22567
|
+
grant_type: "authorization_code",
|
|
22568
|
+
code,
|
|
22569
|
+
code_verifier: codeVerifier,
|
|
22570
|
+
redirect_uri: redirectURI ?? void 0,
|
|
22571
|
+
...this._clientAuth()
|
|
22572
|
+
});
|
|
22573
|
+
await this.setToken(token);
|
|
22574
|
+
return token;
|
|
22575
|
+
}
|
|
22576
|
+
/**
|
|
22577
|
+
* Gets a valid access token, refreshing it first if it's expired. Already bound, so it can be passed
|
|
22578
|
+
* directly as {@link Options.authToken}.
|
|
22579
|
+
*/
|
|
22580
|
+
async getAccessToken() {
|
|
22581
|
+
if (this._token === null)
|
|
22582
|
+
throw new Error("No OAuth token is available, the user must authorize first.");
|
|
22583
|
+
if (this.isExpired)
|
|
22584
|
+
return (await this.refresh()).access_token;
|
|
22585
|
+
return this._token.access_token;
|
|
22586
|
+
}
|
|
22587
|
+
/**
|
|
22588
|
+
* Validates the url the user was redirected back to (state, and any error e621 sent), then exchanges
|
|
22589
|
+
* its code for a token.
|
|
22590
|
+
* @param callbackURL - The full callback url, or just its query string.
|
|
22591
|
+
*/
|
|
22592
|
+
async handleCallback(callbackURL, options) {
|
|
22593
|
+
const params = callbackURL instanceof URLSearchParams ? callbackURL : typeof callbackURL === "string" && !/^[a-z][\d+.a-z-]*:/i.test(callbackURL) ? new URLSearchParams(callbackURL) : new URL(callbackURL).searchParams;
|
|
22594
|
+
const state = params.get("state");
|
|
22595
|
+
const error = params.get("error");
|
|
22596
|
+
if (error)
|
|
22597
|
+
throw new OAuthCallbackError(error, params.get("error_description"), state);
|
|
22598
|
+
if (state !== options.state)
|
|
22599
|
+
throw new OAuthCallbackError("invalid_state", "The callback's state does not match the expected state.", state);
|
|
22600
|
+
const code = params.get("code");
|
|
22601
|
+
if (!code)
|
|
22602
|
+
throw new OAuthCallbackError("invalid_request", "The callback is missing the authorization code.", state);
|
|
22603
|
+
return this.exchangeCode(code, options.codeVerifier, options.redirectURI ?? this.redirectURI);
|
|
22604
|
+
}
|
|
22605
|
+
/** Introspects a token - by default the current access token. */
|
|
22606
|
+
async introspect(token = this._requireToken().access_token) {
|
|
22607
|
+
return this._tokens.introspect({ token, ...this._clientAuth() });
|
|
22608
|
+
}
|
|
22609
|
+
/**
|
|
22610
|
+
* Refreshes the access token, and stores the new token. Concurrent calls share the same request.
|
|
22611
|
+
* @param scopes - Narrow the new token to these scopes.
|
|
22612
|
+
*/
|
|
22613
|
+
async refresh(scopes) {
|
|
22614
|
+
if (this._refreshing)
|
|
22615
|
+
return this._refreshing;
|
|
22616
|
+
const refreshToken = this._requireToken().refresh_token;
|
|
22617
|
+
this._refreshing = this._tokens.create({
|
|
22618
|
+
grant_type: "refresh_token",
|
|
22619
|
+
refresh_token: refreshToken,
|
|
22620
|
+
scope: scopes?.join(" "),
|
|
22621
|
+
...this._clientAuth()
|
|
22622
|
+
}).then(async (token) => {
|
|
22623
|
+
await this.setToken(token);
|
|
22624
|
+
return token;
|
|
22625
|
+
}).finally(() => {
|
|
22626
|
+
this._refreshing = null;
|
|
22627
|
+
});
|
|
22628
|
+
return this._refreshing;
|
|
22629
|
+
}
|
|
22630
|
+
/** Revokes the current token (which also invalidates its refresh token), and clears it. */
|
|
22631
|
+
async revoke() {
|
|
22632
|
+
const token = this._requireToken();
|
|
22633
|
+
await this._tokens.revoke({ token: token.access_token, token_type_hint: "access_token", ...this._clientAuth() });
|
|
22634
|
+
await this.setToken(null);
|
|
22635
|
+
}
|
|
22636
|
+
/** Replaces the stored token, calling {@link OAuthClientOptions.onToken}. */
|
|
22637
|
+
async setToken(token) {
|
|
22638
|
+
this._token = token;
|
|
22639
|
+
await this._onToken?.(token);
|
|
22640
|
+
}
|
|
22641
|
+
/** Gets metadata about the current access token. */
|
|
22642
|
+
async tokenInfo() {
|
|
22643
|
+
return this._tokenInfo.get();
|
|
22644
|
+
}
|
|
22645
|
+
/** Gets the OpenID Connect claims of the authorized user, gated by the token's scopes. */
|
|
22646
|
+
async userInfo() {
|
|
22647
|
+
return this._userinfo.get();
|
|
22648
|
+
}
|
|
22649
|
+
};
|
|
22650
|
+
|
|
22443
22651
|
// dist/lib/index.js
|
|
22444
22652
|
var E621 = class {
|
|
22445
22653
|
client;
|
|
@@ -22488,6 +22696,8 @@ export {
|
|
|
22488
22696
|
NewsUpdate_default as NewsUpdate,
|
|
22489
22697
|
Note_default as Note,
|
|
22490
22698
|
NoteVersion_default as NoteVersion,
|
|
22699
|
+
OAuthCallbackError,
|
|
22700
|
+
OAuthClient,
|
|
22491
22701
|
Pool_default as Pool,
|
|
22492
22702
|
PoolVersion_default as PoolVersion,
|
|
22493
22703
|
Post_default as Post,
|