refactor(webhooks): updated webhooks signatures and payload to match standard-webhook spec (#430)

This commit is contained in:
Corentin Thomasset
2025-07-25 11:29:26 +02:00
committed by GitHub
parent 67b3b14cdf
commit a8cff8cedc
11 changed files with 269 additions and 85 deletions

View File

@@ -0,0 +1,5 @@
---
"@papra/webhooks": minor
---
Breaking change: updated webhooks signatures and payload format to match standard-webhook spec

View File

@@ -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:"

View File

@@ -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',
},
);
}

View File

@@ -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<void>;
onError?: (args: { body: string; signature: string; webhookId: string; timestamp: string; error: unknown }) => void | Promise<void>;
}) {
const eventEmitter = new EventEmitter<WebhookEvents & { '*': (payload: BuildWebhookEventPayload<WebhookPayloads>) => void }>();
const eventEmitter = new EventEmitter<WebhookEvents & { '*': (payload: StandardWebhookEventPayload) => 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);
},
};
}

View File

@@ -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';

View File

@@ -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', () => {

View File

@@ -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<boolean> {
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));
}

View File

@@ -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<T extends WebhookPayloads>({ 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;
}

View File

@@ -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<string, string>;
}) {
const response = await ofetch.raw<unknown>(url, {
@@ -23,39 +24,43 @@ export async function webhookHttpClient({
};
}
export async function triggerWebhook({
export async function triggerWebhook<T extends WebhookPayloads>({
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<string, string> = {
'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,
});

View File

@@ -24,11 +24,11 @@ export type DocumentDeletedPayload = WebhookPayload<
export type WebhookPayloads = DocumentCreatedPayload | DocumentDeletedPayload;
type ExtractEventName<T> = T extends WebhookPayload<infer E, any> ? E : never;
export type BuildWebhookEventPayload<T> = T & { timestampMs: number };
export type BuildStandardWebhookEventPayload<T extends WebhookPayloads> = { type: T['event']; timestamp: string; data: T['payload'] };
export type BuildWebhookEvents<T extends WebhookPayloads> = {
[K in ExtractEventName<T>]: (args: BuildWebhookEventPayload<Extract<T, WebhookPayload<K, any>>>) => void;
[K in ExtractEventName<T>]: (args: BuildStandardWebhookEventPayload<Extract<T, WebhookPayload<K, any>>>) => void;
};
export type WebhookEvents = BuildWebhookEvents<WebhookPayloads>;
export type WebhookEventPayload = BuildWebhookEventPayload<WebhookPayloads>;
export type StandardWebhookEventPayload = BuildStandardWebhookEventPayload<WebhookPayloads>;

68
pnpm-lock.yaml generated
View File

@@ -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))