diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca8ebedfdd75..ba50d8edc855 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -747,6 +747,29 @@ jobs: directory: dev-packages/node-integration-tests token: ${{ secrets.CODECOV_TOKEN }} + job_cloudflare_integration_tests: + name: Cloudflare Integration Tests + needs: [job_get_metadata, job_build] + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + - name: Restore caches + uses: ./.github/actions/restore-cache + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + + - name: Run integration tests + working-directory: dev-packages/cloudflare-integration-tests + run: yarn test + job_remix_integration_tests: name: Remix (Node ${{ matrix.node }}) Tests needs: [job_get_metadata, job_build] @@ -1095,6 +1118,7 @@ jobs: job_deno_unit_tests, job_node_unit_tests, job_node_integration_tests, + job_cloudflare_integration_tests, job_browser_playwright_tests, job_browser_loader_tests, job_remix_integration_tests, diff --git a/.gitignore b/.gitignore index f784704ac31a..f381e7e6e24d 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ packages/gatsby/gatsby-node.d.ts # intellij *.iml +/**/.wrangler/* diff --git a/dev-packages/cloudflare-integration-tests/.eslintrc.js b/dev-packages/cloudflare-integration-tests/.eslintrc.js new file mode 100644 index 000000000000..899a60f9a2bd --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/.eslintrc.js @@ -0,0 +1,35 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['*.ts'], + parserOptions: { + project: ['tsconfig.json'], + sourceType: 'module', + }, + }, + { + files: ['suites/**/*.ts', 'suites/**/*.mjs'], + globals: { + fetch: 'readonly', + }, + rules: { + '@typescript-eslint/typedef': 'off', + // Explicitly allow ts-ignore with description for Node integration tests + // Reason: We run these tests on TS3.8 which doesn't support `@ts-expect-error` + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + 'ts-expect-error': true, + }, + ], + // We rely on having imports after init() is called for OTEL + 'import/first': 'off', + }, + }, + ], +}; diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts new file mode 100644 index 000000000000..6050ff6816c4 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -0,0 +1,78 @@ +import type { Contexts, Envelope, Event, SdkInfo } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import { expect } from 'vitest'; + +export const UUID_MATCHER = expect.stringMatching(/^[0-9a-f]{32}$/); +export const UUID_V4_MATCHER = expect.stringMatching( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, +); +export const SHORT_UUID_MATCHER = expect.stringMatching(/^[0-9a-f]{16}$/); +export const ISO_DATE_MATCHER = expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + +function dropUndefinedKeys>(obj: T): T { + for (const [key, value] of Object.entries(obj)) { + if (value === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete obj[key]; + } + } + return obj; +} + +function getSdk(): SdkInfo { + return { + integrations: expect.any(Array), + name: 'sentry.javascript.cloudflare', + packages: [ + { + name: 'npm:@sentry/cloudflare', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; +} + +function defaultContexts(eventContexts: Contexts = {}): Contexts { + return dropUndefinedKeys({ + trace: { + trace_id: UUID_MATCHER, + span_id: SHORT_UUID_MATCHER, + }, + cloud_resource: { 'cloud.provider': 'cloudflare' }, + culture: { timezone: expect.any(String) }, + runtime: { name: 'cloudflare' }, + ...eventContexts, + }); +} + +export function expectedEvent(event: Event): Event { + return dropUndefinedKeys({ + event_id: UUID_MATCHER, + timestamp: expect.any(Number), + environment: 'production', + platform: 'javascript', + sdk: getSdk(), + ...event, + contexts: defaultContexts(event.contexts), + }); +} + +export function eventEnvelope(event: Event): Envelope { + return [ + { + event_id: UUID_MATCHER, + sent_at: ISO_DATE_MATCHER, + sdk: { name: 'sentry.javascript.cloudflare', version: SDK_VERSION }, + trace: { + environment: event.environment || 'production', + public_key: 'public', + trace_id: UUID_MATCHER, + sample_rate: expect.any(String), + sampled: expect.any(String), + transaction: expect.any(String), + }, + }, + [[{ type: 'event' }, expectedEvent(event)]], + ]; +} diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json new file mode 100644 index 000000000000..4c28622b6a4d --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -0,0 +1,27 @@ +{ + "name": "@sentry-internal/cloudflare-integration-tests", + "version": "9.38.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "private": true, + "scripts": { + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix", + "test": "vitest run", + "test:watch": "yarn test --watch" + }, + "dependencies": { + "@sentry/cloudflare": "9.38.0" + }, + "devDependencies": { + "@sentry-internal/test-utils": "9.38.0", + "@cloudflare/workers-types": "^4.20250708.0", + "vitest": "^3.2.4", + "wrangler": "4.22.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts new file mode 100644 index 000000000000..849b011250f9 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -0,0 +1,235 @@ +import type { Envelope, EnvelopeItemType } from '@sentry/core'; +import { normalize } from '@sentry/core'; +import { createBasicSentryServer } from '@sentry-internal/test-utils'; +import { spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import { inspect } from 'util'; +import { expect } from 'vitest'; + +const CLEANUP_STEPS = new Set<() => void>(); + +export function cleanupChildProcesses(): void { + for (const step of CLEANUP_STEPS) { + step(); + } + CLEANUP_STEPS.clear(); +} + +process.on('exit', cleanupChildProcesses); + +function deferredPromise( + done?: () => void, +): { resolve: (val: T) => void; reject: (reason?: unknown) => void; promise: Promise } { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = (val: T) => { + res(val); + }; + reject = (reason: Error) => { + rej(reason); + }; + }); + if (!resolve || !reject) { + throw new Error('Failed to create deferred promise'); + } + return { + resolve, + reject, + promise: promise.finally(() => done?.()), + }; +} + +type Expected = Envelope | ((envelope: Envelope) => void); + +type StartResult = { + completed(): Promise; + makeRequest( + method: 'get' | 'post', + path: string, + options?: { headers?: Record; data?: BodyInit; expectError?: boolean }, + ): Promise; +}; + +/** Creates a test runner */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createRunner(...paths: string[]) { + const testPath = join(...paths); + + if (!existsSync(testPath)) { + throw new Error(`Test scenario not found: ${testPath}`); + } + + const expectedEnvelopes: Expected[] = []; + // By default, we ignore session & sessions + const ignored: Set = new Set(['session', 'sessions', 'client_report']); + + return { + expect: function (expected: Expected) { + expectedEnvelopes.push(expected); + return this; + }, + expectN: function (n: number, expected: Expected) { + for (let i = 0; i < n; i++) { + expectedEnvelopes.push(expected); + } + return this; + }, + ignore: function (...types: EnvelopeItemType[]) { + types.forEach(t => ignored.add(t)); + return this; + }, + unignore: function (...types: EnvelopeItemType[]) { + for (const t of types) { + ignored.delete(t); + } + return this; + }, + start: function (): StartResult { + const { resolve, reject, promise: isComplete } = deferredPromise(cleanupChildProcesses); + const expectedEnvelopeCount = expectedEnvelopes.length; + + let envelopeCount = 0; + const { resolve: setWorkerPort, promise: workerPortPromise } = deferredPromise(); + let child: ReturnType | undefined; + + /** Called after each expect callback to check if we're complete */ + function expectCallbackCalled(): void { + envelopeCount++; + if (envelopeCount === expectedEnvelopeCount) { + resolve(); + } + } + + function newEnvelope(envelope: Envelope): void { + if (process.env.DEBUG) log('newEnvelope', inspect(envelope, false, null, true)); + + const envelopeItemType = envelope[1][0][0].type; + + if (ignored.has(envelopeItemType)) { + return; + } + + const expected = expectedEnvelopes.shift(); + + // Catch any error or failed assertions and pass them to done to end the test quickly + try { + if (!expected) { + return; + } + + if (typeof expected === 'function') { + expected(envelope); + } else { + expect(envelope).toEqual(expected); + } + expectCallbackCalled(); + } catch (e) { + reject(e); + } + } + + createBasicSentryServer(newEnvelope) + .then(([mockServerPort, mockServerClose]) => { + if (mockServerClose) { + CLEANUP_STEPS.add(() => { + mockServerClose(); + }); + } + + if (process.env.DEBUG) log('Starting scenario', testPath); + + const stdio: ('inherit' | 'ipc' | 'ignore')[] = process.env.DEBUG + ? ['inherit', 'inherit', 'inherit', 'ipc'] + : ['ignore', 'ignore', 'ignore', 'ipc']; + + child = spawn( + 'wrangler', + [ + 'dev', + '--config', + join(testPath, 'wrangler.jsonc'), + '--show-interactive-dev-session', + 'false', + '--var', + `SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`, + ], + { stdio }, + ); + + CLEANUP_STEPS.add(() => { + child?.kill(); + }); + + child.on('error', e => { + // eslint-disable-next-line no-console + console.error('Error starting child process:', e); + reject(e); + }); + + child.on('message', (message: string) => { + const msg = JSON.parse(message) as { event: string; port?: number }; + if (msg.event === 'DEV_SERVER_READY' && typeof msg.port === 'number') { + setWorkerPort(msg.port); + if (process.env.DEBUG) log('worker ready on port', msg.port); + } + }); + }) + .catch(e => reject(e)); + + return { + completed: async function (): Promise { + return isComplete; + }, + makeRequest: async function ( + method: 'get' | 'post', + path: string, + options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {}, + ): Promise { + const url = `http://localhost:${await workerPortPromise}${path}`; + const body = options.data; + const headers = options.headers || {}; + const expectError = options.expectError || false; + + if (process.env.DEBUG) log('making request', method, url, headers, body); + + try { + const res = await fetch(url, { headers, method, body }); + + if (!res.ok) { + if (!expectError) { + reject(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`)); + } + + return; + } + + if (expectError) { + reject(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); + return; + } + + if (res.headers.get('content-type')?.includes('application/json')) { + return await res.json(); + } + + return (await res.text()) as T; + } catch (e) { + if (expectError) { + return; + } + + reject(e); + return; + } + }, + }; + }, + }; +} + +function log(...args: unknown[]): void { + // eslint-disable-next-line no-console + console.log(...args.map(arg => normalize(arg))); +} diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/index.ts b/dev-packages/cloudflare-integration-tests/suites/basic/index.ts new file mode 100644 index 000000000000..08c497cf1172 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/basic/index.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; +} + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + }), + { + async fetch(_request, _env, _ctx) { + throw new Error('This is a test error from the Cloudflare integration tests'); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/test.ts b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts new file mode 100644 index 000000000000..1bd7b4bac094 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts @@ -0,0 +1,32 @@ +import { expect, it } from 'vitest'; +import { eventEnvelope } from '../../expect'; +import { createRunner } from '../../runner'; + +it('Basic error in fetch handler', async () => { + const runner = createRunner(__dirname) + .expect( + eventEnvelope({ + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'This is a test error from the Cloudflare integration tests', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'cloudflare', handled: false }, + }, + ], + }, + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.any(String), + }, + }), + ) + .start(); + await runner.makeRequest('get', '/', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc new file mode 100644 index 000000000000..24fb2861023d --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"] +} diff --git a/dev-packages/cloudflare-integration-tests/tsconfig.json b/dev-packages/cloudflare-integration-tests/tsconfig.json new file mode 100644 index 000000000000..38816b36116e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["suites/**/*.ts", "*.ts"], + + "compilerOptions": { + // Although this seems wrong to include `DOM` here, it's necessary to make + // global fetch available in tests in lower Node versions. + "lib": ["ES2020"], + "esModuleInterop": true, + } +} diff --git a/dev-packages/cloudflare-integration-tests/vite.config.mts b/dev-packages/cloudflare-integration-tests/vite.config.mts new file mode 100644 index 000000000000..cfa15b12c3f1 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/vite.config.mts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vitest/config'; +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + coverage: { + enabled: false, + }, + isolate: false, + include: ['./suites/**/test.ts'], + testTimeout: 20_000, + // Ensure we can see debug output when DEBUG=true + ...(process.env.DEBUG + ? { + disableConsoleIntercept: true, + silent: false, + } + : {}), + // By default Vitest uses child processes to run tests but all our tests + // already run in their own processes. We use threads instead because the + // overhead is significantly less. + pool: 'threads', + reporters: process.env.DEBUG + ? ['default', { summary: false }] + : process.env.GITHUB_ACTIONS + ? ['dot', 'github-actions'] + : ['verbose'], + }, +}); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 05bcd9c17c69..b3f7a19a1d16 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -71,6 +71,7 @@ "yargs": "^16.2.0" }, "devDependencies": { + "@sentry-internal/test-utils": "9.38.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 1006d71bf3f0..c13f8ee17c26 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -12,6 +12,7 @@ import type { TransactionEvent, } from '@sentry/core'; import { normalize } from '@sentry/core'; +import { createBasicSentryServer } from '@sentry-internal/test-utils'; import { execSync, spawn, spawnSync } from 'child_process'; import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; @@ -27,7 +28,6 @@ import { assertSentrySessions, assertSentryTransaction, } from './assertions'; -import { createBasicSentryServer } from './server'; const CLEANUP_STEPS = new Set(); diff --git a/dev-packages/node-integration-tests/utils/server.ts b/dev-packages/node-integration-tests/utils/server.ts index 92e0477c845c..a1ba3f522fb1 100644 --- a/dev-packages/node-integration-tests/utils/server.ts +++ b/dev-packages/node-integration-tests/utils/server.ts @@ -1,43 +1,6 @@ -import type { Envelope } from '@sentry/core'; -import { parseEnvelope } from '@sentry/core'; import express from 'express'; import type { AddressInfo } from 'net'; -/** - * Creates a basic Sentry server that accepts POST to the envelope endpoint - * - * This does no checks on the envelope, it just calls the callback if it managed to parse an envelope from the raw POST - * body data. - */ -export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Promise<[number, () => void]> { - const app = express(); - - app.use(express.raw({ type: () => true, inflate: true, limit: '100mb' })); - app.post('/api/:id/envelope/', (req, res) => { - try { - const env = parseEnvelope(req.body as Buffer); - onEnvelope(env); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } - - res.status(200).send(); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - const address = server.address() as AddressInfo; - resolve([ - address.port, - () => { - server.close(); - }, - ]); - }); - }); -} - type HeaderAssertCallback = (headers: Record) => void; /** Creates a test server that can be used to check headers */ diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index d2317019bada..2a52de62f5fe 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -43,6 +43,9 @@ "peerDependencies": { "@playwright/test": "~1.53.2" }, + "dependencies": { + "express": "^4.21.1" + }, "devDependencies": { "@playwright/test": "~1.53.2", "@sentry/core": "9.38.0" diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 6cfbe61d9306..e9ae76f592ed 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -10,3 +10,4 @@ export { } from './event-proxy-server'; export { getPlaywrightConfig } from './playwright-config'; +export { createBasicSentryServer } from './server'; diff --git a/dev-packages/test-utils/src/server.ts b/dev-packages/test-utils/src/server.ts new file mode 100644 index 000000000000..b8941b4b0c32 --- /dev/null +++ b/dev-packages/test-utils/src/server.ts @@ -0,0 +1,39 @@ +import type { Envelope } from '@sentry/core'; +import { parseEnvelope } from '@sentry/core'; +import express from 'express'; +import type { AddressInfo } from 'net'; + +/** + * Creates a basic Sentry server that accepts POST to the envelope endpoint + * + * This does no checks on the envelope, it just calls the callback if it managed to parse an envelope from the raw POST + * body data. + */ +export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Promise<[number, () => void]> { + const app = express(); + + app.use(express.raw({ type: () => true, inflate: true, limit: '100mb' })); + app.post('/api/:id/envelope/', (req, res) => { + try { + const env = parseEnvelope(req.body as Buffer); + onEnvelope(env); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + + res.status(200).send(); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + resolve([ + address.port, + () => { + server.close(); + }, + ]); + }); + }); +} diff --git a/dev-packages/test-utils/tsconfig.json b/dev-packages/test-utils/tsconfig.json index 7b0fa87fc45b..43f50e435628 100644 --- a/dev-packages/test-utils/tsconfig.json +++ b/dev-packages/test-utils/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "target": "ES2022", - "module": "ES2022" + "module": "ES2022", + // package-specific options + "esModuleInterop": true, }, "include": ["src/**/*.ts"] } diff --git a/package.json b/package.json index 591424f91ec5..3db67f909d91 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,10 @@ "dedupe-deps:check": "yarn-deduplicate yarn.lock --list --fail", "dedupe-deps:fix": "yarn-deduplicate yarn.lock", "postpublish": "lerna run --stream --concurrency 1 postpublish", - "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\" test", - "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\" test:unit", + "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests,cloudflare-integration-tests}\" test", + "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests,cloudflare-integration-tests}\" test:unit", "test:update-snapshots": "lerna run test:update-snapshots", - "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\"", + "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests,cloudflare-integration-tests}\"", "test:pr:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts --affected", "test:pr:node": "UNIT_TEST_ENV=node ts-node ./scripts/ci-unit-tests.ts --affected", "test:ci:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts", @@ -95,6 +95,7 @@ "dev-packages/bundle-analyzer-scenarios", "dev-packages/e2e-tests", "dev-packages/node-integration-tests", + "dev-packages/cloudflare-integration-tests", "dev-packages/node-core-integration-tests", "dev-packages/test-utils", "dev-packages/size-limit-gh-action", diff --git a/yarn.lock b/yarn.lock index 333cac9f5d06..793e1dcd0912 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2694,6 +2694,11 @@ resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250620.0.tgz#a22e635a631212963b84e315191614b20c4ad317" integrity sha512-EVvRB/DJEm6jhdKg+A4Qm4y/ry1cIvylSgSO3/f/Bv161vldDRxaXM2YoQQWFhLOJOw0qtrHsKOD51KYxV1XCw== +"@cloudflare/workers-types@^4.20250708.0": + version "4.20250708.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250708.0.tgz#80b7087a6bd475ec4b29591be310913f2d76caf9" + integrity sha512-jEIHy5lPtGFumnRWNR4tE9lZvR+E+DRGvSKe/C32uAlk9n7fjGfyCGN1QSCn7MTEZjXLWTck3K0fsmOjt6VWCQ== + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"