From a8cff8cedc062be3ed1d454e9de6e456553a4d8c Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Fri, 25 Jul 2025 11:29:26 +0200 Subject: [PATCH] refactor(webhooks): updated webhooks signatures and payload to match standard-webhook spec (#430) --- .changeset/bumpy-aliens-juggle.md | 5 + packages/webhooks/package.json | 2 + .../webhooks/src/handler/handler.errors.ts | 20 +++- .../webhooks/src/handler/handler.services.ts | 43 +++++---- packages/webhooks/src/index.ts | 2 +- packages/webhooks/src/signature.test.ts | 95 +++++++++++++++++-- packages/webhooks/src/signature.ts | 67 ++++++++++--- packages/webhooks/src/webhooks.models.ts | 13 +-- packages/webhooks/src/webhooks.services.ts | 33 ++++--- packages/webhooks/src/webhooks.types.ts | 6 +- pnpm-lock.yaml | 68 ++++++++----- 11 files changed, 269 insertions(+), 85 deletions(-) create mode 100644 .changeset/bumpy-aliens-juggle.md diff --git a/.changeset/bumpy-aliens-juggle.md b/.changeset/bumpy-aliens-juggle.md new file mode 100644 index 0000000..78e2274 --- /dev/null +++ b/.changeset/bumpy-aliens-juggle.md @@ -0,0 +1,5 @@ +--- +"@papra/webhooks": minor +--- + +Breaking change: updated webhooks signatures and payload format to match standard-webhook spec diff --git a/packages/webhooks/package.json b/packages/webhooks/package.json index f27aa95..86c08ff 100644 --- a/packages/webhooks/package.json +++ b/packages/webhooks/package.json @@ -42,12 +42,14 @@ }, "dependencies": { "@corentinth/chisels": "^1.3.0", + "@paralleldrive/cuid2": "^2.2.2", "ofetch": "^1.4.1", "tsee": "^1.3.4" }, "devDependencies": { "@antfu/eslint-config": "catalog:", "eslint": "catalog:", + "standardwebhooks": "^1.0.0", "typescript": "catalog:", "unbuild": "catalog:", "vitest": "catalog:" diff --git a/packages/webhooks/src/handler/handler.errors.ts b/packages/webhooks/src/handler/handler.errors.ts index 2c79f57..9f2d816 100644 --- a/packages/webhooks/src/handler/handler.errors.ts +++ b/packages/webhooks/src/handler/handler.errors.ts @@ -2,7 +2,25 @@ export function createInvalidSignatureError() { return Object.assign( new Error('[Papra Webhooks] Invalid signature'), { - code: 'INVALID_SIGNATURE', + code: 'webhook.invalid_signature', + }, + ); +} + +export function createUnsupportedSignatureVersionError() { + return Object.assign( + new Error('[Papra Webhooks] Unsupported signature version, supported versions are "v1"'), + { + code: 'webhook.unsupported_signature_version', + }, + ); +} + +export function createInvalidSignatureFormatError() { + return Object.assign( + new Error('[Papra Webhooks] Invalid signature format, unprocessable signature'), + { + code: 'webhook.invalid_signature_format', }, ); } diff --git a/packages/webhooks/src/handler/handler.services.ts b/packages/webhooks/src/handler/handler.services.ts index fa87d49..690e358 100644 --- a/packages/webhooks/src/handler/handler.services.ts +++ b/packages/webhooks/src/handler/handler.services.ts @@ -1,36 +1,45 @@ -import type { BuildWebhookEventPayload, WebhookEvents, WebhookPayloads } from '../webhooks.types'; +import type { StandardWebhookEventPayload, WebhookEvents } from '../webhooks.types'; import { EventEmitter } from 'tsee'; import { verifySignature } from '../signature'; import { parseBody } from '../webhooks.models'; import { createInvalidSignatureError } from './handler.errors'; +function handleError({ error }: { error: unknown }) { + if (error) { + throw error; + } + + throw createInvalidSignatureError(); +} + export function createWebhooksHandler({ secret, - onInvalidSignature = () => { - createInvalidSignatureError(); - }, + onError = handleError, }: { secret: string; - onInvalidSignature?: ({ bodyBuffer, signature }: { bodyBuffer: ArrayBuffer; signature: string }) => void | Promise; + onError?: (args: { body: string; signature: string; webhookId: string; timestamp: string; error: unknown }) => void | Promise; }) { - const eventEmitter = new EventEmitter) => void }>(); + const eventEmitter = new EventEmitter void }>(); return { on: eventEmitter.on, ee: eventEmitter, - handle: async ({ bodyBuffer, signature }: { bodyBuffer: ArrayBuffer; signature: string }) => { - const isValid = await verifySignature({ bodyBuffer, signature, secret }); + handle: async ({ body, signature, webhookId, timestamp }: { body: string; signature: string; webhookId: string; timestamp: string }) => { + try { + const isValid = await verifySignature({ serializedPayload: body, signature, secret, webhookId, timestamp }); - if (!isValid) { - await onInvalidSignature({ bodyBuffer, signature }); - return; + if (!isValid) { + throw createInvalidSignatureError(); + } + + const parsedBody = parseBody(body); + const { type } = parsedBody; + + eventEmitter.emit(type, parsedBody as any); + eventEmitter.emit('*', parsedBody); + } catch (error) { + await onError({ body, signature, webhookId, timestamp, error }); } - - const payload = parseBody(bodyBuffer.toString()); - const { event } = payload; - - eventEmitter.emit(event, payload as any); - eventEmitter.emit('*', payload); }, }; } diff --git a/packages/webhooks/src/index.ts b/packages/webhooks/src/index.ts index 1acb0ac..a0894ae 100644 --- a/packages/webhooks/src/index.ts +++ b/packages/webhooks/src/index.ts @@ -1,4 +1,4 @@ export { createWebhooksHandler } from './handler/handler.services'; export { EVENT_NAMES, type EventName } from './webhooks.constants'; export { triggerWebhook } from './webhooks.services'; -export type { WebhookEventPayload, WebhookEvents, WebhookPayload, WebhookPayloads } from './webhooks.types'; +export type { StandardWebhookEventPayload, WebhookEvents, WebhookPayload, WebhookPayloads } from './webhooks.types'; diff --git a/packages/webhooks/src/signature.test.ts b/packages/webhooks/src/signature.test.ts index 92b4702..1c05ec1 100644 --- a/packages/webhooks/src/signature.test.ts +++ b/packages/webhooks/src/signature.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from 'vitest'; +import { Buffer } from 'node:buffer'; +import { Webhook } from 'standardwebhooks'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { createInvalidSignatureFormatError, createUnsupportedSignatureVersionError } from './handler/handler.errors'; import { arrayBufferToBase64, base64ToArrayBuffer, signBody, verifySignature } from './signature'; const arrayBuffer = (str: string) => new TextEncoder().encode(str).buffer as ArrayBuffer; @@ -6,25 +9,103 @@ const arrayBuffer = (str: string) => new TextEncoder().encode(str).buffer as Arr describe('signature', () => { describe('signBody', () => { test('a buffer can be signed with a secret, the resulting signature is a base64 encoded string', async () => { - const bodyBuffer = arrayBuffer('test'); + const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') }; + const serializedPayload = JSON.stringify(payload); + const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244'; + const timestamp = '1753390766'; const secret = 'secret-key'; - const { signature } = await signBody({ bodyBuffer, secret }); + const { signature } = await signBody({ serializedPayload, webhookId, timestamp, secret }); - expect(signature).to.equal('2yIt56m6njKnw7VCoPEYRQE1jSIxyuYutt8/c1ezh9M='); + expect(signature).to.equal('v1,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE='); }); }); describe('verifySignature', () => { test('verify that the signature of a buffer has been created with a given secret', async () => { - const bodyBuffer = arrayBuffer('test'); + const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') }; + const serializedPayload = JSON.stringify(payload); + const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244'; + const timestamp = '1753390766'; const secret = 'secret-key'; - const signature = '2yIt56m6njKnw7VCoPEYRQE1jSIxyuYutt8/c1ezh9M='; + const signature = 'v1,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE='; - const result = await verifySignature({ bodyBuffer, signature, secret }); + const result = await verifySignature({ serializedPayload, webhookId, timestamp, signature, secret }); expect(result).to.equal(true); }); + + test('an error is thrown when the version is not supported', async () => { + const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') }; + const serializedPayload = JSON.stringify(payload); + const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244'; + const timestamp = '1753390766'; + const secret = 'secret-key'; + const signature = 'v2,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE='; + + expect(verifySignature({ serializedPayload, webhookId, timestamp, signature, secret })).rejects.toThrow(createUnsupportedSignatureVersionError()); + }); + + test('an error is thrown when the signature is not valid', async () => { + const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') }; + const serializedPayload = JSON.stringify(payload); + const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244'; + const timestamp = '1753390766'; + const secret = 'secret-key'; + const signature = ''; + + expect(verifySignature({ serializedPayload, webhookId, timestamp, signature, secret })).rejects.toThrow(createInvalidSignatureFormatError()); + }); + }); + + describe('standardwebhooks compatibility', () => { + // Because standardwebhooks uses hardcoded Date.now() + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test('a signed payload can be verified using the "standardwebhooks" package', async () => { + const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') }; + const serializedPayload = JSON.stringify(payload); + const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244'; + const timestamp = '1753390766'; + const secret = 'secret-key'; + + // Because standardwebhooks uses hardcoded Date.now() to check for webhook expiration... + vi.setSystemTime(new Date(Number(timestamp) * 1000)); + + const webhook = new Webhook(Buffer.from(secret).toString('base64')); + + const result = await webhook.verify(serializedPayload, { + 'webhook-id': webhookId, + 'webhook-timestamp': timestamp, + 'webhook-signature': 'v1,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE=', + }); + + expect(result).to.eql({ + event: 'foo.bar', + payload: { biz: 'baz' }, + now: '2025-07-25T00:00:00.000Z', + }); + }); + + test('the signature is the same as the one generated by the "standardwebhooks" package', async () => { + const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') }; + const serializedPayload = JSON.stringify(payload); + const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244'; + const timestamp = '1753390766'; + const secret = 'secret-key'; + + const { signature } = await signBody({ serializedPayload, webhookId, timestamp, secret }); + + const standardWebhookSignature = new Webhook(Buffer.from(secret).toString('base64')).sign(webhookId, new Date(Number(timestamp) * 1000), serializedPayload); + + expect(standardWebhookSignature).to.equal(signature); + }); }); describe('arrayBufferToBase64', () => { diff --git a/packages/webhooks/src/signature.ts b/packages/webhooks/src/signature.ts index a3388d2..a3e8c48 100644 --- a/packages/webhooks/src/signature.ts +++ b/packages/webhooks/src/signature.ts @@ -1,3 +1,7 @@ +import { createInvalidSignatureFormatError, createUnsupportedSignatureVersionError } from './handler/handler.errors'; + +const WEBHOOK_SIGNATURE_HMAC_VERSION = 'v1'; + export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) { return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); } @@ -6,37 +10,74 @@ export function base64ToArrayBuffer(base64: string) { return new Uint8Array(atob(base64).split('').map(char => char.charCodeAt(0))).buffer; } +function createSignaturePayload({ + serializedPayload, + webhookId, + timestamp, +}: { + serializedPayload: string; + webhookId: string; + timestamp: string; +}) { + return `${webhookId}.${timestamp}.${serializedPayload}`; +} + +async function hmacSign({ secret, payload }: { secret: string; payload: string }) { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + return crypto.subtle.sign('HMAC', key, encoder.encode(payload)); +} + export async function signBody({ - bodyBuffer, + serializedPayload, + webhookId, + timestamp, secret, }: { - bodyBuffer: ArrayBuffer; + serializedPayload: string; + webhookId: string; + timestamp: string; secret: string; }) { - const encoder = new TextEncoder(); - const keyData = encoder.encode(secret); - const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + const payload = createSignaturePayload({ serializedPayload, webhookId, timestamp }); - const signature = await crypto.subtle.sign('HMAC', key, bodyBuffer); - const signatureBase64 = arrayBufferToBase64(signature); + const rawSignature = await hmacSign({ secret, payload }); + const signatureBase64 = arrayBufferToBase64(rawSignature); + const signature = `${WEBHOOK_SIGNATURE_HMAC_VERSION},${signatureBase64}`; - return { signature: signatureBase64 }; + return { signature }; } export async function verifySignature({ - bodyBuffer, + serializedPayload, + webhookId, + timestamp, signature: base64Signature, secret, }: { - bodyBuffer: ArrayBuffer; + serializedPayload: string; + webhookId: string; + timestamp: string; signature: string; secret: string; }): Promise { + const [version, signature] = base64Signature.split(',', 2); + + if (!signature || !version) { + throw createInvalidSignatureFormatError(); + } + + if (version !== WEBHOOK_SIGNATURE_HMAC_VERSION) { + throw createUnsupportedSignatureVersionError(); + } + + const payload = createSignaturePayload({ serializedPayload, webhookId, timestamp }); + + const signatureBuffer = base64ToArrayBuffer(signature); + const encoder = new TextEncoder(); const keyData = encoder.encode(secret); const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']); - const signatureBuffer = base64ToArrayBuffer(base64Signature); - - return crypto.subtle.verify('HMAC', key, signatureBuffer, bodyBuffer); + return crypto.subtle.verify('HMAC', key, signatureBuffer, encoder.encode(payload)); } diff --git a/packages/webhooks/src/webhooks.models.ts b/packages/webhooks/src/webhooks.models.ts index 8f6e193..e4fb64b 100644 --- a/packages/webhooks/src/webhooks.models.ts +++ b/packages/webhooks/src/webhooks.models.ts @@ -1,14 +1,15 @@ -import type { WebhookEventPayload, WebhookPayloads } from './webhooks.types'; +import type { StandardWebhookEventPayload, WebhookPayloads } from './webhooks.types'; -export function serializeBody({ now = new Date(), ...payload }: { now?: Date } & WebhookPayloads) { - const body: WebhookEventPayload = { - ...payload, - timestampMs: now.getTime(), +export function serializeBody({ now = new Date(), payload, event }: { now?: Date; payload: T['payload']; event: T['event'] }) { + const body: StandardWebhookEventPayload = { + data: payload, + type: event, + timestamp: now.toISOString(), }; return JSON.stringify(body); } export function parseBody(body: string) { - return JSON.parse(body) as WebhookEventPayload; + return JSON.parse(body) as StandardWebhookEventPayload; } diff --git a/packages/webhooks/src/webhooks.services.ts b/packages/webhooks/src/webhooks.services.ts index 2b78974..31c4437 100644 --- a/packages/webhooks/src/webhooks.services.ts +++ b/packages/webhooks/src/webhooks.services.ts @@ -1,4 +1,5 @@ import type { WebhookPayloads } from './webhooks.types'; +import { createId } from '@paralleldrive/cuid2'; import { ofetch } from 'ofetch'; import { signBody } from './signature'; import { serializeBody } from './webhooks.models'; @@ -9,7 +10,7 @@ export async function webhookHttpClient({ }: { url: string; method: string; - body: ArrayBuffer; + body: string; headers: Record; }) { const response = await ofetch.raw(url, { @@ -23,39 +24,43 @@ export async function webhookHttpClient({ }; } -export async function triggerWebhook({ +export async function triggerWebhook({ webhookUrl, webhookSecret, httpClient = webhookHttpClient, now = new Date(), - ...payload - + payload, + event, + webhookId = `msg_${createId()}`, }: { webhookUrl: string; webhookSecret?: string | null; httpClient?: typeof webhookHttpClient; + payload: T['payload']; now?: Date; -} & WebhookPayloads) { - const { event } = payload; + event: T['event']; + webhookId?: string; +}) { + const timestamp = Math.floor(now.getTime() / 1000).toString(); const headers: Record = { - 'User-Agent': 'papra-webhook-client', - 'Content-Type': 'application/json', - 'X-Event': event, + 'user-agent': 'papra-webhook-client', + 'content-type': 'application/json', + 'webhook-id': webhookId, + 'webhook-timestamp': timestamp, }; - const body = serializeBody({ ...payload, now }); - const bodyBuffer = new TextEncoder().encode(body).buffer as ArrayBuffer; + const body = serializeBody({ event, payload, now }); if (webhookSecret) { - const { signature } = await signBody({ bodyBuffer, secret: webhookSecret }); - headers['X-Signature'] = signature; + const { signature } = await signBody({ serializedPayload: body, webhookId, timestamp, secret: webhookSecret }); + headers['webhook-signature'] = signature; } const { responseData, responseStatus } = await httpClient({ url: webhookUrl, method: 'POST', - body: bodyBuffer, + body, headers, }); diff --git a/packages/webhooks/src/webhooks.types.ts b/packages/webhooks/src/webhooks.types.ts index 556d191..a711923 100644 --- a/packages/webhooks/src/webhooks.types.ts +++ b/packages/webhooks/src/webhooks.types.ts @@ -24,11 +24,11 @@ export type DocumentDeletedPayload = WebhookPayload< export type WebhookPayloads = DocumentCreatedPayload | DocumentDeletedPayload; type ExtractEventName = T extends WebhookPayload ? E : never; -export type BuildWebhookEventPayload = T & { timestampMs: number }; +export type BuildStandardWebhookEventPayload = { type: T['event']; timestamp: string; data: T['payload'] }; export type BuildWebhookEvents = { - [K in ExtractEventName]: (args: BuildWebhookEventPayload>>) => void; + [K in ExtractEventName]: (args: BuildStandardWebhookEventPayload>>) => void; }; export type WebhookEvents = BuildWebhookEvents; -export type WebhookEventPayload = BuildWebhookEventPayload; +export type StandardWebhookEventPayload = BuildStandardWebhookEventPayload; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee2289c..2e92edb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ catalogs: '@vitest/coverage-v8': specifier: ^3.0.2 version: 3.2.4 + better-auth: + specifier: ^1.2.8 + version: 1.2.8 eslint: specifier: ^9.30.1 version: 9.30.1 @@ -527,6 +530,9 @@ importers: '@corentinth/chisels': specifier: ^1.3.0 version: 1.3.1 + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 ofetch: specifier: ^1.4.1 version: 1.4.1 @@ -540,6 +546,9 @@ importers: eslint: specifier: 'catalog:' version: 9.30.1(jiti@2.4.2) + standardwebhooks: + specifier: ^1.0.0 + version: 1.0.0 typescript: specifier: 'catalog:' version: 5.8.3 @@ -2633,10 +2642,6 @@ packages: '@noble/ciphers@0.6.0': resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} - '@noble/hashes@1.6.1': - resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==} - engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -3257,6 +3262,9 @@ packages: peerDependencies: solid-js: ^1.8.6 + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@stylistic/eslint-plugin@2.13.0': resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5133,6 +5141,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} @@ -5906,9 +5917,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - loupe@3.1.4: resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} @@ -7297,6 +7305,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + starlight-links-validator@0.16.0: resolution: {integrity: sha512-wInToor19C7UxhesPuxTBIhB1LH1wzNQHD4HaumfcB+yFhg5u80yQEnkZDrABHrUEEEwFm//NoZbWhnUj1m2ug==} engines: {node: '>=18.17.1'} @@ -10117,6 +10128,12 @@ snapshots: eslint: 9.27.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.5.1(eslint@9.30.1(jiti@2.4.2))': + dependencies: + eslint: 9.30.1(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': dependencies: eslint: 9.27.0(jiti@2.4.2) @@ -10689,8 +10706,6 @@ snapshots: '@noble/ciphers@0.6.0': {} - '@noble/hashes@1.6.1': {} - '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -10734,7 +10749,7 @@ snapshots: '@paralleldrive/cuid2@2.2.2': dependencies: - '@noble/hashes': 1.6.1 + '@noble/hashes': 1.8.0 '@pdfslick/core@2.3.0(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1))': dependencies: @@ -11410,6 +11425,8 @@ snapshots: dependencies: solid-js: 1.9.7 + '@stablelib/base64@1.0.1': {} + '@stylistic/eslint-plugin@2.13.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/utils': 8.21.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) @@ -12695,7 +12712,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 + loupe: 3.1.4 pathval: 2.0.0 chalk@4.1.2: @@ -13456,15 +13473,15 @@ snapshots: eslint-plugin-astro@1.3.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) - '@jridgewell/sourcemap-codec': 1.5.4 - '@typescript-eslint/types': 8.35.1 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.30.1(jiti@2.4.2)) + '@jridgewell/sourcemap-codec': 1.5.0 + '@typescript-eslint/types': 8.26.1 astro-eslint-parser: 1.1.0(typescript@5.8.3) eslint: 9.30.1(jiti@2.4.2) - eslint-compat-utils: 0.6.5(eslint@9.30.1(jiti@2.4.2)) + eslint-compat-utils: 0.6.4(eslint@9.30.1(jiti@2.4.2)) globals: 15.15.0 - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 + postcss: 8.5.3 + postcss-selector-parser: 7.0.0 transitivePeerDependencies: - supports-color - typescript @@ -13779,7 +13796,7 @@ snapshots: debug: 4.4.1 escape-string-regexp: 4.0.0 eslint: 9.30.1(jiti@2.4.2) - eslint-compat-utils: 0.6.4(eslint@9.30.1(jiti@2.4.2)) + eslint-compat-utils: 0.6.5(eslint@9.30.1(jiti@2.4.2)) natural-compare: 1.4.0 yaml-eslint-parser: 1.3.0 transitivePeerDependencies: @@ -13867,11 +13884,11 @@ snapshots: '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.30.1 - '@eslint/plugin-kit': 0.3.1 + '@eslint/plugin-kit': 0.3.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 @@ -14012,6 +14029,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fast-uri@3.0.6: {} fast-xml-parser@4.4.1: @@ -14719,7 +14738,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: @@ -14930,8 +14949,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.3: {} - loupe@3.1.4: {} lru-cache@10.4.3: {} @@ -16754,6 +16771,11 @@ snapshots: stackback@0.0.2: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + starlight-links-validator@0.16.0(@astrojs/starlight@0.34.3(astro@5.8.0(@azure/storage-blob@12.27.0)(@types/node@24.0.10)(idb-keyval@6.2.1)(jiti@2.4.2)(rollup@4.39.0)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))): dependencies: '@astrojs/starlight': 0.34.3(astro@5.8.0(@azure/storage-blob@12.27.0)(@types/node@24.0.10)(idb-keyval@6.2.1)(jiti@2.4.2)(rollup@4.39.0)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))