@stravigor/oauth2 0.4.5

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 ADDED
@@ -0,0 +1,72 @@
1
+ # @stravigor/oauth2
2
+
3
+ OAuth2 server for the [Strav](https://www.npmjs.com/package/@stravigor/core) framework. Authorization Code + PKCE, Client Credentials, Refresh Token rotation, Token Revocation (RFC 7009), Token Introspection (RFC 7662), personal access tokens, and scoped API access.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @stravigor/oauth2
9
+ bun strav package:install oauth2
10
+ ```
11
+
12
+ Requires `@stravigor/core` as a peer dependency.
13
+
14
+ ## Setup
15
+
16
+ ```ts
17
+ import { defineActions } from '@stravigor/oauth2'
18
+ import User from './models/user'
19
+
20
+ const actions = defineActions<User>({
21
+ async findById(id) { return User.find(id) },
22
+ identifierOf(user) { return user.email },
23
+ })
24
+ ```
25
+
26
+ ```ts
27
+ import { OAuth2Provider } from '@stravigor/oauth2'
28
+
29
+ app.use(new OAuth2Provider(actions))
30
+ ```
31
+
32
+ ```bash
33
+ bun strav oauth2:setup # Create tables + personal access client
34
+ bun strav oauth2:client --name "My App" --redirect "https://app.com/callback"
35
+ ```
36
+
37
+ ## Middleware
38
+
39
+ ```ts
40
+ import { oauth, scopes } from '@stravigor/oauth2'
41
+ import { compose } from '@stravigor/core/http/middleware'
42
+
43
+ router.group({ prefix: '/api', middleware: [oauth()] }, r => {
44
+ r.get('/user', ctx => ctx.json({ user: ctx.get('user') }))
45
+ r.get('/repos', compose([scopes('repos:read')], listRepos))
46
+ r.post('/repos', compose([scopes('repos:write')], createRepo))
47
+ })
48
+ ```
49
+
50
+ ## Personal Access Tokens
51
+
52
+ ```ts
53
+ import { oauth2 } from '@stravigor/oauth2'
54
+
55
+ const { token } = await oauth2.createPersonalToken(user, 'CLI Tool', ['read', 'write'])
56
+ ```
57
+
58
+ ## CLI
59
+
60
+ ```bash
61
+ bun strav oauth2:setup # Create tables and personal access client
62
+ bun strav oauth2:client # Create a new OAuth2 client
63
+ bun strav oauth2:purge # Clean up expired tokens and codes
64
+ ```
65
+
66
+ ## Documentation
67
+
68
+ See the full [OAuth2 guide](../../guides/oauth2.md).
69
+
70
+ ## License
71
+
72
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@stravigor/oauth2",
3
+ "version": "0.4.5",
4
+ "type": "module",
5
+ "description": "OAuth2 server implementation for the Strav framework",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./*": "./src/*.ts"
10
+ },
11
+ "strav": {
12
+ "commands": "src/commands"
13
+ },
14
+ "files": [
15
+ "src/",
16
+ "stubs/",
17
+ "package.json",
18
+ "tsconfig.json"
19
+ ],
20
+ "peerDependencies": {
21
+ "@stravigor/core": "0.4.4"
22
+ },
23
+ "scripts": {
24
+ "test": "bun test tests/",
25
+ "typecheck": "tsc --noEmit"
26
+ }
27
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { OAuth2Actions } from './types.ts'
2
+
3
+ /**
4
+ * Type-safe identity function for defining OAuth2 actions.
5
+ * Zero runtime cost — just provides autocompletion and type checking.
6
+ *
7
+ * @example
8
+ * import { defineActions } from '@stravigor/oauth2'
9
+ * import { User } from '../models/user'
10
+ *
11
+ * export default defineActions<User>({
12
+ * findById: (id) => User.find(id),
13
+ * identifierOf: (user) => user.email,
14
+ * })
15
+ */
16
+ export function defineActions<TUser = unknown>(
17
+ actions: OAuth2Actions<TUser>
18
+ ): OAuth2Actions<TUser> {
19
+ return actions
20
+ }
@@ -0,0 +1,154 @@
1
+ import OAuth2Manager from './oauth2_manager.ts'
2
+ import { randomHex } from '@stravigor/core/helpers'
3
+ import type { OAuthAuthCodeData } from './types.ts'
4
+
5
+ function hashCode(plain: string): string {
6
+ return new Bun.CryptoHasher('sha256').update(plain).digest('hex')
7
+ }
8
+
9
+ /**
10
+ * Static helper for managing OAuth2 authorization codes.
11
+ *
12
+ * Codes are SHA-256 hashed before storage and are single-use.
13
+ *
14
+ * @example
15
+ * const { code, codeData } = await AuthCode.create({ ... })
16
+ * const record = await AuthCode.consume(plainCode, clientId, redirectUri, codeVerifier)
17
+ */
18
+ export default class AuthCode {
19
+ /**
20
+ * Create a new authorization code.
21
+ * Returns the plain-text code (sent to client via redirect) and the DB record.
22
+ */
23
+ static async create(params: {
24
+ clientId: string
25
+ userId: string
26
+ redirectUri: string
27
+ scopes: string[]
28
+ codeChallenge?: string | null
29
+ codeChallengeMethod?: string | null
30
+ }): Promise<{ code: string; codeData: OAuthAuthCodeData }> {
31
+ const config = OAuth2Manager.config
32
+ const plainCode = randomHex(40)
33
+ const hashedCode = hashCode(plainCode)
34
+ const expiresAt = new Date(Date.now() + config.authCodeLifetime * 60_000)
35
+
36
+ const rows = await OAuth2Manager.db.sql`
37
+ INSERT INTO "_strav_oauth_auth_codes" (
38
+ "client_id", "user_id", "code", "redirect_uri", "scopes",
39
+ "code_challenge", "code_challenge_method", "expires_at"
40
+ )
41
+ VALUES (
42
+ ${params.clientId},
43
+ ${params.userId},
44
+ ${hashedCode},
45
+ ${params.redirectUri},
46
+ ${JSON.stringify(params.scopes)},
47
+ ${params.codeChallenge ?? null},
48
+ ${params.codeChallengeMethod ?? null},
49
+ ${expiresAt}
50
+ )
51
+ RETURNING *
52
+ `
53
+
54
+ return {
55
+ code: plainCode,
56
+ codeData: AuthCode.hydrate(rows[0] as Record<string, unknown>),
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Consume an authorization code. Validates and marks it as used.
62
+ *
63
+ * Checks:
64
+ * - Code exists and belongs to the client
65
+ * - Code is not expired
66
+ * - Code has not been used before
67
+ * - Redirect URI matches
68
+ * - PKCE code_verifier matches (if code_challenge was set)
69
+ *
70
+ * Returns the code data if valid, null otherwise.
71
+ */
72
+ static async consume(
73
+ plainCode: string,
74
+ clientId: string,
75
+ redirectUri: string,
76
+ codeVerifier?: string | null
77
+ ): Promise<OAuthAuthCodeData | null> {
78
+ const hash = hashCode(plainCode)
79
+
80
+ const rows = await OAuth2Manager.db.sql`
81
+ SELECT * FROM "_strav_oauth_auth_codes"
82
+ WHERE "code" = ${hash} AND "client_id" = ${clientId}
83
+ LIMIT 1
84
+ `
85
+ if (rows.length === 0) return null
86
+
87
+ const record = AuthCode.hydrate(rows[0] as Record<string, unknown>)
88
+
89
+ // Already used — potential replay attack
90
+ if (record.usedAt) return null
91
+
92
+ // Expired
93
+ if (record.expiresAt.getTime() < Date.now()) return null
94
+
95
+ // Redirect URI mismatch
96
+ if (record.redirectUri !== redirectUri) return null
97
+
98
+ // PKCE verification
99
+ if (record.codeChallenge) {
100
+ if (!codeVerifier) return null
101
+
102
+ if (record.codeChallengeMethod === 'S256') {
103
+ const verifierHash = new Bun.CryptoHasher('sha256').update(codeVerifier).digest('base64url')
104
+ if (verifierHash !== record.codeChallenge) return null
105
+ } else {
106
+ // plain method
107
+ if (codeVerifier !== record.codeChallenge) return null
108
+ }
109
+ }
110
+
111
+ // Mark as used
112
+ await OAuth2Manager.db.sql`
113
+ UPDATE "_strav_oauth_auth_codes"
114
+ SET "used_at" = NOW()
115
+ WHERE "id" = ${record.id}
116
+ `
117
+
118
+ return record
119
+ }
120
+
121
+ /** Prune expired and used auth codes. Returns the number of deleted rows. */
122
+ static async prune(): Promise<number> {
123
+ const result = await OAuth2Manager.db.sql`
124
+ DELETE FROM "_strav_oauth_auth_codes"
125
+ WHERE "expires_at" < NOW() OR "used_at" IS NOT NULL
126
+ `
127
+ return result.count ?? 0
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Internal
132
+ // ---------------------------------------------------------------------------
133
+
134
+ static hydrate(row: Record<string, unknown>): OAuthAuthCodeData {
135
+ return {
136
+ id: String(row.id),
137
+ clientId: String(row.client_id),
138
+ userId: row.user_id as string,
139
+ redirectUri: row.redirect_uri as string,
140
+ scopes: parseJsonb(row.scopes) as string[],
141
+ codeChallenge: (row.code_challenge as string) ?? null,
142
+ codeChallengeMethod: (row.code_challenge_method as string) ?? null,
143
+ expiresAt: row.expires_at as Date,
144
+ usedAt: (row.used_at as Date) ?? null,
145
+ createdAt: row.created_at as Date,
146
+ }
147
+ }
148
+ }
149
+
150
+ function parseJsonb(value: unknown): unknown {
151
+ if (value === null || value === undefined) return []
152
+ if (typeof value === 'string') return JSON.parse(value)
153
+ return value
154
+ }
package/src/client.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { timingSafeEqual as nodeTimingSafeEqual } from 'node:crypto'
2
+ import OAuth2Manager from './oauth2_manager.ts'
3
+ import { randomHex } from '@stravigor/core/helpers'
4
+ import type { OAuthClientData, CreateClientInput, GrantType } from './types.ts'
5
+
6
+ function hashSecret(plain: string): string {
7
+ return new Bun.CryptoHasher('sha256').update(plain).digest('hex')
8
+ }
9
+
10
+ /**
11
+ * Static helper for managing OAuth2 clients.
12
+ *
13
+ * Client secrets are SHA-256 hashed before storage — the plain-text
14
+ * secret is returned exactly once at creation time.
15
+ *
16
+ * @example
17
+ * const { client, plainSecret } = await OAuthClient.create({ name: 'Mobile App', redirectUris: ['myapp://callback'] })
18
+ * const client = await OAuthClient.find(id)
19
+ * await OAuthClient.revoke(id)
20
+ */
21
+ export default class OAuthClient {
22
+ /** Create a new OAuth client. Returns the client record and the plain secret (if confidential). */
23
+ static async create(input: CreateClientInput): Promise<{
24
+ client: OAuthClientData
25
+ plainSecret: string | null
26
+ }> {
27
+ const confidential = input.confidential ?? true
28
+ const firstParty = input.firstParty ?? false
29
+ const grantTypes = input.grantTypes ?? ['authorization_code', 'refresh_token']
30
+
31
+ let plainSecret: string | null = null
32
+ let hashedSecret: string | null = null
33
+
34
+ if (confidential) {
35
+ plainSecret = randomHex(32)
36
+ hashedSecret = hashSecret(plainSecret)
37
+ }
38
+
39
+ const rows = await OAuth2Manager.db.sql`
40
+ INSERT INTO "_strav_oauth_clients" (
41
+ "name", "secret", "redirect_uris", "scopes", "grant_types",
42
+ "confidential", "first_party", "revoked"
43
+ )
44
+ VALUES (
45
+ ${input.name},
46
+ ${hashedSecret},
47
+ ${JSON.stringify(input.redirectUris)},
48
+ ${input.scopes !== undefined ? JSON.stringify(input.scopes) : null},
49
+ ${JSON.stringify(grantTypes)},
50
+ ${confidential},
51
+ ${firstParty},
52
+ ${false}
53
+ )
54
+ RETURNING *
55
+ `
56
+
57
+ return {
58
+ client: OAuthClient.hydrate(rows[0] as Record<string, unknown>),
59
+ plainSecret,
60
+ }
61
+ }
62
+
63
+ /** Find a client by ID. Returns null if not found or revoked. */
64
+ static async find(id: string): Promise<OAuthClientData | null> {
65
+ const rows = await OAuth2Manager.db.sql`
66
+ SELECT * FROM "_strav_oauth_clients" WHERE "id" = ${id} LIMIT 1
67
+ `
68
+ if (rows.length === 0) return null
69
+ return OAuthClient.hydrate(rows[0] as Record<string, unknown>)
70
+ }
71
+
72
+ /** Find a client by ID, including revoked clients. */
73
+ static async findIncludingRevoked(id: string): Promise<OAuthClientData | null> {
74
+ const rows = await OAuth2Manager.db.sql`
75
+ SELECT * FROM "_strav_oauth_clients" WHERE "id" = ${id} LIMIT 1
76
+ `
77
+ if (rows.length === 0) return null
78
+ return OAuthClient.hydrate(rows[0] as Record<string, unknown>)
79
+ }
80
+
81
+ /** Verify a plain-text client secret against the stored hash. */
82
+ static async verifySecret(client: OAuthClientData, plainSecret: string): Promise<boolean> {
83
+ const rows = await OAuth2Manager.db.sql`
84
+ SELECT "secret" FROM "_strav_oauth_clients" WHERE "id" = ${client.id} LIMIT 1
85
+ `
86
+ if (rows.length === 0) return false
87
+ const stored = (rows[0] as Record<string, unknown>).secret as string | null
88
+ if (!stored) return false
89
+
90
+ const hash = hashSecret(plainSecret)
91
+ return timingSafeEqual(stored, hash)
92
+ }
93
+
94
+ /** List all non-revoked clients. */
95
+ static async all(): Promise<OAuthClientData[]> {
96
+ const rows = await OAuth2Manager.db.sql`
97
+ SELECT * FROM "_strav_oauth_clients" WHERE "revoked" = false ORDER BY "created_at" DESC
98
+ `
99
+ return (rows as Record<string, unknown>[]).map(OAuthClient.hydrate)
100
+ }
101
+
102
+ /** List all clients belonging to a user (clients they created). */
103
+ static async allForUser(userId: string): Promise<OAuthClientData[]> {
104
+ // All non-revoked clients visible to the user
105
+ return OAuthClient.all()
106
+ }
107
+
108
+ /** Soft-revoke a client. */
109
+ static async revoke(id: string): Promise<void> {
110
+ await OAuth2Manager.db.sql`
111
+ UPDATE "_strav_oauth_clients" SET "revoked" = true, "updated_at" = NOW()
112
+ WHERE "id" = ${id}
113
+ `
114
+ }
115
+
116
+ /** Hard-delete a client and all its tokens/codes. */
117
+ static async destroy(id: string): Promise<void> {
118
+ const db = OAuth2Manager.db
119
+ await db.sql`DELETE FROM "_strav_oauth_auth_codes" WHERE "client_id" = ${id}`
120
+ await db.sql`DELETE FROM "_strav_oauth_tokens" WHERE "client_id" = ${id}`
121
+ await db.sql`DELETE FROM "_strav_oauth_clients" WHERE "id" = ${id}`
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Internal
126
+ // ---------------------------------------------------------------------------
127
+
128
+ static hydrate(row: Record<string, unknown>): OAuthClientData {
129
+ return {
130
+ id: String(row.id),
131
+ name: row.name as string,
132
+ redirectUris: parseJsonb(row.redirect_uris) as string[],
133
+ scopes: parseJsonb(row.scopes) as string[] | null,
134
+ grantTypes: parseJsonb(row.grant_types) as GrantType[],
135
+ confidential: row.confidential as boolean,
136
+ firstParty: row.first_party as boolean,
137
+ revoked: row.revoked as boolean,
138
+ createdAt: row.created_at as Date,
139
+ updatedAt: row.updated_at as Date,
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Timing-safe string comparison. */
145
+ function timingSafeEqual(a: string, b: string): boolean {
146
+ if (a.length !== b.length) return false
147
+ return nodeTimingSafeEqual(Buffer.from(a), Buffer.from(b))
148
+ }
149
+
150
+ /** Parse a JSONB column that may already be an object or a string. */
151
+ function parseJsonb(value: unknown): unknown {
152
+ if (value === null || value === undefined) return null
153
+ if (typeof value === 'string') return JSON.parse(value)
154
+ return value
155
+ }
@@ -0,0 +1,152 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { bootstrap, shutdown } from '@stravigor/core/cli/bootstrap'
4
+ import OAuth2Manager from '../oauth2_manager.ts'
5
+ import OAuthClient from '../client.ts'
6
+ import OAuthToken from '../token.ts'
7
+ import AuthCode from '../auth_code.ts'
8
+
9
+ export function register(program: Command): void {
10
+ // ── oauth2:setup ────────────────────────────────────────────────────
11
+
12
+ program
13
+ .command('oauth2:setup')
14
+ .description('Create OAuth2 tables and a default personal access client')
15
+ .action(async () => {
16
+ let db
17
+ try {
18
+ const { db: database, config } = await bootstrap()
19
+ db = database
20
+
21
+ new OAuth2Manager(db, config)
22
+
23
+ console.log(chalk.dim('Creating OAuth2 tables...'))
24
+ await OAuth2Manager.ensureTables()
25
+ console.log(chalk.green('OAuth2 tables created successfully.'))
26
+
27
+ // Create personal access client if not already configured
28
+ const patClientId = OAuth2Manager.config.personalAccessClient
29
+ if (!patClientId) {
30
+ console.log(chalk.dim('Creating personal access client...'))
31
+ const { client } = await OAuthClient.create({
32
+ name: 'Personal Access Client',
33
+ redirectUris: [],
34
+ confidential: true,
35
+ firstParty: true,
36
+ grantTypes: [],
37
+ })
38
+ console.log(chalk.green(`Personal access client created: ${chalk.bold(client.id)}`))
39
+ console.log(
40
+ chalk.yellow(
41
+ `\nAdd this to your config/oauth2.ts:\n personalAccessClient: '${client.id}'`
42
+ )
43
+ )
44
+ } else {
45
+ console.log(chalk.dim(`Personal access client already configured: ${patClientId}`))
46
+ }
47
+ } catch (err) {
48
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
49
+ process.exit(1)
50
+ } finally {
51
+ if (db) await shutdown(db)
52
+ }
53
+ })
54
+
55
+ // ── oauth2:client ───────────────────────────────────────────────────
56
+
57
+ program
58
+ .command('oauth2:client')
59
+ .description('Create a new OAuth2 client')
60
+ .requiredOption('--name <name>', 'Client name')
61
+ .option('--redirect <uris...>', 'Redirect URIs', [])
62
+ .option('--public', 'Create a public (non-confidential) client', false)
63
+ .option('--first-party', 'Mark as a first-party (trusted) client', false)
64
+ .option('--credentials', 'Enable client_credentials grant', false)
65
+ .action(
66
+ async (options: {
67
+ name: string
68
+ redirect: string[]
69
+ public: boolean
70
+ firstParty: boolean
71
+ credentials: boolean
72
+ }) => {
73
+ let db
74
+ try {
75
+ const { db: database, config } = await bootstrap()
76
+ db = database
77
+
78
+ new OAuth2Manager(db, config)
79
+ await OAuth2Manager.ensureTables()
80
+
81
+ const grantTypes = ['authorization_code', 'refresh_token'] as (
82
+ | 'authorization_code'
83
+ | 'client_credentials'
84
+ | 'refresh_token'
85
+ )[]
86
+ if (options.credentials) {
87
+ grantTypes.push('client_credentials')
88
+ }
89
+
90
+ const { client, plainSecret } = await OAuthClient.create({
91
+ name: options.name,
92
+ redirectUris: options.redirect,
93
+ confidential: !options.public,
94
+ firstParty: options.firstParty,
95
+ grantTypes,
96
+ })
97
+
98
+ console.log(chalk.green('\nOAuth2 client created successfully.\n'))
99
+ console.log(` ${chalk.dim('Client ID:')} ${chalk.bold(client.id)}`)
100
+
101
+ if (plainSecret) {
102
+ console.log(` ${chalk.dim('Client Secret:')} ${chalk.bold(plainSecret)}`)
103
+ console.log(chalk.yellow('\n Store the secret securely — it will not be shown again.'))
104
+ } else {
105
+ console.log(` ${chalk.dim('Type:')} Public (no secret)`)
106
+ }
107
+
108
+ console.log(
109
+ ` ${chalk.dim('Redirect URIs:')} ${client.redirectUris.join(', ') || '(none)'}`
110
+ )
111
+ console.log(` ${chalk.dim('Grant Types:')} ${client.grantTypes.join(', ')}`)
112
+ console.log(` ${chalk.dim('First Party:')} ${client.firstParty}`)
113
+ } catch (err) {
114
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
115
+ process.exit(1)
116
+ } finally {
117
+ if (db) await shutdown(db)
118
+ }
119
+ }
120
+ )
121
+
122
+ // ── oauth2:purge ────────────────────────────────────────────────────
123
+
124
+ program
125
+ .command('oauth2:purge')
126
+ .description('Purge expired tokens and used authorization codes')
127
+ .option('--days <days>', 'Delete revoked tokens older than N days', '7')
128
+ .action(async (options: { days: string }) => {
129
+ let db
130
+ try {
131
+ const { db: database, config } = await bootstrap()
132
+ db = database
133
+
134
+ new OAuth2Manager(db, config)
135
+
136
+ const days = parseInt(options.days, 10)
137
+
138
+ console.log(chalk.dim('Purging expired tokens...'))
139
+ const tokenCount = await OAuthToken.prune(days)
140
+ console.log(chalk.green(` ${tokenCount} token(s) pruned.`))
141
+
142
+ console.log(chalk.dim('Purging used/expired authorization codes...'))
143
+ const codeCount = await AuthCode.prune()
144
+ console.log(chalk.green(` ${codeCount} authorization code(s) pruned.`))
145
+ } catch (err) {
146
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
147
+ process.exit(1)
148
+ } finally {
149
+ if (db) await shutdown(db)
150
+ }
151
+ })
152
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { StravError } from '@stravigor/core/exceptions/strav_error'
2
+
3
+ /** Base error for all OAuth2 errors. */
4
+ export class OAuth2Error extends StravError {
5
+ constructor(
6
+ message: string,
7
+ public readonly errorCode: string = 'server_error',
8
+ public readonly statusCode: number = 400
9
+ ) {
10
+ super(message)
11
+ }
12
+
13
+ /** Build a JSON response matching RFC 6749 error format. */
14
+ toJSON(): Record<string, string> {
15
+ return {
16
+ error: this.errorCode,
17
+ error_description: this.message,
18
+ }
19
+ }
20
+ }
21
+
22
+ /** The authorization grant type is not supported. */
23
+ export class UnsupportedGrantError extends OAuth2Error {
24
+ constructor() {
25
+ super('The authorization grant type is not supported.', 'unsupported_grant_type')
26
+ }
27
+ }
28
+
29
+ /** Client authentication failed. */
30
+ export class InvalidClientError extends OAuth2Error {
31
+ constructor(message = 'Client authentication failed.') {
32
+ super(message, 'invalid_client', 401)
33
+ }
34
+ }
35
+
36
+ /** The provided authorization grant is invalid or expired. */
37
+ export class InvalidGrantError extends OAuth2Error {
38
+ constructor(message = 'The provided authorization grant is invalid, expired, or revoked.') {
39
+ super(message, 'invalid_grant')
40
+ }
41
+ }
42
+
43
+ /** The request is missing a required parameter or is malformed. */
44
+ export class InvalidRequestError extends OAuth2Error {
45
+ constructor(message = 'The request is missing a required parameter.') {
46
+ super(message, 'invalid_request')
47
+ }
48
+ }
49
+
50
+ /** The requested scope is invalid or unknown. */
51
+ export class InvalidScopeError extends OAuth2Error {
52
+ constructor(message = 'The requested scope is invalid or unknown.') {
53
+ super(message, 'invalid_scope')
54
+ }
55
+ }
56
+
57
+ /** The resource owner denied the authorization request. */
58
+ export class AccessDeniedError extends OAuth2Error {
59
+ constructor(message = 'The resource owner denied the request.') {
60
+ super(message, 'access_denied', 403)
61
+ }
62
+ }