feat(clerk-auth): Migrate to Clerk Core v2 (#465)

* chore(valibot-validator): Replace `jsonT` in tests

* fix(valibot-validator): Handle optional schema

* test(valibot-validator): Update tests

* chore(valibot-validator): Add changeset

* chore(valibot-validator): Fix formatting

* remove old changeset

* feat(clerk-auth): Migrate to Clerk Core v2

* chore: Add changeset

* fix: Add back tsup devDep

* chore: Fix lockfile

* chore: infer clerkAuth type instead of importing it directly

* chore: refactor redirect handling

* chore: remove unnecessary `rimraf` devDep

* chore: rewrite clerk devDeps versions

* chore: fix peerDeps

* chore: update lockfile

* drop `@clerk/shared` peerDep
pull/474/head
Andrei 2024-04-25 15:43:36 +02:00 committed by GitHub
parent 173e4ef01c
commit 1823a28627
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 85 additions and 235 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/clerk-auth': major
---
Migrate to Clerk Core v2

View File

@ -5,8 +5,4 @@ module.exports = {
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'],
transform: { '^.+\\.m?tsx?$': 'ts-jest' },
testPathIgnorePatterns: ['/node_modules/', '/jest/'],
moduleNameMapper: {
'#crypto': '@clerk/backend/dist/runtime/node/crypto.js',
'#fetch': '@clerk/backend/dist/runtime/node/fetch.js',
},
}

View File

@ -38,16 +38,19 @@
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"@clerk/backend": ">=0.30.0 <1",
"@clerk/backend": "^1.0.0",
"hono": ">=3.*"
},
"devDependencies": {
"@clerk/backend": "^0.30.1",
"@clerk/backend": "^1.0.0",
"@types/react": "^18",
"hono": "^3.11.7",
"jest": "^29.7.0",
"node-fetch-native": "^1.4.0",
"react": "^18.2.0",
"tsup": "^8.0.1"
},
"engines": {
"node": ">=16.x.x"
}
}

View File

