Skip to content

Commit a7ddb74

Browse files
feat: standardize error response (#18)
* feat: standardize error response * fix: tests * feat: creating constant error names * feat: creating 'MissingDefault' error variantion for api keys * feat: creating error map * fix: wrong error code in EnvErrorMap * test: fix tests to use error types * fix: lint * docs: fix typos and improve inline docs for error constants Fix typos in EnvError.code TSDoc, update stale AuthError.code reference, add doc comments to all exported error constants and Errors map, and re-export error constants from package root. --------- Co-authored-by: Tomas Pozo <tomaspozogarzon@gmail.com>
1 parent 5e53e3c commit a7ddb74

16 files changed

Lines changed: 181 additions & 64 deletions

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ Extracts credentials from a Request and validates against the allow config.
189189
```ts
190190
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
191191
if (error) {
192-
return Response.json({ error: error.message }, { status: error.status })
192+
return Response.json({ message: error.message }, { status: error.status })
193193
}
194194
```
195195
@@ -246,7 +246,10 @@ export default {
246246
if (url.pathname === '/todos') {
247247
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
248248
if (error)
249-
return Response.json({ error: error.message }, { status: error.status })
249+
return Response.json(
250+
{ message: error.message },
251+
{ status: error.status },
252+
)
250253
251254
const supabase = createContextClient(auth.token)
252255
const { data } = await supabase.from('todos').select()

src/core/create-admin-client.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { EnvError } from '../errors.js'
3+
import {
4+
EnvError,
5+
MissingDefaultSecretKeyError,
6+
MissingSecretKeyError,
7+
} from '../errors.js'
48
import { createAdminClient } from './create-admin-client.js'
59

610
const validEnv = {
@@ -46,7 +50,7 @@ describe('createAdminClient', () => {
4650
})
4751
} catch (e) {
4852
expect(e).toBeInstanceOf(EnvError)
49-
expect((e as EnvError).code).toBe('MISSING_SECRET_KEY')
53+
expect((e as EnvError).code).toBe(MissingDefaultSecretKeyError)
5054
}
5155
})
5256

@@ -72,7 +76,7 @@ describe('createAdminClient', () => {
7276
createAdminClient(validEnv, 'nonexistent')
7377
} catch (e) {
7478
expect(e).toBeInstanceOf(EnvError)
75-
expect((e as EnvError).code).toBe('MISSING_SECRET_KEY')
79+
expect((e as EnvError).code).toBe(MissingSecretKeyError)
7680
}
7781
})
7882

src/core/create-admin-client.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
22

3-
import { EnvError } from '../errors.js'
3+
import {
4+
Errors,
5+
MissingDefaultSecretKeyError,
6+
MissingSecretKeyError,
7+
} from '../errors.js'
48
import type { SupabaseEnv } from '../types.js'
59
import { resolveEnv } from './resolve-env.js'
610

@@ -33,11 +37,9 @@ export function createAdminClient<Database = unknown>(
3337
const secretKey =
3438
keys[name] ?? (keyName == null ? Object.values(keys)[0] : undefined)
3539
if (!secretKey) {
36-
const msg =
37-
name === 'default'
38-
? 'No default secret key found. Set SUPABASE_SECRET_KEY or include a "default" entry in SUPABASE_SECRET_KEYS.'
39-
: `No "${name}" secret key found. Include a "${name}" entry in SUPABASE_SECRET_KEYS.`
40-
throw new EnvError(msg, 'MISSING_SECRET_KEY')
40+
throw name === 'default'
41+
? Errors[MissingDefaultSecretKeyError]()
42+
: Errors[MissingSecretKeyError](name)
4143
}
4244

4345
return createClient(resolved.url, secretKey, {

src/core/create-context-client.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { EnvError } from '../errors.js'
3+
import {
4+
EnvError,
5+
MissingDefaultPublishableKeyError,
6+
MissingPublishableKeyError,
7+
} from '../errors.js'
48
import { createContextClient } from './create-context-client.js'
59

610
const validEnv = {
@@ -46,7 +50,7 @@ describe('createContextClient', () => {
4650
})
4751
} catch (e) {
4852
expect(e).toBeInstanceOf(EnvError)
49-
expect((e as EnvError).code).toBe('MISSING_PUBLISHABLE_KEY')
53+
expect((e as EnvError).code).toBe(MissingDefaultPublishableKeyError)
5054
}
5155
})
5256

@@ -74,7 +78,7 @@ describe('createContextClient', () => {
7478
createContextClient('test-token', validEnv, 'nonexistent')
7579
} catch (e) {
7680
expect(e).toBeInstanceOf(EnvError)
77-
expect((e as EnvError).code).toBe('MISSING_PUBLISHABLE_KEY')
81+
expect((e as EnvError).code).toBe(MissingPublishableKeyError)
7882
}
7983
})
8084

