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 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:
@@ -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
- else if ("errors" in error && typeof error.errors === "object" && error.errors !== null) {
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.0";
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
- let config;
875
- const commonConfig = {
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") && resolved.authKey && resolved.authUser) {
886
- input.headers.set("Authorization", `Basic ${btoa(`${resolved.authUser}:${resolved.authKey}`)}`);
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
- if (resolved.authKey && resolved.authUser) {
892
- const headers = new Headers(init.headers);
893
- if (!headers.has("Authorization")) {
894
- headers.set("Authorization", `Basic ${btoa(`${resolved.authUser}:${resolved.authKey}`)}`);
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,