diff --git a/.changeset/clerk-hono-initial.md b/.changeset/clerk-hono-initial.md new file mode 100644 index 00000000000..051352e23b8 --- /dev/null +++ b/.changeset/clerk-hono-initial.md @@ -0,0 +1,31 @@ +--- +'@clerk/hono': minor +--- + +Initial release of `@clerk/hono` - the official Clerk SDK for Hono. + +This package provides: +- `clerkMiddleware()` - Middleware to authenticate requests and attach auth data to Hono context +- `getAuth(c)` - Helper to retrieve auth data from Hono context +- `verifyWebhook(c)` - Webhook verification via `@clerk/hono/webhooks` + +**Usage:** + +```typescript +import { Hono } from 'hono'; +import { clerkMiddleware, getAuth } from '@clerk/hono'; + +const app = new Hono(); + +app.use('*', clerkMiddleware()); + +app.get('/protected', (c) => { + const { userId } = getAuth(c); + if (!userId) { + return c.json({ error: 'Unauthorized' }, 401); + } + return c.json({ userId }); +}); +``` + +Based on the community `@hono/clerk-auth` package. Thank you to Vaggelis Yfantis for the original implementation! diff --git a/.changeset/config.json b/.changeset/config.json index ace011cb04b..26976909e27 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,6 +7,7 @@ } ], "commit": false, + "ignore": ["@clerk/hono"], "fixed": [], "linked": [], "access": "public", diff --git a/packages/hono/README.md b/packages/hono/README.md new file mode 100644 index 00000000000..671c863906c --- /dev/null +++ b/packages/hono/README.md @@ -0,0 +1,154 @@ +

+ + + + + + +
+