src/core/create-context-client.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
22

3-
import { EnvError } from '../errors.js'
3+
import {
4+
Errors,
5+
MissingDefaultPublishableKeyError,
6+
MissingPublishableKeyError,
7+
} from '../errors.js'
48
import type { SupabaseEnv } from '../types.js'
59
import { resolveEnv } from './resolve-env.js'
610

@@ -37,11 +41,9 @@ export function createContextClient<Database = unknown>(
3741
const anonKey =
3842
keys[name] ?? (keyName == null ? Object.values(keys)[0] : undefined)
3943
if (!anonKey) {
40-
const msg =
41-
name === 'default'
42-
? 'No default publishable key found. Set SUPABASE_PUBLISHABLE_KEY or include a "default" entry in SUPABASE_PUBLISHABLE_KEYS.'
43-
: `No "${name}" publishable key found. Include a "${name}" entry in SUPABASE_PUBLISHABLE_KEYS.`
44-
throw new EnvError(msg, 'MISSING_PUBLISHABLE_KEY')
44+
throw name === 'default'
45+
? Errors[MissingDefaultPublishableKeyError]()
46+
: Errors[MissingPublishableKeyError](name)
4547
}
4648

4749
return createClient(resolved.url, anonKey, {

src/core/resolve-env.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest'
22

33
import { resolveEnv } from './resolve-env.js'
4+
import { MissingSupabaseURLError } from '../errors.js'
45

56
describe('resolveEnv', () => {
67
afterEach(() => {
@@ -10,7 +11,7 @@ describe('resolveEnv', () => {
1011
it('returns error when SUPABASE_URL is missing', () => {
1112
const result = resolveEnv()
1213
expect(result.error).not.toBeNull()
13-
expect(result.error!.code).toBe('MISSING_SUPABASE_URL')
14+
expect(result.error!.code).toBe(MissingSupabaseURLError)
1415
})
1516

1617
it('reads SUPABASE_URL from process.env', () => {

src/core/resolve-env.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EnvError } from '../errors.js'
1+
import { EnvError, Errors, MissingSupabaseURLError } from '../errors.js'
22
import type { JsonWebKeySet, SupabaseEnv } from '../types.js'
33

44
/**
@@ -104,10 +104,7 @@ export function resolveEnv(
104104
if (!url) {
105105
return {
106106
data: null,
107-
error: new EnvError(
108-
'SUPABASE_URL is required but not set',
109-
'MISSING_SUPABASE_URL',
110-
),
107+
error: Errors[MissingSupabaseURLError](),
111108
}
112109
}
113110

src/core/verify-auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ interface VerifyAuthOptions {
4141
* })
4242
*
4343
* if (error) {
44-
* return Response.json({ error: error.message }, { status: error.status })
44+
* return Response.json({ message: error.message }, { status: error.status })
4545
* }
4646
*
4747
* console.log(auth.userClaims!.id) // "d0f1a2b3-..."

src/core/verify-credentials.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { beforeAll, describe, expect, it } from 'vitest'
33

44
import type { Credentials, JsonWebKeySet, SupabaseEnv } from '../types.js'
55
import { verifyCredentials } from './verify-credentials.js'
6+
import { InvalidCredentialsError } from '../errors.js'
67

78
function makeEnv(overrides?: Partial<SupabaseEnv>): Partial<SupabaseEnv> {
89
return {
@@ -50,7 +51,7 @@ describe('verifyCredentials', () => {
5051
env: makeEnv(),
5152
})
5253
expect(result.error).not.toBeNull()
53-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
54+
expect(result.error!.code).toBe(InvalidCredentialsError)
5455
})
5556

5657
it('only matches default key when bare public is used', async () => {
@@ -66,7 +67,7 @@ describe('verifyCredentials', () => {
6667
env,
6768
})
6869
expect(result.error).not.toBeNull()
69-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
70+
expect(result.error!.code).toBe(InvalidCredentialsError)
7071
})
7172

7273
it('matches named key with colon syntax and returns keyName', async () => {
@@ -101,7 +102,7 @@ describe('verifyCredentials', () => {
101102
env,
102103
})
103104
expect(result.error).not.toBeNull()
104-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
105+
expect(result.error!.code).toBe(InvalidCredentialsError)
105106
})
106107

107108
it('rejects wrong named key type', async () => {
@@ -117,7 +118,7 @@ describe('verifyCredentials', () => {
117118
env,
118119
})
119120
expect(result.error).not.toBeNull()
120-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
121+
expect(result.error!.code).toBe(InvalidCredentialsError)
121122
})
122123

123124
it('matches any key with wildcard syntax', async () => {
@@ -179,7 +180,7 @@ describe('verifyCredentials', () => {
179180
env: makeEnv(),
180181
})
181182
expect(result.error).not.toBeNull()
182-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
183+
expect(result.error!.code).toBe(InvalidCredentialsError)
183184
})
184185

185186
it('only matches default key when bare secret is used', async () => {
@@ -192,7 +193,7 @@ describe('verifyCredentials', () => {
192193
env,
193194
})
194195
expect(result.error).not.toBeNull()
195-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
196+
expect(result.error!.code).toBe(InvalidCredentialsError)
196197
})
197198

198199
it('matches secret named key with colon syntax and returns keyName', async () => {
@@ -218,7 +219,7 @@ describe('verifyCredentials', () => {
218219
env,
219220
})
220221
expect(result.error).not.toBeNull()
221-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
222+
expect(result.error!.code).toBe(InvalidCredentialsError)
222223
})
223224

224225
it('rejects wrong secret named key type', async () => {
@@ -231,7 +232,7 @@ describe('verifyCredentials', () => {
231232
env,
232233
})
233234
expect(result.error).not.toBeNull()
234-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
235+
expect(result.error!.code).toBe(InvalidCredentialsError)
235236
})
236237

237238
it('matches any key with wildcard syntax', async () => {
@@ -309,7 +310,7 @@ describe('verifyCredentials', () => {
309310
env: makeEnv({ jwks }),
310311
})
311312
expect(result.error).not.toBeNull()
312-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
313+
expect(result.error!.code).toBe(InvalidCredentialsError)
313314
})
314315

315316
it('fails with no token', async () => {
@@ -319,7 +320,7 @@ describe('verifyCredentials', () => {
319320
env: makeEnv({ jwks }),
320321
})
321322
expect(result.error).not.toBeNull()
322-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
323+
expect(result.error!.code).toBe(InvalidCredentialsError)
323324
})
324325

325326
it('fails with expired JWT', async () => {
@@ -341,7 +342,7 @@ describe('verifyCredentials', () => {
341342
env: makeEnv({ jwks: expiredJwks }),
342343
})
343344
expect(result.error).not.toBeNull()
344-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
345+
expect(result.error!.code).toBe(InvalidCredentialsError)
345346
})
346347
})
347348

@@ -386,7 +387,7 @@ describe('verifyCredentials', () => {
386387
env,
387388
})
388389
expect(result.error).not.toBeNull()
389-
expect(result.error!.code).toBe('INVALID_CREDENTIALS')
390+
expect(result.error!.code).toBe(InvalidCredentialsError)
390391
})
391392
})
392393

src/core/verify-credentials.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLocalJWKSet, jwtVerify } from 'jose'
22

3-
import { AuthError } from '../errors.js'
3+
import { AuthError, Errors, InvalidCredentialsError } from '../errors.js'
44
import type {
55
Allow,
66
AllowWithKey,
@@ -203,7 +203,7 @@ async function tryMode(
203203
* allow: ['user', 'public'],
204204
* })
205205
* if (error) {
206-
* return Response.json({ error: error.message }, { status: error.status })
206+
* return Response.json({ message: error.message }, { status: error.status })
207207
* }
208208
* ```
209209
*/
@@ -232,6 +232,6 @@ export async function verifyCredentials(
232232

233233
return {
234234
data: null,
235-
error: new AuthError('Invalid credentials', 'INVALID_CREDENTIALS', 401),
235+
error: Errors[InvalidCredentialsError](),
236236
}
237237
}

0 commit comments

Comments
 (0)