diff --git a/dist/plugins.ts b/dist/plugins.ts index 52e2731..7dd252a 100644 --- a/dist/plugins.ts +++ b/dist/plugins.ts @@ -5,3 +5,4 @@ export { StripeSubscriptionPlugin } from '../plugins/stripe' export { ChangeDataCapturePlugin } from '../plugins/cdc' export { QueryLogPlugin } from '../plugins/query-log' export { ResendPlugin } from '../plugins/resend' +export { ClerkPlugin } from '../plugins/clerk' diff --git a/package.json b/package.json index d4e455a..417babc 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@libsql/client": "^0.14.0", "@outerbase/sdk": "2.0.0-rc.3", "clsx": "^2.1.1", + "cookie": "^1.0.2", "cron-parser": "^4.9.0", "hono": "^4.6.14", "jose": "^5.9.6", @@ -58,6 +59,7 @@ "mysql2": "^3.11.4", "node-sql-parser": "^4.18.0", "pg": "^8.13.1", + "svix": "^1.59.2", "tailwind-merge": "^2.6.0", "vite": "^5.4.11" }, diff --git a/plugins/clerk/README.md b/plugins/clerk/README.md new file mode 100644 index 0000000..0337a56 --- /dev/null +++ b/plugins/clerk/README.md @@ -0,0 +1,87 @@ +# Clerk Plugin + +The Clerk Plugin for Starbase provides a quick and simple way for applications to add Clerk user information to their database. + +For more information on how to setup webhooks for your Clerk instance, please refer to their excellent [guide](https://clerk.com/docs/webhooks/sync-data). + +## Usage + +Add the ClerkPlugin plugin to your Starbase configuration: + +```typescript +import { ClerkPlugin } from './plugins/clerk' +const clerkPlugin = new ClerkPlugin({ + dataSource, + clerkInstanceId: 'ins_**********', + clerkSigningSecret: 'whsec_**********', + clerkSessionPublicKey: '-----BEGIN PUBLIC KEY***' +}) +const plugins = [ + clerkPlugin, + // ... other plugins +] satisfies StarbasePlugin[] +``` + +If you want to use the Clerk plugin to verify sessions, change the function `authenticate` in `src/index.ts` to the following: + +```diff +... existing code ... +- if (!payload.sub) { ++ if (!payload.sub || !await clerkPlugin.sessionExistsInDb(payload)) { + throw new Error( + 'Invalid JWT payload, subject not found.' + ) + } + + context = payload +} else { ++ const authenticated = await clerkPlugin.authenticate({ ++ cookie: request.headers.get("Cookie"), ++ token, ++ }) + // If no JWT secret or JWKS endpoint is provided, then the request has no authorization. +- throw new Error('Unauthorized request') ++ if (!authenticated) throw new Error('Unauthorized request') ++ context = authenticated +} +... existing code ... +``` + +## Configuration Options + +| Option | Type | Default | Description | +| ----------------------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `dataSource` | DataSource | `null` | dataSource is needed to create tables and execute queries. | +| `clerkSigningSecret` | string | `null` | Access your signing secret from (https://dashboard.clerk.com/last-active?path=webhooks) | +| `clerkInstanceId` | string | `null` | (optional) Access your instance ID from (https://dashboard.clerk.com/last-active?path=settings) | +| `clerkSessionPublicKey` | string | `null` | (optional) Access your public key from (https://dashboard.clerk.com/last-active?path=api-keys) if you want to verify using a public key | +| `verifySessions` | boolean | `true` | (optional) Verify sessions, this creates a user_session table to store session data | +| `permittedOrigins` | string[] | `[]` | (optional) A list of allowed origins | + +## How To Use + +### Available Methods + +- `authenticate` - Authenticates a request using the Clerk session public key, returns the payload if authenticated, false in any other case. +- `sessionExistsInDb` - Checks if a user session exists in the database, returns true if it does, false in any other case. + +### Webhook Setup + +For our Starbase instance to receive webhook events when user information changes, we need to add our plugin endpoint to Clerk. + +1. Visit the Webhooks page for your Clerk instance: https://dashboard.clerk.com/last-active?path=webhooks +2. Add a new endpoint with the following settings: + - URL: `https:///clerk/webhook` + - Events: + - `User`, + - `Session` if you also want to verify sessions ("session.pending" does not appear to be sent by Clerk, so you can keep it deselected) +3. Save by clicking "Create" and copy the signing secret into the Clerk plugin +4. If you want to verify sessions, you will need to add a public key to your Clerk instance: + - Visit the API Keys page for your Clerk instance: https://dashboard.clerk.com/last-active?path=api-keys + - Click the copy icon next to `JWKS Public Key` +5. Copy the public key into the Clerk plugin +6. Alternatively, you can use a JWKS endpoint instead of a public key. + - Visit the API Keys page for your Clerk instance: https://dashboard.clerk.com/last-active?path=api-keys + - Click the copy icon next to `JWKS URL` + - Paste the URL under `AUTH_JWKS_ENDPOINT` in your `wrangler.toml` + - Tweak the `authenticate` function in `src/index.ts` to check whether the session exists in the database, as shown in the [Usage](#usage) section. diff --git a/plugins/clerk/index.ts b/plugins/clerk/index.ts new file mode 100644 index 0000000..1d9c4f3 --- /dev/null +++ b/plugins/clerk/index.ts @@ -0,0 +1,263 @@ +import { parse } from 'cookie' +import { jwtVerify, importSPKI, JWTPayload } from 'jose' +import { Webhook } from 'svix' +import { StarbaseApp } from '../../src/handler' +import { StarbasePlugin } from '../../src/plugin' +import { DataSource } from '../../src/types' +import { createResponse } from '../../src/utils' +import CREATE_USER_TABLE from './sql/create-user-table.sql' +import CREATE_SESSION_TABLE from './sql/create-session-table.sql' +import UPSERT_USER from './sql/upsert-user.sql' +import GET_USER_INFORMATION from './sql/get-user-information.sql' +import DELETE_USER from './sql/delete-user.sql' +import UPSERT_SESSION from './sql/upsert-session.sql' +import DELETE_SESSION from './sql/delete-session.sql' +import GET_SESSION from './sql/get-session.sql' + +type ClerkEvent = { + instance_id: string +} & ( + | { + type: 'user.created' | 'user.updated' + data: { + id: string + first_name: string + last_name: string + email_addresses: Array<{ + id: string + email_address: string + }> + primary_email_address_id: string + } + } + | { + type: 'user.deleted' + data: { id: string } + } + | { + type: 'session.created' | 'session.ended' | 'session.removed' | 'session.revoked' + data: { + id: string + user_id: string + } + } +) + +const SQL_QUERIES = { + CREATE_USER_TABLE, + CREATE_SESSION_TABLE, + UPSERT_USER, + GET_USER_INFORMATION, // Currently not used, but can be turned into an endpoint + DELETE_USER, + UPSERT_SESSION, + DELETE_SESSION, + GET_SESSION, +} + +export class ClerkPlugin extends StarbasePlugin { + private dataSource?: DataSource + pathPrefix: string = '/clerk' + clerkInstanceId?: string + clerkSigningSecret: string + clerkSessionPublicKey?: string + permittedOrigins: string[] + verifySessions: boolean + constructor(opts?: { + clerkInstanceId?: string + clerkSigningSecret: string + clerkSessionPublicKey?: string + verifySessions?: boolean + permittedOrigins?: string[] + dataSource: DataSource + }) { + super('starbasedb:clerk', { + // The `requiresAuth` is set to false to allow for the webhooks sent by Clerk to be accessible + requiresAuth: false, + }) + + if (!opts?.clerkSigningSecret) { + throw new Error('A signing secret is required for this plugin.') + } + + this.clerkInstanceId = opts.clerkInstanceId + this.clerkSigningSecret = opts.clerkSigningSecret + this.clerkSessionPublicKey = opts.clerkSessionPublicKey + this.verifySessions = opts.verifySessions ?? true + this.permittedOrigins = opts.permittedOrigins ?? [] + this.dataSource = opts.dataSource + } + + override async register(app: StarbaseApp) { + app.use(async (_, next) => { + // Create user table if it doesn't exist + await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.CREATE_USER_TABLE, + params: [], + }) + + if (this.verifySessions) { + // Create session table if it doesn't exist + await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.CREATE_SESSION_TABLE, + params: [], + }) + } + + await next() + }) + + // Webhook to handle Clerk events + app.post(`${this.pathPrefix}/webhook`, async (c) => { + const wh = new Webhook(this.clerkSigningSecret) + const svix_id = c.req.header('svix-id') + const svix_signature = c.req.header('svix-signature') + const svix_timestamp = c.req.header('svix-timestamp') + + if (!svix_id || !svix_signature || !svix_timestamp) { + return createResponse( + undefined, + 'Missing required headers: svix-id, svix-signature, svix-timestamp', + 400 + ) + } + + const body = await c.req.text() + + try { + const event = wh.verify(body, { + 'svix-id': svix_id, + 'svix-timestamp': svix_timestamp, + 'svix-signature': svix_signature, + }) as ClerkEvent + + if (this.clerkInstanceId && 'instance_id' in event && event.instance_id !== this.clerkInstanceId) { + return createResponse( + undefined, + 'Invalid instance ID', + 401 + ) + } + + if (event.type === 'user.deleted') { + const { id } = event.data + + await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.DELETE_USER, + params: [id], + }) + + // todo if user is deleted, delete all sessions for that user + } else if ( + event.type === 'user.updated' || + event.type === 'user.created' + ) { + const { id, first_name, last_name, email_addresses, primary_email_address_id } = event.data + + const email = email_addresses.find( + (email: any) => email.id === primary_email_address_id + )?.email_address + + await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.UPSERT_USER, + params: [id, email, first_name, last_name], + }) + } else if (event.type === 'session.created') { + const { id, user_id } = event.data + + await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.UPSERT_SESSION, + params: [id, user_id], + }) + } else if (event.type === 'session.ended' || event.type === 'session.removed' || event.type === 'session.revoked') { + const { id, user_id } = event.data + + await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.DELETE_SESSION, + params: [id, user_id], + }) + } + + return createResponse({ success: true }, undefined, 200) + } catch (error: any) { + console.error('Webhook processing error:', error) + return createResponse( + undefined, + `Webhook processing failed: ${error.message}`, + 400 + ) + } + }) + } + + /** + * Authenticates a request using the Clerk session public key. + * heavily references https://clerk.com/docs/backend-requests/handling/manual-jwt + * @param cookie The cookie to authenticate. + * @param token The token to authenticate. + * @returns {JWTPayload | false} The decoded payload if authenticated, false if not. + */ + public async authenticate({ cookie, token: tokenCrossOrigin }: { cookie?: string | null, token?: string }) { + if (!this.verifySessions || !this.clerkSessionPublicKey) { + console.error('Public key or session verification is not enabled.') + return false + } + + const COOKIE_NAME = "__session" + const tokenSameOrigin = cookie ? parse(cookie)[COOKIE_NAME] : undefined + if (!tokenSameOrigin && !tokenCrossOrigin) return false + + try { + const publicKey = await importSPKI(this.clerkSessionPublicKey, 'RS256') + const token = tokenSameOrigin || tokenCrossOrigin + const decoded = await jwtVerify<{ sid: string; sub: string }>(token!, publicKey) + + const currentTime = Math.floor(Date.now() / 1000) + if ( + (decoded.payload.exp && decoded.payload.exp < currentTime) + || (decoded.payload.nbf && decoded.payload.nbf > currentTime) + ) { + console.error('Token is expired or not yet valid') + return false + } + + if (this.permittedOrigins.length > 0 && decoded.payload.azp + && !this.permittedOrigins.includes(decoded.payload.azp as string) + ) { + console.error("Invalid 'azp' claim") + return false + } + + const sessionExists = await this.sessionExistsInDb(decoded.payload) + if (!sessionExists) { + console.error("Session not found") + return false + } + + return decoded.payload + } catch (error) { + console.error('Authentication error:', error) + return false + } + } + + /** + * Checks if a user session exists in the database. + * @param sessionId The session ID to check. + * @param userId The user ID to check. + * @param dataSource The data source to use for the check. + * @returns {boolean} True if the session exists, false if not. + */ + public async sessionExistsInDb(payload: { sub: string, sid: string }): Promise { + try { + const result: any = await this.dataSource?.rpc.executeQuery({ + sql: SQL_QUERIES.GET_SESSION, + params: [payload.sid, payload.sub], + }) + + return result?.length > 0 + } catch (error) { + console.error('db error while fetching session:', error) + return false + } + } +} diff --git a/plugins/clerk/meta.json b/plugins/clerk/meta.json new file mode 100644 index 0000000..863cbf3 --- /dev/null +++ b/plugins/clerk/meta.json @@ -0,0 +1,32 @@ +{ + "version": "1.0.0", + "resources": { + "tables": { + "user": [ + "user_id", + "email", + "first_name", + "last_name", + "created_at", + "updated_at", + "deleted_at" + ], + "session": [ + "session_id", + "user_id", + "created_at", + "updated_at", + "deleted_at" + ] + }, + "secrets": {}, + "variables": {} + }, + "dependencies": { + "tables": { + "*": ["user_id"] + }, + "secrets": {}, + "variables": {} + } +} diff --git a/plugins/clerk/sql/create-session-table.sql b/plugins/clerk/sql/create-session-table.sql new file mode 100644 index 0000000..8133496 --- /dev/null +++ b/plugins/clerk/sql/create-session-table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS user_session ( + session_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + status TEXT DEFAULT 'active', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +) \ No newline at end of file diff --git a/plugins/clerk/sql/create-user-table.sql b/plugins/clerk/sql/create-user-table.sql new file mode 100644 index 0000000..a779c23 --- /dev/null +++ b/plugins/clerk/sql/create-user-table.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS user ( + user_id TEXT PRIMARY KEY, + email TEXT NOT NULL, + first_name TEXT, + last_name TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + deleted_at DATETIME DEFAULT NULL +) \ No newline at end of file diff --git a/plugins/clerk/sql/delete-session.sql b/plugins/clerk/sql/delete-session.sql new file mode 100644 index 0000000..689b185 --- /dev/null +++ b/plugins/clerk/sql/delete-session.sql @@ -0,0 +1 @@ +DELETE FROM user_session WHERE session_id = ? AND user_id = ? \ No newline at end of file diff --git a/plugins/clerk/sql/delete-user.sql b/plugins/clerk/sql/delete-user.sql new file mode 100644 index 0000000..8818ded --- /dev/null +++ b/plugins/clerk/sql/delete-user.sql @@ -0,0 +1,3 @@ +UPDATE user +SET deleted_at = CURRENT_TIMESTAMP +WHERE user_id = ? AND deleted_at IS NULL \ No newline at end of file diff --git a/plugins/clerk/sql/get-session.sql b/plugins/clerk/sql/get-session.sql new file mode 100644 index 0000000..e8d1e6a --- /dev/null +++ b/plugins/clerk/sql/get-session.sql @@ -0,0 +1 @@ +SELECT * FROM user_session WHERE session_id = ? AND user_id = ? \ No newline at end of file diff --git a/plugins/clerk/sql/get-user-information.sql b/plugins/clerk/sql/get-user-information.sql new file mode 100644 index 0000000..a681bc3 --- /dev/null +++ b/plugins/clerk/sql/get-user-information.sql @@ -0,0 +1,2 @@ +SELECT email, first_name, last_name FROM user +WHERE user_id = ? AND deleted_at IS NULL \ No newline at end of file diff --git a/plugins/clerk/sql/upsert-session.sql b/plugins/clerk/sql/upsert-session.sql new file mode 100644 index 0000000..7128632 --- /dev/null +++ b/plugins/clerk/sql/upsert-session.sql @@ -0,0 +1,4 @@ +INSERT INTO user_session (session_id, user_id) +VALUES (?, ?) +ON CONFLICT(session_id) DO UPDATE SET +updated_at = CURRENT_TIMESTAMP \ No newline at end of file diff --git a/plugins/clerk/sql/upsert-user.sql b/plugins/clerk/sql/upsert-user.sql new file mode 100644 index 0000000..50c0ee1 --- /dev/null +++ b/plugins/clerk/sql/upsert-user.sql @@ -0,0 +1,8 @@ +INSERT INTO user (user_id, email, first_name, last_name) +VALUES (?, ?, ?, ?) +ON CONFLICT(user_id) DO UPDATE SET +email = excluded.email, +first_name = excluded.first_name, +last_name = excluded.last_name, +updated_at = CURRENT_TIMESTAMP, +deleted_at = NULL \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 146fa1c..62ef9be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,12 @@ importers: '@outerbase/sdk': specifier: 2.0.0-rc.3 version: 2.0.0-rc.3 - '@phosphor-icons/react': - specifier: ^2.1.7 - version: 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) clsx: specifier: ^2.1.1 version: 2.1.1 + cookie: + specifier: ^1.0.2 + version: 1.0.2 cron-parser: specifier: ^4.9.0 version: 4.9.0 @@ -41,6 +41,9 @@ importers: pg: specifier: ^8.13.1 version: 8.13.1 + svix: + specifier: ^1.59.2 + version: 1.59.2 tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -600,13 +603,6 @@ packages: resolution: {integrity: sha512-bmV4hlzs5sz01IDWNHdJC2ZD4ezM4UEwG1fEQi59yByHRtPOVDjK7Z5iQ8e1MbR0814vdhv9hMcUKP8SJDA7vQ==} hasBin: true - '@phosphor-icons/react@2.1.7': - resolution: {integrity: sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==} - engines: {node: '>=10'} - peerDependencies: - react: '>= 16.8' - react-dom: '>= 16.8' - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -706,6 +702,9 @@ packages: cpu: [x64] os: [win32] + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@tailwindcss/node@4.0.6': resolution: {integrity: sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q==} @@ -875,9 +874,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} @@ -956,10 +952,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -971,6 +963,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -1005,10 +1001,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -1048,6 +1040,9 @@ packages: es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -1087,6 +1082,9 @@ packages: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -1099,10 +1097,6 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - form-data@4.0.1: - resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} - engines: {node: '>= 6'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1391,14 +1385,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1485,6 +1471,15 @@ packages: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1654,19 +1649,16 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - react-dom@19.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} - peerDependencies: - react: ^19.0.0 - - react@19.0.0: - resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} - engines: {node: '>=0.10.0'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} readdirp@4.0.2: resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} engines: {node: '>= 14.16.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve@1.22.9: resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} hasBin: true @@ -1696,9 +1688,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -1810,6 +1799,12 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svix-fetch@3.0.0: + resolution: {integrity: sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw==} + + svix@1.59.2: + resolution: {integrity: sha512-LYdryeUpJsPCAiNb5c+lZcPTDf/hEgQ+KegRFed1INR2loD+uE6JKyhW32AEjaQquNdhaCK4ed2P1GrOtySa5Q==} + tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} @@ -1846,6 +1841,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -1882,6 +1880,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + vite-node@2.1.8: resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1947,14 +1948,23 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-url@14.1.1: resolution: {integrity: sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2421,11 +2431,6 @@ snapshots: dependencies: handlebars: 4.7.8 - '@phosphor-icons/react@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': - dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - '@pkgjs/parseargs@0.11.0': optional: true @@ -2486,6 +2491,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.30.1': optional: true + '@stablelib/base64@1.0.1': {} + '@tailwindcss/node@4.0.6': dependencies: enhanced-resolve: 5.18.1 @@ -2655,8 +2662,6 @@ snapshots: assertion-error@2.0.1: {} - asynckit@0.4.0: {} - aws-ssl-profiles@1.1.2: {} balanced-match@1.0.2: {} @@ -2728,16 +2733,14 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - commander@12.1.0: {} convert-source-map@2.0.0: {} cookie@0.7.2: {} + cookie@1.0.2: {} + cron-parser@4.9.0: dependencies: luxon: 3.5.0 @@ -2762,8 +2765,6 @@ snapshots: defu@6.1.4: {} - delayed-stream@1.0.0: {} - denque@2.1.0: {} detect-libc@1.0.3: {} @@ -2789,6 +2790,8 @@ snapshots: es-module-lexer@1.6.0: {} + es6-promise@4.2.8: {} + esbuild@0.17.19: optionalDependencies: '@esbuild/android-arm': 0.17.19 @@ -2868,6 +2871,8 @@ snapshots: expect-type@1.1.0: {} + fast-sha256@1.3.0: {} + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -2882,12 +2887,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.1: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -3160,12 +3159,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mime@3.0.0: {} mimic-fn@4.0.0: {} @@ -3236,6 +3229,10 @@ snapshots: node-domexception@1.0.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -3374,15 +3371,12 @@ snapshots: punycode@2.3.1: {} - react-dom@19.0.0(react@19.0.0): - dependencies: - react: 19.0.0 - scheduler: 0.25.0 - - react@19.0.0: {} + querystringify@2.2.0: {} readdirp@4.0.2: {} + requires-port@1.0.0: {} + resolve@1.22.9: dependencies: is-core-module: 2.16.0 @@ -3437,8 +3431,6 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.25.0: {} - selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.11 @@ -3531,6 +3523,24 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svix-fetch@3.0.0: + dependencies: + node-fetch: 2.7.0 + whatwg-fetch: 3.6.20 + transitivePeerDependencies: + - encoding + + svix@1.59.2: + dependencies: + '@stablelib/base64': 1.0.1 + '@types/node': 22.10.2 + es6-promise: 4.2.8 + fast-sha256: 1.3.0 + svix-fetch: 3.0.0 + url-parse: 1.5.10 + transitivePeerDependencies: + - encoding + tailwind-merge@2.6.0: {} tailwindcss@4.0.6: {} @@ -3557,6 +3567,8 @@ snapshots: dependencies: is-number: 7.0.0 + tr46@0.0.3: {} + tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -3589,6 +3601,11 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + vite-node@2.1.8(@types/node@22.10.2)(lightningcss@1.29.1): dependencies: cac: 6.7.14 @@ -3654,13 +3671,22 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + whatwg-fetch@3.6.20: {} + whatwg-url@14.1.1: dependencies: tr46: 5.0.0 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/index.ts b/src/index.ts index 2713b73..8df0b3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { QueryLogPlugin } from '../plugins/query-log' import { StatsPlugin } from '../plugins/stats' import { CronPlugin } from '../plugins/cron' import { InterfacePlugin } from '../plugins/interface' +import { ClerkPlugin } from '../plugins/clerk' export { StarbaseDBDurableObject } from './do'