@ -1,14 +1,13 @@
import type { ClerkOptions } from '@clerk/backend'
import { Clerk, createIsomorphicRequest, constants } from '@clerk/backend'
import { type ClerkClient, type ClerkOptions, createClerkClient } from '@clerk/backend'
import type { Context, MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter'
type ClerkAuth = Awaited<ReturnType<ReturnType<typeof Clerk>['authenticateRequest']>>['toAuth']
type ClerkAuth = ReturnType<Awaited<ReturnType<ClerkClient['authenticateRequest']>>['toAuth']>
declare module 'hono' {
interface ContextVariableMap {
clerk: ReturnType<typeof Clerk>
clerkAuth: ReturnType<ClerkAuth>
clerk: ClerkClient
clerkAuth: ClerkAuth
}
}
@ -21,7 +20,6 @@ type ClerkEnv = {
CLERK_PUBLISHABLE_KEY: string
CLERK_API_URL: string
CLERK_API_VERSION: string
CLERK_FRONTEND_API: string
}
export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
@ -30,10 +28,9 @@ export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
const { secretKey, publishableKey, apiUrl, apiVersion, ...rest } = options || {
secretKey: clerkEnv.CLERK_SECRET_KEY || '',
publishableKey: clerkEnv.CLERK_PUBLISHABLE_KEY || '',
apiUrl: clerkEnv.CLERK_API_URL || 'https://api.clerk.dev',
apiVersion: clerkEnv.CLERK_API_VERSION || 'v1',
apiUrl: clerkEnv.CLERK_API_URL,
apiVersion: clerkEnv.CLERK_API_VERSION,
}
const frontendApi = clerkEnv.CLERK_FRONTEND_API || ''
if (!secretKey) {
throw new Error('Missing Clerk Secret key')
}
@ -42,7 +39,7 @@ export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
throw new Error('Missing Clerk Publishable key')
}
const clerkClient = Clerk({
const clerkClient = createClerkClient({
...rest,
apiUrl,
apiVersion,
@ -50,35 +47,22 @@ export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
publishableKey,
})
const requestState = await clerkClient.authenticateRequest({
const requestState = await clerkClient.authenticateRequest(c.req.raw, {
...rest,
secretKey,
publishableKey,
request: createIsomorphicRequest((Request) => {
return new Request(c.req.url, {
method: c.req.method,
headers: c.req.raw.headers,
})
}),
})
// Interstitial cases
if (requestState.isUnknown) {
c.header(constants.Headers.AuthReason, requestState.reason)
c.header(constants.Headers.AuthMessage, requestState.message)
return c.body(null, 401)
}
if (requestState.headers) {
requestState.headers.forEach((value, key) => c.res.headers.append(key, value))
if (requestState.isInterstitial) {
const interstitialHtmlPage = clerkClient.localInterstitial({
publishableKey,
frontendApi,
})
c.header(constants.Headers.AuthReason, requestState.reason)
c.header(constants.Headers.AuthMessage, requestState.message)
return c.html(interstitialHtmlPage, 401)
const locationHeader = requestState.headers.get('location')
if (locationHeader) {
return c.redirect(locationHeader, 307)
} else if (requestState.status === 'handshake') {
throw new Error('Clerk: unexpected handshake without redirect')
}
}
c.set('clerkAuth', requestState.toAuth())

View File

@ -8,15 +8,13 @@ const EnvVariables = {
}
const authenticateRequestMock = jest.fn()
const localInterstitialMock = jest.fn()
jest.mock('@clerk/backend', () => {
return {
...jest.requireActual('@clerk/backend'),
Clerk: () => {
createClerkClient: () => {
return {
authenticateRequest: (...args: any) => authenticateRequestMock(...args),
localInterstitial: (...args: any) => localInterstitialMock(...args),
}
},
}
@ -36,10 +34,8 @@ describe('clerkMiddleware()', () => {
})
test('handles signin with Authorization Bearer', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: false,
isSignedIn: true,
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
@ -67,20 +63,17 @@ describe('clerkMiddleware()', () => {
expect(response.status).toEqual(200)
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
expect(authenticateRequestMock).toBeCalledWith(
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
secretKey: EnvVariables.CLERK_SECRET_KEY,
publishableKey: EnvVariables.CLERK_PUBLISHABLE_KEY,
request: expect.any(Request),
})
}),
)
})
test('handles signin with cookie', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: false,
isSignedIn: true,
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
@ -108,22 +101,25 @@ describe('clerkMiddleware()', () => {
expect(response.status).toEqual(200)
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
expect(authenticateRequestMock).toBeCalledWith(
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
secretKey: EnvVariables.CLERK_SECRET_KEY,
publishableKey: EnvVariables.CLERK_PUBLISHABLE_KEY,
request: expect.any(Request),
})
}),
)
})
test('handles unknown case by terminating the request with empty response and 401 http code', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: true,
isInterstitial: false,
isSignedIn: false,
test('handles handshake case by redirecting the request to fapi', async () => {
authenticateRequestMock.mockResolvedValueOnce({
status: 'handshake',
reason: 'auth-reason',
message: 'auth-message',
headers: new Headers({
location: 'https://fapi.example.com/v1/clients/handshake',
'x-clerk-auth-message': 'auth-message',
'x-clerk-auth-reason': 'auth-reason',
'x-clerk-auth-status': 'handshake',
}),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
@ -142,50 +138,18 @@ describe('clerkMiddleware()', () => {
const response = await app.request(req)
expect(response.status).toEqual(401)
expect(response.headers.get('x-clerk-auth-reason')).toEqual('auth-reason')
expect(response.headers.get('x-clerk-auth-message')).toEqual('auth-message')
expect(await response.text()).toEqual('')
})
test('handles interstitial case by terminating the request with interstitial html page and 401 http code', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: true,
isSignedIn: false,
reason: 'auth-reason',
message: 'auth-message',
toAuth: () => 'mockedAuth',
expect(response.status).toEqual(307)
expect(Object.fromEntries(response.headers.entries())).toMatchObject({
location: 'https://fapi.example.com/v1/clients/handshake',
'x-clerk-auth-status': 'handshake',
'x-clerk-auth-reason': 'auth-reason',
'x-clerk-auth-message': 'auth-message',
})
localInterstitialMock.mockReturnValue('<html><body>Interstitial</body></html>')
const app = new Hono()
app.use('*', clerkMiddleware())
app.get('/', (ctx) => {
const auth = getAuth(ctx)
return ctx.json({ auth })
})
const req = new Request('http://localhost/', {
headers: {
cookie: '_gcl_au=value1; ko_id=value2; __session=deadbeef; __client_uat=1675692233',
},
})
const response = await app.request(req)
expect(response.status).toEqual(401)
expect(response.headers.get('content-type')).toMatch('text/html')
expect(response.headers.get('x-clerk-auth-reason')).toEqual('auth-reason')
expect(response.headers.get('x-clerk-auth-message')).toEqual('auth-message')
expect(await response.text()).toEqual('<html><body>Interstitial</body></html>')
})
test('handles signout case by populating the req.auth', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: false,
isSignedIn: false,
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
@ -206,12 +170,11 @@ describe('clerkMiddleware()', () => {
expect(response.status).toEqual(200)
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
expect(authenticateRequestMock).toBeCalledWith(
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
secretKey: EnvVariables.CLERK_SECRET_KEY,
publishableKey: EnvVariables.CLERK_PUBLISHABLE_KEY,
request: expect.any(Request),
})
}),
)
})
})

155
yarn.lock
View File

@ -788,42 +788,35 @@ __metadata:
languageName: node
linkType: hard
"@clerk/backend@npm:^0.30.1":
version: 0.30.3
resolution: "@clerk/backend@npm:0.30.3"
"@clerk/backend@npm:^1.0.0":
version: 1.0.0
resolution: "@clerk/backend@npm:1.0.0"
dependencies:
"@clerk/shared": "npm:0.24.3"
"@clerk/types": "npm:3.54.0"
"@peculiar/webcrypto": "npm:1.4.1"
"@types/node": "npm:16.18.6"
"@clerk/shared": "npm:2.0.0"
cookie: "npm:0.5.0"
deepmerge: "npm:4.2.2"
node-fetch-native: "npm:1.0.1"
snakecase-keys: "npm:5.4.4"
tslib: "npm:2.4.1"
checksum: 558a15525d1a5f90a505e607408ed902f5e06f5c126c95fa4bf69623ec493276ff9ce199e3f2d47516fc0f324ca20bf021b5909be405c31e0f7123fdfa172c64
checksum: 95c03aabba87abd60427aa59e91706a61075cc00ad02ef3dd3760abe76761a4358c5e45d1c17c8f85e8ec859a60fff66b6d0f9beeaadc51e4c2bbc0f2af77af9
languageName: node
linkType: hard
"@clerk/shared@npm:0.24.3":
version: 0.24.3
resolution: "@clerk/shared@npm:0.24.3"
"@clerk/shared@npm:2.0.0":
version: 2.0.0
resolution: "@clerk/shared@npm:2.0.0"
dependencies:
glob-to-regexp: "npm:0.4.1"
js-cookie: "npm:3.0.1"
std-env: "npm:^3.7.0"
swr: "npm:2.2.0"
peerDependencies:
react: ">=16"
checksum: b204aeded6ef0d0ec843c3785fb857ec59cbe8609b7519ab73a3f43470752ee0cb910b1c205b6c3064105a3f8ebd5390d32c6cfc506476d219d39065cf3c25ba
languageName: node
linkType: hard
"@clerk/types@npm:3.54.0":
version: 3.54.0
resolution: "@clerk/types@npm:3.54.0"
dependencies:
csstype: "npm:3.1.1"
checksum: fb68a7cf471431a061e5adae0bbb4382952ccaa0a490649620a35cf0d46b4fbdb4909a766c92c2236c0c1abfeccbd9c12977cb0317a1f70854251d29444571e6
react: ">=18"
react-dom: ">=18"
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
checksum: 3423ac83d8b2dad30135ada38dc0b2d065f087c2315f80e94dc8fe8ba0b525a9c2aab8a4b57ad1c5cbef917759377d5175dc4a4faabf0917b6f28a8e19efeca5
languageName: node
linkType: hard
@ -1818,7 +1811,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hono/clerk-auth@workspace:packages/clerk-auth"
dependencies:
"@clerk/backend": "npm:^0.30.1"
"@clerk/backend": "npm:^1.0.0"
"@types/react": "npm:^18"
hono: "npm:^3.11.7"
jest: "npm:^29.7.0"
@ -1826,7 +1819,7 @@ __metadata:
react: "npm:^18.2.0"
tsup: "npm:^8.0.1"
peerDependencies:
"@clerk/backend": ">=0.30.0 <1"
"@clerk/backend": ^1.0.0
hono: ">=3.*"
languageName: unknown
linkType: soft
@ -3299,39 +3292,6 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-schema@npm:^2.3.0, @peculiar/asn1-schema@npm:^2.3.6":
version: 2.3.8
resolution: "@peculiar/asn1-schema@npm:2.3.8"
dependencies:
asn1js: "npm:^3.0.5"
pvtsutils: "npm:^1.3.5"
tslib: "npm:^2.6.2"
checksum: 65f16b2a7eb91365b6dac47730ffcad4617ef04b821e0a4286c379ac7283588b0a6744032ee686e0914a0886c2a055108ed945b9c4d22821a3b123640b61f3b2
languageName: node
linkType: hard
"@peculiar/json-schema@npm:^1.1.12":
version: 1.1.12
resolution: "@peculiar/json-schema@npm:1.1.12"
dependencies:
tslib: "npm:^2.0.0"
checksum: 202132c66dcc6b6aca5d0af971c015be2e163da2f7f992910783c5d39c8a7db59b6ec4f4ce419459a1f954b7e1d17b6b253f0e60072c1b3d254079f4eaebc311
languageName: node
linkType: hard
"@peculiar/webcrypto@npm:1.4.1":
version: 1.4.1
resolution: "@peculiar/webcrypto@npm:1.4.1"
dependencies:
"@peculiar/asn1-schema": "npm:^2.3.0"
"@peculiar/json-schema": "npm:^1.1.12"
pvtsutils: "npm:^1.3.2"
tslib: "npm:^2.4.1"
webcrypto-core: "npm:^1.7.4"
checksum: 5acf1b025664525452e2b0748573b0f4100c6840d71ff5577188dfb81b97d463911deff17b4b0c3e59f35fe93c54fec4591f1c42f0a54dae1d5710a03c5e55d3
languageName: node
linkType: hard
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
@ -4074,13 +4034,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:16.18.6":
version: 16.18.6
resolution: "@types/node@npm:16.18.6"
checksum: 88192f5cd3d21ca827898c903ce6fbb8a92a51d0f9d8f7e93ac3f2f3b46cdd9f29c969fe3af9ba004833bb265c6330042f37d11cd97b9e4f54dabf2b34399075
languageName: node
linkType: hard
"@types/node@npm:^12.7.1":
version: 12.20.55
resolution: "@types/node@npm:12.20.55"
@ -5241,17 +5194,6 @@ __metadata:
languageName: node
linkType: hard
"asn1js@npm:^3.0.1, asn1js@npm:^3.0.5":
version: 3.0.5
resolution: "asn1js@npm:3.0.5"
dependencies:
pvtsutils: "npm:^1.3.2"
pvutils: "npm:^1.1.3"
tslib: "npm:^2.4.0"
checksum: bb8eaf4040c8f49dd475566874986f5976b81bae65a6b5526e2208a13cdca323e69ce297bcd435fdda3eb6933defe888e71974d705b6fcb14f2734a907f8aed4
languageName: node
linkType: hard
"assertion-error@npm:^1.1.0":
version: 1.1.0
resolution: "assertion-error@npm:1.1.0"
@ -6694,13 +6636,6 @@ __metadata:
languageName: node
linkType: hard
"csstype@npm:3.1.1":
version: 3.1.1
resolution: "csstype@npm:3.1.1"
checksum: 7c8b8c5923049d84132581c13bae6e1faf999746fe3998ba5f3819a8e1cdc7512ace87b7d0a4a69f0f4b8ba11daf835d4f1390af23e09fc4f0baad52c084753a
languageName: node
linkType: hard
"csstype@npm:^3.0.2, csstype@npm:^3.1.2":
version: 3.1.3
resolution: "csstype@npm:3.1.3"
@ -6911,13 +6846,6 @@ __metadata:
languageName: node
linkType: hard
"deepmerge@npm:4.2.2":
version: 4.2.2
resolution: "deepmerge@npm:4.2.2"
checksum: d6136eee869057fea7a829aa2d10073ed49db5216e42a77cc737dd385334aab9b68dae22020a00c24c073d5f79cbbdd3f11b8d4fc87700d112ddaa0e1f968ef2
languageName: node
linkType: hard
"deepmerge@npm:^4.2.2":
version: 4.3.1
resolution: "deepmerge@npm:4.3.1"
@ -13777,13 +13705,6 @@ __metadata:
languageName: node
linkType: hard
"node-fetch-native@npm:1.0.1":
version: 1.0.1
resolution: "node-fetch-native@npm:1.0.1"
checksum: 27841116388ea5309037400de7fa1003712e974dc57a048f78e5fc659fa80095403f34051c069096a9bd705c7445876d88624121365847f617520325693d67c8
languageName: node
linkType: hard
"node-fetch-native@npm:^1.4.0":
version: 1.4.1
resolution: "node-fetch-native@npm:1.4.1"
@ -15233,22 +15154,6 @@ __metadata:
languageName: node
linkType: hard
"pvtsutils@npm:^1.3.2, pvtsutils@npm:^1.3.5":
version: 1.3.5
resolution: "pvtsutils@npm:1.3.5"
dependencies:
tslib: "npm:^2.6.1"
checksum: d425aed316907e0b447a459bfb97c55d22270c3cfdba5a07ec90da0737b0e40f4f1771a444636f85bb6a453de90ff8c6b5f4f6ddba7597977166af49974b4534
languageName: node
linkType: hard
"pvutils@npm:^1.1.3":
version: 1.1.3
resolution: "pvutils@npm:1.1.3"
checksum: 23489e6b3c76b6afb6964a20f891d6bef092939f401c78bba186b2bfcdc7a13904a0af0a78f7933346510f8c1228d5ab02d3c80e968fd84d3c76ff98d8ec9aac
languageName: node
linkType: hard
"qs@npm:6.11.0":
version: 6.11.0
resolution: "qs@npm:6.11.0"
@ -16561,6 +16466,13 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.7.0":
version: 3.7.0
resolution: "std-env@npm:3.7.0"
checksum: 60edf2d130a4feb7002974af3d5a5f3343558d1ccf8d9b9934d225c638606884db4a20d2fe6440a09605bca282af6b042ae8070a10490c0800d69e82e478f41e
languageName: node
linkType: hard
"stoppable@npm:^1.1.0":
version: 1.1.0
resolution: "stoppable@npm:1.1.0"
@ -17418,7 +17330,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.1, tslib@npm:^2.6.2":
"tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.4.0":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb
@ -18729,19 +18641,6 @@ __metadata:
languageName: node
linkType: hard
"webcrypto-core@npm:^1.7.4":
version: 1.7.7
resolution: "webcrypto-core@npm:1.7.7"
dependencies:
"@peculiar/asn1-schema": "npm:^2.3.6"
"@peculiar/json-schema": "npm:^1.1.12"
asn1js: "npm:^3.0.1"
pvtsutils: "npm:^1.3.2"
tslib: "npm:^2.4.0"
checksum: 57f0bee4e6c39f04fe5fc5fa615f245b3a9d41b330855cd1c525b96e9124d94e6cd06a174cbe1ff63dcb3b296995ae516e3ff02bad94baddd2a4e1060a854282
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"