UNPKG

@sapphire/plugin-api

Version:

Plugin for @sapphire/framework to expose a REST API

1 lines • 10.8 kB
{"version":3,"sources":["../../../../../src/lib/structures/http/Auth.ts"],"names":["OAuth2Scopes","randomBytes","createCipheriv","createDecipheriv","RouteBases","Routes","isThenable","fetch"],"mappings":"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","file":"Auth.cjs","sourcesContent":["import { isThenable, type Awaitable } from '@sapphire/utilities';\nimport { createCipheriv, createDecipheriv, randomBytes } from 'crypto';\nimport {\n\tOAuth2Scopes,\n\tRouteBases,\n\tRoutes,\n\ttype RESTGetAPICurrentUserConnectionsResult,\n\ttype RESTGetAPICurrentUserGuildsResult,\n\ttype RESTGetAPICurrentUserResult,\n\ttype Snowflake\n} from 'discord.js';\nimport { fetch } from 'undici';\n\nexport class Auth {\n\t/**\n\t * The client's application id, this can be retrieved in Discord Developer Portal at https://discord.com/developers/applications.\n\t * @since 1.0.0\n\t */\n\tpublic id: Snowflake;\n\n\t/**\n\t * The name for the cookie, this will be used to identify a Secure HttpOnly cookie.\n\t * @since 1.0.0\n\t */\n\tpublic cookie: string;\n\n\t/**\n\t * The scopes defined at https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes.\n\t * @since 1.0.0\n\t */\n\tpublic scopes: readonly OAuth2Scopes[];\n\n\t/**\n\t * The redirect uri.\n\t * @since 1.0.0\n\t */\n\tpublic redirect: string | undefined;\n\n\t/**\n\t * The transformers used for {@link Auth.fetchData}.\n\t * @since 1.4.0\n\t */\n\tpublic transformers: LoginDataTransformer[];\n\n\tpublic domainOverwrite: string | null = null;\n\n\t#secret: string;\n\n\tprivate constructor(options: ServerOptionsAuth) {\n\t\tthis.id = options.id;\n\t\tthis.cookie = options.cookie ?? 'SAPPHIRE_AUTH';\n\t\tthis.scopes = options.scopes ?? [OAuth2Scopes.Identify];\n\t\tthis.redirect = options.redirect;\n\t\tthis.#secret = options.secret;\n\t\tthis.transformers = options.transformers ?? [];\n\t\tthis.domainOverwrite = options.domainOverwrite ?? null;\n\t}\n\n\t/**\n\t * The client secret, this can be retrieved in Discord Developer Portal at https://discord.com/developers/applications.\n\t * @since 1.0.0\n\t */\n\tpublic get secret() {\n\t\treturn this.#secret;\n\t}\n\n\t/**\n\t * Encrypts an object with aes-256-cbc to use as a token.\n\t * @since 1.0.0\n\t * @param data An object to encrypt\n\t */\n\tpublic encrypt(data: AuthData): string {\n\t\tconst iv = randomBytes(16);\n\t\tconst cipher = createCipheriv('aes-256-cbc', this.#secret, iv);\n\t\treturn `${cipher.update(JSON.stringify(data), 'utf8', 'base64') + cipher.final('base64')}.${iv.toString('base64')}`;\n\t}\n\n\t/**\n\t * Decrypts an object with aes-256-cbc to use as a token.\n\t * @since 1.0.0\n\t * @param token An data to decrypt\n\t */\n\tpublic decrypt(token: string): AuthData | null {\n\t\tconst [data, iv] = token.split('.');\n\t\tconst decipher = createDecipheriv('aes-256-cbc', this.#secret, Buffer.from(iv, 'base64'));\n\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(decipher.update(data, 'base64', 'utf8') + decipher.final('utf8')) as AuthData;\n\t\t\t// If the token expired, return null:\n\t\t\treturn parsed.expires >= Date.now() ? parsed : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Retrieves the data for a specific user.\n\t * @since 1.4.0\n\t * @param token The access token from the user.\n\t */\n\tpublic async fetchData(token: string): Promise<LoginData> {\n\t\t// Fetch the information:\n\t\tconst [user, guilds, connections] = await Promise.all([\n\t\t\tthis.fetchInformation<RESTGetAPICurrentUserResult>(OAuth2Scopes.Identify, token, `${RouteBases.api}${Routes.user()}`),\n\t\t\tthis.fetchInformation<RESTGetAPICurrentUserGuildsResult>(OAuth2Scopes.Guilds, token, `${RouteBases.api}${Routes.userGuilds()}`),\n\t\t\tthis.fetchInformation<RESTGetAPICurrentUserConnectionsResult>(\n\t\t\t\tOAuth2Scopes.Connections,\n\t\t\t\ttoken,\n\t\t\t\t`${RouteBases.api}${Routes.userConnections()}`\n\t\t\t)\n\t\t]);\n\n\t\t// Transform the information:\n\t\tlet data: LoginData = { user, guilds, connections };\n\t\tfor (const transformer of this.transformers) {\n\t\t\tconst result = transformer(data);\n\t\t\tif (isThenable(result)) data = await result;\n\t\t\telse data = result as LoginData;\n\t\t}\n\n\t\treturn data;\n\t}\n\n\tprivate async fetchInformation<T>(scope: OAuth2Scopes, token: string, url: string): Promise<T | null | undefined> {\n\t\tif (!this.scopes.includes(scope)) return undefined;\n\n\t\tconst result = await fetch(url, {\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${token}`\n\t\t\t}\n\t\t});\n\n\t\treturn result.ok ? ((await result.json()) as T) : null;\n\t}\n\n\tpublic static create(options?: ServerOptionsAuth): Auth | null {\n\t\tif (!options?.secret || !options.id) return null;\n\t\treturn new Auth(options);\n\t}\n}\n\n/**\n * Defines the authentication data, this is to be encrypted and decrypted by the server.\n * @since 1.0.0\n */\nexport interface AuthData {\n\t/**\n\t * The user ID.\n\t * @since 1.0.0\n\t */\n\tid: string;\n\n\t/**\n\t * The timestamp at which the token expires.\n\t * @since 1.0.0\n\t */\n\texpires: number;\n\n\t/**\n\t * The refresh token.\n\t * @since 1.0.0\n\t */\n\trefresh: string;\n\n\t/**\n\t * The access token.\n\t * @since 1.0.0\n\t */\n\ttoken: string;\n}\n\n/**\n * Defines the authentication options.\n * @since 1.0.0\n */\nexport interface ServerOptionsAuth {\n\t/**\n\t * The client's application id, this can be retrieved in Discord Developer Portal at https://discord.com/developers/applications.\n\t * @since 1.0.0\n\t */\n\tid: Snowflake;\n\n\t/**\n\t * The name for the cookie, this will be used to identify a Secure HttpOnly cookie.\n\t * @since 1.0.0\n\t * @default 'SAPPHIRE_AUTH'\n\t */\n\tcookie?: string;\n\n\t/**\n\t * The client secret, this can be retrieved in Discord Developer Portal at https://discord.com/developers/applications.\n\t * @since 1.0.0\n\t */\n\tsecret: string;\n\n\t/**\n\t * The scopes defined at https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes.\n\t * @since 1.0.0\n\t * @default [OAuth2Scopes.Identify]\n\t */\n\tscopes?: OAuth2Scopes[];\n\n\t/**\n\t * The redirect uri. This will default to {@link OAuth2BodyData.redirectUri} if missing.\n\t * @since 1.0.0\n\t */\n\tredirect?: string;\n\n\t/**\n\t * The login data transformers used for {@link Auth.fetchData}.\n\t * @since 1.4.0\n\t * @default []\n\t */\n\ttransformers?: LoginDataTransformer[];\n\t/**\n\t * The domain that should be used for the cookie. This overwrites the automatic detection of the domain.\n\t * @remark if you want to support subdomains (`one.example.two` and `two.example.com`) then you need to use prefix your domain with a `.`, for example `.example.com`\n\t * @since 2.1.0\n\t * @default undefined\n\t */\n\tdomainOverwrite?: string;\n}\n\n/**\n * The login data sent when fetching data from a user.\n * @since 1.4.0\n */\nexport interface LoginData {\n\t/**\n\t * The user data, defined when the `'identify'` scope is defined.\n\t * @since 1.4.0\n\t */\n\tuser?: RESTGetAPICurrentUserResult | null;\n\n\t/**\n\t * The guilds data, defined when the `'guilds'` scope is defined.\n\t * @since 1.4.0\n\t */\n\tguilds?: RESTGetAPICurrentUserGuildsResult | null;\n\n\t/**\n\t * The connections data, defined when the `'connections'` scope is defined.\n\t * @since 1.4.0\n\t */\n\tconnections?: RESTGetAPICurrentUserConnectionsResult | null;\n}\n\n/**\n * A login data transformer.\n * @since 1.4.0\n */\nexport interface LoginDataTransformer<T extends LoginData = LoginData> {\n\t/**\n\t * Transforms the object by mutating its properties or adding new ones.\n\t * @since 1.4.0\n\t */\n\t(data: LoginData): Awaitable<T>;\n}\n"]}