diff --git a/.size-limit.json b/.size-limit.json index a8fe87d..dbc9629 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -10,4 +10,4 @@ "path": "./packages/webhook/_cjs/index.js", "limit": "250 kB" } -] \ No newline at end of file +] diff --git a/.vscode/settings.json b/.vscode/settings.json index e0d5914..320808c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "biomejs.biome" + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome" } diff --git a/biome.json b/biome.json index 766b001..d25dcd4 100644 --- a/biome.json +++ b/biome.json @@ -1,41 +1,44 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "ignore": [ - "node_modules", - "**/node_modules", - "cache", - "coverage", - "tsconfig.json", - "tsconfig.*.json", - "_cjs", - "_esm", - "_types", - "bun.lockb" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - } -} \ No newline at end of file + "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", + "files": { + "ignore": [ + "node_modules", + "**/node_modules", + "cache", + "coverage", + "tsconfig.json", + "tsconfig.*.json", + "_cjs", + "_esm", + "_types" + ] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + }, + "style": { + "noUnusedTemplateLiteral": "warn" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "lineWidth": 80, + "indentWidth": 4, + "indentStyle": "space" + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "trailingCommas": "none" + } + } +} diff --git a/examples/webhook/api/approve.ts b/examples/webhook/api/approve.ts index efa9608..ecbb1c5 100644 --- a/examples/webhook/api/approve.ts +++ b/examples/webhook/api/approve.ts @@ -1,14 +1,17 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import { pimlicoWebhookVerifier } from '@pimlico/webhook' +import { pimlicoWebhookVerifier } from "@pimlico/webhook" +import type { VercelRequest, VercelResponse } from "@vercel/node" -const apiKey = process.env.PIMLICO_API_KEY as string; +const apiKey = process.env.PIMLICO_API_KEY as string -const verifyWebhook = pimlicoWebhookVerifier(apiKey); +const verifyWebhook = pimlicoWebhookVerifier(apiKey) export default async function handler(req: VercelRequest, res: VercelResponse) { - const body = await verifyWebhook(req.headers as Record, Buffer.from(JSON.stringify(req.body))); + const body = await verifyWebhook( + req.headers as Record, + Buffer.from(JSON.stringify(req.body)) + ) - return res.status(200).json({ - sponsor: true - }) + return res.status(200).json({ + sponsor: true + }) } diff --git a/examples/webhook/api/invalid-response.ts b/examples/webhook/api/invalid-response.ts index 8e51c88..dbfbe52 100644 --- a/examples/webhook/api/invalid-response.ts +++ b/examples/webhook/api/invalid-response.ts @@ -1,5 +1,5 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' +import type { VercelRequest, VercelResponse } from "@vercel/node" export default function handler(req: VercelRequest, res: VercelResponse) { - return res.status(200).send('Invalid response test') + return res.status(200).send("Invalid response test") } diff --git a/examples/webhook/api/non-200.ts b/examples/webhook/api/non-200.ts index b59fc11..93b2c6d 100644 --- a/examples/webhook/api/non-200.ts +++ b/examples/webhook/api/non-200.ts @@ -1,7 +1,7 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' +import type { VercelRequest, VercelResponse } from "@vercel/node" export default function handler(req: VercelRequest, res: VercelResponse) { - return res.status(403).json({ - sponsor: true, - }) + return res.status(403).json({ + sponsor: true + }) } diff --git a/examples/webhook/api/reject.ts b/examples/webhook/api/reject.ts index 8bd3ffb..07c0ce4 100644 --- a/examples/webhook/api/reject.ts +++ b/examples/webhook/api/reject.ts @@ -1,7 +1,7 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' +import type { VercelRequest, VercelResponse } from "@vercel/node" export default function handler(req: VercelRequest, res: VercelResponse) { - return res.status(200).json({ - sponsor: false, - }) + return res.status(200).json({ + sponsor: false + }) } diff --git a/examples/webhook/api/too-long.ts b/examples/webhook/api/too-long.ts index 39b6ec5..1eaf97b 100644 --- a/examples/webhook/api/too-long.ts +++ b/examples/webhook/api/too-long.ts @@ -1,9 +1,9 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' +import type { VercelRequest, VercelResponse } from "@vercel/node" export default function handler(req: VercelRequest, res: VercelResponse) { - setTimeout(() => { - return res.status(200).json({ - sponsor: true - }); - }, 10_000); + setTimeout(() => { + return res.status(200).json({ + sponsor: true + }) + }, 10_000) } diff --git a/examples/webhook/package.json b/examples/webhook/package.json index f03e927..63a505c 100644 --- a/examples/webhook/package.json +++ b/examples/webhook/package.json @@ -1,18 +1,18 @@ { - "name": "@pimlico/example", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "private": true, - "dependencies": { - "@pimlico/webhook": "workspace:*", - "@vercel/node": "^3.1.7", - "vercel": "^34.2.7" - } -} \ No newline at end of file + "name": "@pimlico/example", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "private": true, + "dependencies": { + "@pimlico/webhook": "workspace:*", + "@vercel/node": "^3.1.7", + "vercel": "^34.2.7" + } +} diff --git a/package.json b/package.json index 88ff358..f76f2d1 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,25 @@ { - "description": "", - "main": "index.js", - "scripts": { - "lint": "biome check --error-on-warnings .", - "format": "biome format --write .", - "webhook": "pnpm --filter @pimlico/webhook", - "build": "pnpm webhook build", - "clean": "rimraf ./packages/*/node_modules ./packages/*/_cjs ./packages/*/_esm ./packages/*/_types ./packages/*/dist ./packages/*/node_modules ./node_modules" - }, - "private": true, - "keywords": [], - "author": "Pimlico", - "license": "MIT", - "engines": { - "node": ">=20.0.0" - }, - "type": "module", - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/node": "^20.11.18", - "rimraf": "^6.0.1", - "tsc-alias": "^1.8.10" - } + "description": "", + "main": "index.js", + "scripts": { + "lint": "biome check --fix --error-on-warnings .", + "format": "biome format --write .", + "webhook": "pnpm --filter @pimlico/webhook", + "build": "pnpm webhook build", + "clean": "rimraf ./packages/*/node_modules ./packages/*/_cjs ./packages/*/_esm ./packages/*/_types ./packages/*/dist ./packages/*/node_modules ./node_modules" + }, + "private": true, + "keywords": [], + "author": "Pimlico", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "type": "module", + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/node": "^20.11.18", + "rimraf": "^6.0.1", + "tsc-alias": "^1.8.10" + } } \ No newline at end of file diff --git a/packages/webhook/package.json b/packages/webhook/package.json index 794ceb3..fbe7699 100644 --- a/packages/webhook/package.json +++ b/packages/webhook/package.json @@ -1,32 +1,32 @@ { - "name": "@pimlico/webhook", - "version": "0.0.1", - "author": "Pimlico", - "main": "./_cjs/index.js", - "module": "./_esm/index.js", - "types": "./_types/index.d.ts", - "typings": "./_types/index.d.ts", - "type": "module", - "sideEffects": false, - "description": "A utility library for working with Pimlico webhooks.", - "license": "MIT", - "exports": { - ".": { - "types": "./_types/index.d.ts", - "import": "./_esm/index.js", - "default": "./_cjs/index.js" - } - }, - "scripts": { - "build": "pnpm build:cjs && pnpm build:esm", - "build:cjs": "tsc --project ./../../tsconfig/tsconfig.webhook.cjs.json && tsc-alias -p ./../../tsconfig/tsconfig.webhook.cjs.json && printf '{\"type\":\"commonjs\"}' > ./_cjs/package.json", - "build:esm": "tsc --project ./../../tsconfig/tsconfig.webhook.esm.json && tsc-alias -p ./../../tsconfig/tsconfig.webhook.esm.json && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./_esm/package.json" - }, - "devDependencies": { - "typescript": "^5.7.2", - "viem": "^2.21.52" - }, - "peerDependencies": { - "viem": "^2.21.52" - } -} \ No newline at end of file + "name": "@pimlico/webhook", + "version": "0.0.1", + "author": "Pimlico", + "main": "./_cjs/index.js", + "module": "./_esm/index.js", + "types": "./_types/index.d.ts", + "typings": "./_types/index.d.ts", + "type": "module", + "sideEffects": false, + "description": "A utility library for working with Pimlico webhooks.", + "license": "MIT", + "exports": { + ".": { + "types": "./_types/index.d.ts", + "import": "./_esm/index.js", + "default": "./_cjs/index.js" + } + }, + "scripts": { + "build": "pnpm build:cjs && pnpm build:esm", + "build:cjs": "tsc --project ./../../tsconfig/tsconfig.webhook.cjs.json && tsc-alias -p ./../../tsconfig/tsconfig.webhook.cjs.json && printf '{\"type\":\"commonjs\"}' > ./_cjs/package.json", + "build:esm": "tsc --project ./../../tsconfig/tsconfig.webhook.esm.json && tsc-alias -p ./../../tsconfig/tsconfig.webhook.esm.json && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./_esm/package.json" + }, + "devDependencies": { + "typescript": "^5.7.2", + "viem": "^2.21.52" + }, + "peerDependencies": { + "viem": "^2.21.52" + } +} diff --git a/packages/webhook/src/index.ts b/packages/webhook/src/index.ts index 7866c68..a42066f 100644 --- a/packages/webhook/src/index.ts +++ b/packages/webhook/src/index.ts @@ -1,35 +1,40 @@ -import * as crypto from "crypto"; -import type { PimlicoSponsorshipPolicyWebhookBody } from "./types"; -import { keyFetcher } from "./key-fetcher"; +import * as crypto from "node:crypto" +import { keyFetcher } from "./key-fetcher" +import type { PimlicoSponsorshipPolicyWebhookBody } from "./types" -export const pimlicoWebhookVerifier = (apiKey: string) => async (headers: Record, body: Buffer) => { - const fetchKey = keyFetcher(apiKey); +export const pimlicoWebhookVerifier = + (apiKey: string) => + async (headers: Record, body: Buffer) => { + const fetchKey = keyFetcher(apiKey) - if (!Buffer.isBuffer(body)) { - throw new Error("expected body to be a Buffer"); - } + if (!Buffer.isBuffer(body)) { + throw new Error("expected body to be a Buffer") + } - if (process.env.UNSAFE_SKIP_WEBHOOK_VERIFY != null) { - return JSON.parse((body).toString()) as PimlicoSponsorshipPolicyWebhookBody; - } + if (process.env.UNSAFE_SKIP_WEBHOOK_VERIFY != null) { + return JSON.parse( + body.toString() + ) as PimlicoSponsorshipPolicyWebhookBody + } - if (!body || body.length == 0) { - throw new Error("invalid webhook: empty payload"); - } + if (!body || body.length === 0) { + throw new Error("invalid webhook: empty payload") + } - const key = await fetchKey(); + const key = await fetchKey() - const signature = Buffer.from(headers["pimlico-signature"] ?? "", "base64"); + const signature = Buffer.from( + headers["pimlico-signature"] ?? "", + "base64" + ) - const message = Buffer.concat( - [ - body - ], - ); + const message = Buffer.concat([body]) - if (!crypto.verify("sha256", message, key, signature)) { - throw new Error("invalid webhook: signature validation failed"); - } + if (!crypto.verify("sha256", message, key, signature)) { + throw new Error("invalid webhook: signature validation failed") + } - return JSON.parse(body.toString()) as PimlicoSponsorshipPolicyWebhookBody; -}; + return JSON.parse( + body.toString() + ) as PimlicoSponsorshipPolicyWebhookBody + } diff --git a/packages/webhook/src/key-fetcher.ts b/packages/webhook/src/key-fetcher.ts index b68206a..c0d1e21 100644 --- a/packages/webhook/src/key-fetcher.ts +++ b/packages/webhook/src/key-fetcher.ts @@ -1,62 +1,70 @@ -import * as crypto from "crypto"; -import type { KeyFetcher, KeyFetcherOptions } from "./types"; +import * as crypto from "node:crypto" +import type { KeyFetcher, KeyFetcherOptions } from "./types" - -const keysBaseURL = "https://api-staging.pimlico.io/webhook-public-key"; +const keysBaseURL = "https://api-staging.pimlico.io/webhook-public-key" export class KeyCache { - private cache = new Map(); - private expirationInMs: number; + private cache = new Map() + private expirationInMs: number constructor(expirationInMs: number) { - this.expirationInMs = expirationInMs; + this.expirationInMs = expirationInMs } get(k: string): crypto.KeyObject | null | undefined { - return this.cache.get(k); + return this.cache.get(k) } set(k: string, v: crypto.KeyObject): void { - this.cache.set(k, v); + this.cache.set(k, v) setTimeout(() => { - this.cache.delete(k); - }, this.expirationInMs); + this.cache.delete(k) + }, this.expirationInMs) } } -export const keyFetcher = (apiKey: string, options: KeyFetcherOptions = {}): KeyFetcher => { - const fetch = options?.fetch ?? (() => Promise.reject(new Error("no fetch function provided"))); - const keyCache = options?.cache ?? new KeyCache(60 * 60 * 1000); +export const keyFetcher = ( + apiKey: string, + options: KeyFetcherOptions = {} +): KeyFetcher => { + const fetch = + options?.fetch ?? + (() => Promise.reject(new Error("no fetch function provided"))) + const keyCache = options?.cache ?? new KeyCache(60 * 60 * 1000) - const normalBaseURL = (options?.baseURL ?? keysBaseURL).replace(/\/$/, ""); + const normalBaseURL = (options?.baseURL ?? keysBaseURL).replace(/\/$/, "") return async () => { - const cachedKey = keyCache.get('key'); + const cachedKey = keyCache.get("key") if (cachedKey) { - return cachedKey; + return cachedKey } - const url = `${normalBaseURL}?apikey=${apiKey}`; + const url = `${normalBaseURL}?apikey=${apiKey}` - const response = await fetch(url); + const response = await fetch(url) if (!response.ok) { - return Promise.reject(new Error(`error fetching key: ${response.status}`)); + return Promise.reject( + new Error(`error fetching key: ${response.status}`) + ) } if (!response.body) { - return Promise.reject(new Error("error fetching key: empty response")); + return Promise.reject( + new Error("error fetching key: empty response") + ) } - const keyBytes = await response.arrayBuffer(); - const key = crypto.createPublicKey(Buffer.from(keyBytes)); + const keyBytes = await response.arrayBuffer() + const key = crypto.createPublicKey(Buffer.from(keyBytes)) - keyCache.set('key', key); + keyCache.set("key", key) - return key; - }; -}; + return key + } +} -export { keyFetcher as pimlicoKeyFetcher }; \ No newline at end of file +export { keyFetcher as pimlicoKeyFetcher } diff --git a/packages/webhook/src/types.ts b/packages/webhook/src/types.ts index 913d47b..7334acd 100644 --- a/packages/webhook/src/types.ts +++ b/packages/webhook/src/types.ts @@ -1,31 +1,31 @@ -import * as crypto from "crypto"; -import type { Address } from "viem"; -import type { UserOperation } from "viem/account-abstraction"; -type Fetch = typeof fetch; +import type * as crypto from "node:crypto" +import type { Address } from "viem" +import type { UserOperation } from "viem/account-abstraction" +type Fetch = typeof fetch -export type KeyFetcher = () => Promise; +export type KeyFetcher = () => Promise export interface KeyFetcherOptions { - baseURL?: string; - cache?: KeyCache; - fetch?: Fetch; + baseURL?: string + cache?: KeyCache + fetch?: Fetch } export interface KeyCache { - get(k: string): crypto.KeyObject | null | undefined; - set(k: string, v: crypto.KeyObject): void; + get(k: string): crypto.KeyObject | null | undefined + set(k: string, v: crypto.KeyObject): void } -export type PimlicoWebhookBody = PimlicoSponsorshipPolicyWebhookBody; +export type PimlicoWebhookBody = PimlicoSponsorshipPolicyWebhookBody export type PimlicoSponsorshipPolicyWebhookBody = { - type: "sponsorshipPolicy.webhook", + type: "sponsorshipPolicy.webhook" data: { object: { - userOperation: UserOperation, - entryPoint: Address, - chainId: number, + userOperation: UserOperation + entryPoint: Address + chainId: number sponsorshipPolicyId: string } } -} \ No newline at end of file +}