+ +# @clerk/hono + +
+ +[![Chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) +[![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs?utm_source=github&utm_medium=clerk_hono) +[![Follow on Twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) + +[Changelog](https://github.com/clerk/javascript/blob/main/packages/hono/CHANGELOG.md) +· +[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml) +· +[Request a Feature](https://feedback.clerk.com/roadmap) +· +[Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_hono) + +
+ +## Getting Started + +[Clerk](https://clerk.com/?utm_source=github&utm_medium=clerk_hono) is the easiest way to add authentication and user management to your Hono application. Add sign up, sign in, and profile management to your application in minutes. + +### Prerequisites + +- Hono 4+ +- Node.js 20+ + +### Installation + +```sh +npm install @clerk/hono +``` + +### Configuration + +Set your Clerk API keys as environment variables: + +```sh +CLERK_SECRET_KEY=sk_**** +CLERK_PUBLISHABLE_KEY=pk_**** +``` + +### Usage + +```typescript +import { Hono } from 'hono'; +import { clerkMiddleware, getAuth } from '@clerk/hono'; + +const app = new Hono(); + +// Apply Clerk middleware to all routes +app.use('*', clerkMiddleware()); + +// Public route +app.get('/', c => { + return c.json({ message: 'Hello!' }); +}); + +// Protected route +app.get('/protected', c => { + const { userId } = getAuth(c); + + if (!userId) { + return c.json({ error: 'Unauthorized' }, 401); + } + + return c.json({ message: 'Hello authenticated user!', userId }); +}); + +export default app; +``` + +### Accessing the Clerk Client + +You can access the Clerk Backend API client directly from the context: + +```typescript +app.get('/user/:id', async c => { + const clerkClient = c.get('clerk'); + const user = await clerkClient.users.getUser(c.req.param('id')); + return c.json({ user }); +}); +``` + +### Using `acceptsToken` for Machine Auth + +```typescript +app.get('/api', c => { + const auth = getAuth(c, { acceptsToken: 'api_key' }); + + if (!auth.userId) { + return c.json({ error: 'Unauthorized' }, 401); + } + + return c.json({ message: 'API access granted' }); +}); +``` + +### Webhook Verification + +```typescript +import { Hono } from 'hono'; +import { verifyWebhook } from '@clerk/hono/webhooks'; + +const app = new Hono(); + +app.post('/webhooks/clerk', async c => { + const evt = await verifyWebhook(c); + + switch (evt.type) { + case 'user.created': + console.log('User created:', evt.data.id); + break; + // Handle other event types... + } + + return c.json({ received: true }); +}); +``` + +## Support + +You can get in touch with us in any of the following ways: + +- Join our official community [Discord server](https://clerk.com/discord) +- On [our support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_hono) + +## Contributing + +We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md). + +## Security + +`@clerk/hono` follows good practices of security, but 100% security cannot be assured. + +`@clerk/hono` is provided **"as is"** without any **warranty**. Use at your own risk. + +_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._ + +## License + +This project is licensed under the **MIT license**. + +See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/hono/LICENSE) for more information. diff --git a/packages/hono/package.json b/packages/hono/package.json new file mode 100644 index 00000000000..34f3b7c8db6 --- /dev/null +++ b/packages/hono/package.json @@ -0,0 +1,91 @@ +{ + "name": "@clerk/hono", + "version": "0.0.1", + "description": "Clerk SDK for Hono", + "keywords": [ + "auth", + "authentication", + "passwordless", + "session", + "jwt", + "hono", + "clerk" + ], + "homepage": "https://clerk.com/", + "bugs": { + "url": "https://github.com/clerk/javascript/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/clerk/javascript.git", + "directory": "packages/hono" + }, + "license": "MIT", + "author": "Clerk", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./webhooks": { + "import": { + "types": "./dist/webhooks.d.mts", + "default": "./dist/webhooks.mjs" + }, + "require": { + "types": "./dist/webhooks.d.ts", + "default": "./dist/webhooks.js" + } + }, + "./types": { + "import": { + "types": "./dist/types.d.mts" + }, + "require": { + "types": "./dist/types.d.ts" + } + } + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup --env.NODE_ENV production", + "clean": "rimraf ./dist", + "dev": "tsup --watch", + "format": "node ../../scripts/format-package.mjs", + "format:check": "node ../../scripts/format-package.mjs --check", + "lint": "eslint src", + "lint:attw": "attw --pack . --profile node16", + "lint:publint": "publint", + "publish:local": "pnpm yalc push --replace --sig", + "test": "vitest run", + "test:watch": "vitest watch" + }, + "dependencies": { + "@clerk/backend": "workspace:^", + "@clerk/shared": "workspace:^" + }, + "devDependencies": { + "hono": "^4.7.4" + }, + "peerDependencies": { + "hono": ">=4" + }, + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/hono/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/hono/src/__tests__/__snapshots__/exports.test.ts.snap new file mode 100644 index 00000000000..ed959cec5cf --- /dev/null +++ b/packages/hono/src/__tests__/__snapshots__/exports.test.ts.snap @@ -0,0 +1,8 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`@clerk/hono public exports > should not include a breaking change 1`] = ` +[ + "clerkMiddleware", + "getAuth", +] +`; diff --git a/packages/hono/src/__tests__/clerkMiddleware.test.ts b/packages/hono/src/__tests__/clerkMiddleware.test.ts new file mode 100644 index 00000000000..154196c7b14 --- /dev/null +++ b/packages/hono/src/__tests__/clerkMiddleware.test.ts @@ -0,0 +1,258 @@ +import { Hono } from 'hono'; + +import { clerkMiddleware, getAuth } from '../index'; + +const EnvVariables = { + CLERK_SECRET_KEY: 'TEST_API_KEY', + CLERK_PUBLISHABLE_KEY: 'TEST_API_KEY', +}; + +const createMockSessionAuth = () => ({ + tokenType: 'session_token' as const, + userId: 'user_123', + sessionId: 'sess_456', + orgId: null, + orgRole: null, + orgSlug: null, +}); + +const authenticateRequestMock = vi.fn(); + +vi.mock(import('@clerk/backend'), async importOriginal => { + const original = await importOriginal(); + + return { + ...original, + createClerkClient(options: Parameters[0]) { + const client = original.createClerkClient(options); + vi.spyOn(client, 'authenticateRequest').mockImplementation(authenticateRequestMock); + return client; + }, + }; +}); + +describe('clerkMiddleware()', () => { + beforeEach(() => { + vi.stubEnv('CLERK_SECRET_KEY', EnvVariables.CLERK_SECRET_KEY); + vi.stubEnv('CLERK_PUBLISHABLE_KEY', EnvVariables.CLERK_PUBLISHABLE_KEY); + authenticateRequestMock.mockReset(); + }); + + test('handles signin with Authorization Bearer', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: createMockSessionAuth, + }); + const app = new Hono(); + app.use('*', clerkMiddleware()); + + app.get('/', c => { + const auth = getAuth(c); + return c.json({ auth }); + }); + + const req = new Request('http://localhost/', { + headers: { + Authorization: 'Bearer deadbeef', + Origin: 'http://origin.com', + Host: 'host.com', + 'X-Forwarded-Port': '1234', + 'X-Forwarded-Host': 'forwarded-host.com', + Referer: 'referer.com', + 'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + }, + }); + + const response = await app.request(req); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual({ auth: createMockSessionAuth() }); + expect(authenticateRequestMock).toHaveBeenCalledWith( + expect.any(Request), + expect.objectContaining({ + secretKey: EnvVariables.CLERK_SECRET_KEY, + }), + ); + }); + + test('handles signin with cookie', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: createMockSessionAuth, + }); + const app = new Hono(); + app.use('*', clerkMiddleware()); + + app.get('/', c => { + const auth = getAuth(c); + return c.json({ auth }); + }); + + const req = new Request('http://localhost/', { + headers: { + cookie: '_gcl_au=value1; ko_id=value2; __session=deadbeef; __client_uat=1675692233', + Origin: 'http://origin.com', + Host: 'host.com', + }, + }); + + const response = await app.request(req); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual({ auth: createMockSessionAuth() }); + }); + + test('handles handshake by redirecting 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: createMockSessionAuth, + }); + const app = new Hono(); + app.use('*', clerkMiddleware()); + + app.get('/', c => { + const auth = getAuth(c); + return c.json({ auth }); + }); + + const req = new Request('http://localhost/', { + headers: { + cookie: '__session=deadbeef; __client_uat=1675692233', + }, + }); + + const response = await app.request(req); + + 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', + }); + }); + + test('throws error when secret key is missing', async () => { + vi.stubEnv('CLERK_SECRET_KEY', ''); + const app = new Hono(); + app.use('*', clerkMiddleware()); + app.get('/', c => c.json({ ok: true })); + + const response = await app.request(new Request('http://localhost/')); + + expect(response.status).toEqual(500); + }); + + test('throws error when publishable key is missing', async () => { + vi.stubEnv('CLERK_PUBLISHABLE_KEY', ''); + const app = new Hono(); + app.use('*', clerkMiddleware()); + app.get('/', c => c.json({ ok: true })); + + const response = await app.request(new Request('http://localhost/')); + + expect(response.status).toEqual(500); + }); +}); + +describe('getAuth()', () => { + beforeEach(() => { + vi.stubEnv('CLERK_SECRET_KEY', EnvVariables.CLERK_SECRET_KEY); + vi.stubEnv('CLERK_PUBLISHABLE_KEY', EnvVariables.CLERK_PUBLISHABLE_KEY); + authenticateRequestMock.mockReset(); + }); + + test('throws error when called without middleware', async () => { + const app = new Hono(); + app.get('/', c => { + const auth = getAuth(c); + return c.json({ auth }); + }); + + const response = await app.request(new Request('http://localhost/')); + + expect(response.status).toEqual(500); + }); + + test('handles acceptsToken option for API keys', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: () => ({ + tokenType: 'api_key', + id: 'ak_1234', + userId: 'user_456', + orgId: null, + }), + }); + const app = new Hono(); + app.use('*', clerkMiddleware()); + + app.get('/', c => { + const auth = getAuth(c, { acceptsToken: 'api_key' }); + return c.json({ auth }); + }); + + const req = new Request('http://localhost/', { + headers: { + Authorization: 'Bearer ak_deadbeef', + }, + }); + + const response = await app.request(req); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual({ + auth: { + tokenType: 'api_key', + id: 'ak_1234', + userId: 'user_456', + orgId: null, + }, + }); + }); + + test('handles acceptsToken option as array', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: () => ({ + tokenType: 'api_key', + id: 'ak_5678', + userId: 'user_789', + orgId: null, + }), + }); + const app = new Hono(); + app.use('*', clerkMiddleware()); + + app.get('/', c => { + const auth = getAuth(c, { acceptsToken: ['api_key', 'session_token'] }); + return c.json({ auth }); + }); + + const req = new Request('http://localhost/', { + headers: { + Authorization: 'Bearer ak_deadbeef', + }, + }); + + const response = await app.request(req); + + expect(response.status).toEqual(200); + expect(await response.json()).toEqual({ + auth: { + tokenType: 'api_key', + id: 'ak_5678', + userId: 'user_789', + orgId: null, + }, + }); + }); +}); diff --git a/packages/hono/src/__tests__/exports.test.ts b/packages/hono/src/__tests__/exports.test.ts new file mode 100644 index 00000000000..1d2a129285b --- /dev/null +++ b/packages/hono/src/__tests__/exports.test.ts @@ -0,0 +1,7 @@ +import * as publicExports from '../index'; + +describe('@clerk/hono public exports', () => { + it('should not include a breaking change', () => { + expect(Object.keys(publicExports).sort()).toMatchSnapshot(); + }); +}); diff --git a/packages/hono/src/clerkMiddleware.ts b/packages/hono/src/clerkMiddleware.ts new file mode 100644 index 00000000000..81451046ba5 --- /dev/null +++ b/packages/hono/src/clerkMiddleware.ts @@ -0,0 +1,98 @@ +import type { AuthObject } from '@clerk/backend'; +import { createClerkClient } from '@clerk/backend'; +import type { AuthenticateRequestOptions, AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal'; +import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'; +import type { MiddlewareHandler } from 'hono'; +import { env } from 'hono/adapter'; + +type ClerkEnv = { + CLERK_SECRET_KEY: string; + CLERK_PUBLISHABLE_KEY: string; + CLERK_API_URL?: string; + CLERK_API_VERSION?: string; +}; + +export type ClerkMiddlewareOptions = Omit; + +/** + * Clerk middleware for Hono that authenticates requests and attaches + * auth data to the Hono context. + * + * @example + * ```ts + * import { Hono } from 'hono'; + * import { clerkMiddleware, getAuth } from '@clerk/hono'; + * + * const app = new Hono(); + * app.use('*', clerkMiddleware()); + * + * app.get('/', (c) => { + * const { userId } = getAuth(c); + * return c.json({ userId }); + * }); + * ``` + */ +export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHandler => { + return async (c, next) => { + const clerkEnv = env(c); + const { secretKey, publishableKey, apiUrl, apiVersion, ...rest } = options || { + secretKey: clerkEnv.CLERK_SECRET_KEY || '', + publishableKey: clerkEnv.CLERK_PUBLISHABLE_KEY || '', + apiUrl: clerkEnv.CLERK_API_URL, + apiVersion: clerkEnv.CLERK_API_VERSION, + }; + + if (!secretKey) { + throw new Error( + 'Clerk: Missing Secret Key. Set CLERK_SECRET_KEY in your environment or pass secretKey to clerkMiddleware().', + ); + } + + if (!publishableKey) { + throw new Error( + 'Clerk: Missing Publishable Key. Set CLERK_PUBLISHABLE_KEY in your environment or pass publishableKey to clerkMiddleware().', + ); + } + + const clerkClient = createClerkClient({ + ...rest, + apiUrl, + apiVersion, + secretKey, + publishableKey, + userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, + }); + + const requestState = await clerkClient.authenticateRequest(c.req.raw, { + ...rest, + secretKey, + publishableKey, + acceptsToken: 'any', + }); + + if (requestState.headers) { + requestState.headers.forEach((value, key) => { + c.res.headers.append(key, value); + }); + + 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'); + } + } + + const authObjectFn = ((authOptions?: AuthOptions) => + getAuthObjectForAcceptedToken({ + authObject: requestState.toAuth(authOptions) as AuthObject, + acceptsToken: 'any', + })) as GetAuthFnNoRequest; + + c.set('clerkAuth', authObjectFn); + c.set('clerk', clerkClient); + + await next(); + }; +}; diff --git a/packages/hono/src/getAuth.ts b/packages/hono/src/getAuth.ts new file mode 100644 index 00000000000..41d8f7c423a --- /dev/null +++ b/packages/hono/src/getAuth.ts @@ -0,0 +1,37 @@ +import type { AuthOptions, GetAuthFn } from '@clerk/backend/internal'; +import type { Context } from 'hono'; + +/** + * Retrieves the Clerk auth object from the Hono context. + * Must be used after clerkMiddleware() has been applied. + * + * @example + * ```ts + * app.get('/protected', (c) => { + * const { userId } = getAuth(c); + * if (!userId) { + * return c.json({ error: 'Unauthorized' }, 401); + * } + * return c.json({ message: 'Hello!' }); + * }); + * ``` + * + * @example Using acceptsToken for API keys + * ```ts + * app.get('/api', (c) => { + * const auth = getAuth(c, { acceptsToken: 'api_key' }); + * // auth will be typed for API key tokens + * }); + * ``` + */ +export const getAuth: GetAuthFn = ((c: Context, options?: AuthOptions) => { + const authFn = c.get('clerkAuth'); + + if (!authFn) { + throw new Error( + 'Clerk: getAuth() called without clerkMiddleware() being applied. Make sure to use clerkMiddleware() before calling getAuth().', + ); + } + + return authFn(options); +}) as GetAuthFn; diff --git a/packages/hono/src/global.d.ts b/packages/hono/src/global.d.ts new file mode 100644 index 00000000000..1ae75219e34 --- /dev/null +++ b/packages/hono/src/global.d.ts @@ -0,0 +1,6 @@ +declare global { + const PACKAGE_NAME: string; + const PACKAGE_VERSION: string; +} + +export {}; diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts new file mode 100644 index 00000000000..948cce534bb --- /dev/null +++ b/packages/hono/src/index.ts @@ -0,0 +1,14 @@ +export { clerkMiddleware } from './clerkMiddleware'; +export type { ClerkMiddlewareOptions } from './clerkMiddleware'; + +export { getAuth } from './getAuth'; + +import type { ClerkHonoVariables } from './types'; +export type { ClerkHonoVariables }; + +// Augment Hono's ContextVariableMap so users get type inference +// for c.get('clerk') and c.get('clerkAuth') +declare module 'hono' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface ContextVariableMap extends ClerkHonoVariables {} +} diff --git a/packages/hono/src/types.ts b/packages/hono/src/types.ts new file mode 100644 index 00000000000..e7a97727ad8 --- /dev/null +++ b/packages/hono/src/types.ts @@ -0,0 +1,11 @@ +import type { ClerkClient } from '@clerk/backend'; +import type { GetAuthFnNoRequest } from '@clerk/backend/internal'; + +/** + * Variables that clerkMiddleware sets on the Hono context. + * Access via c.get('clerk') and c.get('clerkAuth'). + */ +export type ClerkHonoVariables = { + clerk: ClerkClient; + clerkAuth: GetAuthFnNoRequest; +}; diff --git a/packages/hono/src/types/index.ts b/packages/hono/src/types/index.ts new file mode 100644 index 00000000000..4a224c4726b --- /dev/null +++ b/packages/hono/src/types/index.ts @@ -0,0 +1,9 @@ +/** + * Re-export all shared Clerk types for convenient access via @clerk/hono/types + */ +export type * from '@clerk/shared/types'; + +/** + * Hono-specific types + */ +export type { ClerkHonoVariables } from '../types'; diff --git a/packages/hono/src/webhooks.ts b/packages/hono/src/webhooks.ts new file mode 100644 index 00000000000..408c47087cb --- /dev/null +++ b/packages/hono/src/webhooks.ts @@ -0,0 +1,42 @@ +/* eslint-disable import/export */ +import type { VerifyWebhookOptions } from '@clerk/backend/webhooks'; +import { verifyWebhook as verifyWebhookBase } from '@clerk/backend/webhooks'; +import type { Context } from 'hono'; + +// Re-export everything from backend webhooks +export * from '@clerk/backend/webhooks'; + +/** + * Verifies the authenticity of a webhook request from Clerk using Svix. + * + * @param c - The Hono Context object from the webhook handler + * @param options - Optional configuration object + * @param options.signingSecret - Custom signing secret. If not provided, falls back to CLERK_WEBHOOK_SIGNING_SECRET env variable + * @throws Will throw an error if the webhook signature verification fails + * @returns A promise that resolves to the verified webhook event data + * + * @example + * ```ts + * import { Hono } from 'hono'; + * import { verifyWebhook } from '@clerk/hono/webhooks'; + * + * const app = new Hono(); + * + * app.post('/webhooks/clerk', async (c) => { + * const evt = await verifyWebhook(c); + * // Handle the webhook event + * return c.json({ received: true }); + * }); + * ``` + * + * @see {@link https://clerk.com/docs/webhooks/sync-data} to learn more about syncing Clerk data to your application using webhooks + */ +export async function verifyWebhook(c: Context, options?: VerifyWebhookOptions) { + // Hono's c.req.raw is already a standard Web Request + // We need to clone it with the body for verification + const body = await c.req.text(); + const clonedRequest = new Request(c.req.raw, { + body, + }); + return verifyWebhookBase(clonedRequest, options); +} diff --git a/packages/hono/tsconfig.json b/packages/hono/tsconfig.json new file mode 100644 index 00000000000..ffa09e4e241 --- /dev/null +++ b/packages/hono/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "moduleResolution": "NodeNext", + "module": "NodeNext", + "sourceMap": false, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "target": "ES2020", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/hono/tsup.config.ts b/packages/hono/tsup.config.ts new file mode 100644 index 00000000000..70495a48ec5 --- /dev/null +++ b/packages/hono/tsup.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'tsup'; + +import { name, version } from './package.json'; + +export default defineConfig(overrideOptions => { + const isWatch = !!overrideOptions.watch; + + return { + entry: { + index: './src/index.ts', + webhooks: './src/webhooks.ts', + types: './src/types/index.ts', + }, + format: ['cjs', 'esm'], + bundle: true, + clean: true, + minify: false, + sourcemap: true, + dts: true, + define: { + PACKAGE_NAME: `"${name}"`, + PACKAGE_VERSION: `"${version}"`, + __DEV__: `${isWatch}`, + }, + }; +}); diff --git a/packages/hono/vitest.config.mts b/packages/hono/vitest.config.mts new file mode 100644 index 00000000000..e7decfd80a1 --- /dev/null +++ b/packages/hono/vitest.config.mts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [], + test: { + globals: true, + coverage: { + provider: 'v8', + enabled: true, + reporter: ['text', 'json', 'html'], + }, + env: { + CLERK_SECRET_KEY: 'TEST_SECRET_KEY', + CLERK_PUBLISHABLE_KEY: 'TEST_PUBLISHABLE_KEY', + }, + setupFiles: './vitest.setup.mts', + }, +}); diff --git a/packages/hono/vitest.setup.mts b/packages/hono/vitest.setup.mts new file mode 100644 index 00000000000..e8e72798208 --- /dev/null +++ b/packages/hono/vitest.setup.mts @@ -0,0 +1,6 @@ +import { beforeAll } from 'vitest'; + +globalThis.PACKAGE_NAME = '@clerk/hono'; +globalThis.PACKAGE_VERSION = '0.0.0-test'; + +beforeAll(() => {}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb035f97a6..72c2b119821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -693,6 +693,19 @@ importers: specifier: ^5.7.2 version: 5.7.2 + packages/hono: + dependencies: + '@clerk/backend': + specifier: workspace:^ + version: link:../backend + '@clerk/shared': + specifier: workspace:^ + version: link:../shared + devDependencies: + hono: + specifier: ^4.7.4 + version: 4.11.7 + packages/localizations: dependencies: '@clerk/shared': diff --git a/scripts/canary-core3.mjs b/scripts/canary-core3.mjs index 0760d213ac3..66f067294cc 100755 --- a/scripts/canary-core3.mjs +++ b/scripts/canary-core3.mjs @@ -2,9 +2,10 @@ import { $, echo } from 'zx'; -import { constants, getPackageNames } from './common.mjs'; +import { constants, getChangesetIgnoredPackages, getPackageNames } from './common.mjs'; -const packageNames = await getPackageNames(); +const ignoredPackages = await getChangesetIgnoredPackages(); +const packageNames = (await getPackageNames()).filter(name => !ignoredPackages.has(name)); const packageEntries = packageNames.map(name => `'${name}': patch`).join('\n'); const snapshot = `--- diff --git a/scripts/canary.mjs b/scripts/canary.mjs index 8e5e9c7beff..8aae064c236 100755 --- a/scripts/canary.mjs +++ b/scripts/canary.mjs @@ -2,9 +2,10 @@ import { $, echo } from 'zx'; -import { constants, getPackageNames, pinWorkspaceDeps } from './common.mjs'; +import { constants, getChangesetIgnoredPackages, getPackageNames, pinWorkspaceDeps } from './common.mjs'; -const packageNames = await getPackageNames(); +const ignoredPackages = await getChangesetIgnoredPackages(); +const packageNames = (await getPackageNames()).filter(name => !ignoredPackages.has(name)); const packageEntries = packageNames.map(name => `'${name}': patch`).join('\n'); const snapshot = `--- diff --git a/scripts/common.mjs b/scripts/common.mjs index 44b9d2301c3..7fc8c33147b 100644 --- a/scripts/common.mjs +++ b/scripts/common.mjs @@ -5,6 +5,11 @@ export const constants = { ChangesetConfigFile: '.changeset/config.json', }; +export async function getChangesetIgnoredPackages() { + const config = JSON.parse(await readFile(constants.ChangesetConfigFile, 'utf-8')); + return new Set(config.ignore || []); +} + export async function getPackageJsonFiles() { const result = await $`find packages -mindepth 2 -maxdepth 2 -name package.json`.quiet(); return result.stdout.trim().split('\n').filter(Boolean); diff --git a/scripts/snapshot.mjs b/scripts/snapshot.mjs index 43e4ce90f71..a9c7e2b7191 100755 --- a/scripts/snapshot.mjs +++ b/scripts/snapshot.mjs @@ -2,9 +2,10 @@ import { $, argv, echo } from 'zx'; -import { constants, getPackageNames, pinWorkspaceDeps } from './common.mjs'; +import { constants, getChangesetIgnoredPackages, getPackageNames, pinWorkspaceDeps } from './common.mjs'; -const packageNames = await getPackageNames(); +const ignoredPackages = await getChangesetIgnoredPackages(); +const packageNames = (await getPackageNames()).filter(name => !ignoredPackages.has(name)); const packageEntries = packageNames.map(name => `'${name}': patch`).join('\n'); const snapshot = `---