diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..11b8ffe --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Optimizely SDK Configuration +# Get your SDK key from the Optimizely dashboard: Settings > Environments +OPTIMIZELY_SDK_KEY=your_optimizely_sdk_key + +# Optional: Set datafile cache TTL in seconds (default is 300 seconds / 5 minutes) +# Example values: "300" (5 minutes), "600" (10 minutes), etc +OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS="300" + +# Optional: Set log level for development (Error, Warn, Info, Debug) +# LOG_LEVEL=Error diff --git a/.gitignore b/.gitignore index 764bd47..935478f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,96 +11,41 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories +## Node / dependency folders node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity +## Build / test output +dist/ +coverage/ +.nyc_output/ +*.lcov -# dotenv environment variables file +## Environment files .env .env.test -# parcel-bundler cache (https://parceljs.org/) -.cache +## Cloudflare Wrangler +.wrangler/ -# Next.js build output -.next +## Logs +logs +*.log -# Nuxt.js build / generate output -.nuxt -dist +## Editor / OS files +.DS_Store +.vscode/ +thumbs.db -# Gatsby files +## Tool caches .cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ +.rpt2_cache/ -# DynamoDB Local files -.dynamodb/ +## npm +.npm +.npmrc -# TernJS port file -.tern-port +## Misc +.node_repl_history -worker \ No newline at end of file +## Distribution / packaging +dist/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..16e8e66 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..031e740 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["miniflare"] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 86ec5be..171de93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,63 @@ # Change Log + All notable changes to this project will be documented in this file. - + The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). - + +## [1.0.0] - October 10, 2025 + +### Changed + +- Updated `@optimizely/optimizely-sdk` to version `6.1.0` with ES modules support. +- Migrated from addEventListener pattern to modern Cloudflare Workers fetch handler export syntax. +- Replaced `uuid` package with native `crypto.randomUUID()` for user ID generation. +- Replaced Webpack build with native ES modules. +- Migrated configuration from `wrangler.toml` to `wrangler.jsonc`. +- Refactored request handler to return standardized response format: `{statusCode: number, body: string, headers: Object}`. +- Implemented graceful degradation: worker continues to function even if Optimizely initialization fails. +- Enhanced error handling with try-catch blocks around all Optimizely operations - errors are logged but don't break the worker. +- Added comprehensive JSDoc documentation throughout the codebase. +- Improved datafile caching with module-scope cache and configurable TTL via `OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS` environment variable (default: 300 seconds). +- Updated `cookie` package from `0.4.2` to `1.0.2`. +- Migrated from Prettier to Biome for code formatting and linting. + +### Added + +- New `CloudflareRequestHandler` class implementing custom request handler for Cloudflare Workers environment. +- New `getOptimizelyClient` helper function for centralized client management with smart caching. +- New `getDatafile` helper function for fetching datafiles from Optimizely CDN. +- Comprehensive unit test suite using Vitest and vitest-environment-miniflare. +- Test files: `index.test.js`, `optimizely_helper.test.js`, `request_handler.test.js`. +- Test utilities and setup files for easier testing. +- `biome.jsonc` configuration for code quality tools. +- `vitest.config.js` for test configuration. + +### Removed + +- Removed `uuid` package dependency (replaced with native Web Crypto API). +- Removed Webpack build configuration (`webpack.config.js`). +- Removed old `wrangler.toml` (replaced with `wrangler.jsonc`). +- Removed Prettier in favor of Biome. + +### Fixed + +- Response body is now always returned as a string for consistent interface with Optimizely SDK expectations. +- Worker no longer returns 500 errors on Optimizely failures; continues with degraded functionality. +- Improved AbortError handling in request handler with proper error wrapping. + ## [0.2.0] - April 4, 2022 ### Added + - Added cookie based user id memorization to support sticky [bucketing](https://docs.developers.optimizely.com/full-stack/v4.0/docs/how-bucketing-works). ### Changed + - Updated `@optimizely/optimizely-sdk` version to `4.9.1`. - + ## [0.1.0] - November 18, 2021 - + ### Added + - First version of Cloudflare worker template with datafile caching and event dispatcher. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce74a92..5fe308d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,4 +41,4 @@ The YEAR above should be the year of the contribution. If work on the file has b ## Contact -If you have questions, please contact developers@optimizely.com. \ No newline at end of file +If you have questions, please contact developers@optimizely.com. diff --git a/README.md b/README.md index bbea4c2..113c381 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,21 @@ Optimizely Feature Experimentation is an A/B testing and feature management tool Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap. +## Features + +- **Modern ES Modules**: Uses ES module syntax for better tree-shaking and modern JavaScript features +- **Optimizely SDK v6**: Latest version of the Optimizely JavaScript SDK +- **Cloudflare Cache Integration**: Automatic datafile caching using Cloudflare's edge cache +- **Development Tools**: Integrated Biome for linting/formatting, Miniflare for local development +- **Cookie-based User Persistence**: Automatic user ID generation and persistence + ## Get Started -Refer to the [Optimizely Cloudflare Workers Starter Kit documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/cloudflare-workers) for detailed instructions about using this starter kit. +Refer to the [Optimizely Cloudflare Workers Starter Kit documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/cloudflare-workers) for detailed instructions about using this starter kit. ### Prerequisites -1. You will need an **Optimizely Account**. If you do not have an account, you can [register for a free account](https://www.optimizely.com/products/intelligence/full-stack-experimentation/). +1. You will need an **Optimizely Account**. If you do not have an account, you can [register for a free account](https://www.optimizely.com/products/feature-experimentation/). 2. You will need to have a **Cloudflare Account with Workers**. For more information, visit the official [Cloudflare Workers product page here](https://workers.cloudflare.com/). @@ -26,32 +34,114 @@ Refer to the [Optimizely Cloudflare Workers Starter Kit documentation](https://d wrangler generate projectname https://github.com/optimizely/cloudflare-worker-template ``` -2. Add `account_id` in `wrangler.toml`. If you dont know the account ID, just do `wrangler dev` and the CLI will prompt you with the account ID and the instructions to add it. +2. Add `account_id` in `wrangler.jsonc`. If you do not know the account ID, run `wrangler dev` and the CLI will prompt you with the account ID and the instructions to add it. 3. Install node packages. - ``` - npm install - ``` + + ``` + npm install + ``` + +4. **Configure your Optimizely SDK Key**: + + Copy the example environment file: + + ``` + cp .env.example .env + ``` + + Then set your SDK key using one of these methods: + + **Option A: Using Wrangler Secrets (Recommended for production)** + + ``` + wrangler secret put OPTIMIZELY_SDK_KEY + ``` + + **Option B: Using wrangler.jsonc for development** + + Edit `wrangler.jsonc` and replace `YOUR_SDK_KEY` with your actual SDK key: + + ```jsonc + { + "vars": { + "OPTIMIZELY_SDK_KEY": "YOUR_SDK_KEY", + "OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS": "300" // Optional: cache TTL in seconds (default: 300) + } + } + ``` + + **Option C: Using .env file for local development** + + ``` + OPTIMIZELY_SDK_KEY=your_sdk_key + OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS=300 # Optional: cache TTL in seconds (default: 300) + ``` + + > **Note**: Your SDK keys can be found in the Optimizely application under **Settings > Environments**. + +5. **Set up your account ID** (if deploying): Add `account_id` in `wrangler.jsonc`. If you don't know the account ID, run `wrangler whoami` or `wrangler dev` and the CLI will prompt you with instructions. + +## Project Structure + +``` +├── src/ +│ ├── index.js # Main worker entry point +│ ├── optimizely_helper.js # Optimizely SDK integration utilities +│ └── request_handler.js # Cloudflare-specific HTTP request handler +├── test/ +│ ├── index.test.js # Tests for main worker +│ ├── optimizely_helper.test.js # Tests for Optimizely helper functions +│ ├── request_handler.test.js # Tests for request handler +│ ├── setup.js # Test environment setup +│ └── test-utils.js # Shared test utilities and mocks +├── .env.example # Environment variable template +├── biome.jsonc # Biome configuration for linting/formatting +├── package.json # Node.js dependencies and scripts +├── vitest.config.js # Vitest testing framework configuration +└── wrangler.jsonc # Cloudflare Workers configuration +``` ## Use the Cloudflare Workers Starter Kit -The Optimizely starter kit for Cloudflare Workers embeds and extends our [Javascript (Node) SDK](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-node-sdk). For a guide to getting started with our platform more generally, you can reference our [Javascript (Node) Quickstart developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-node-quickstart). +The Optimizely starter kit for Cloudflare Workers embeds and extends our [Javascript SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk). For a guide to getting started with our platform more generally, you can reference our [Javascript Quickstart developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk-quickstart). + +> Note: This starter kit makes use of the "Universal" version of our Javascript SDK which explicitly excludes the datafile manager and event processor features for better performance. The datafile is fetched and cached using Cloudflare's cache API, and event dispatching is handled through the provided platform-specific `getOptimizelyClient()` helper. + +### Development + +This template includes modern development tools for a better developer experience: -> Note: This starter kit in particular makes use of the "Lite" version of our Javascript SDK for Node.js which explicitly excludes the datafile manager and event processor features for better performance. As a result, it is expected that you will provide the datafile manually to the Optimizely SDK either through a local file reference or by using the provided platform-specific `getDatafile()` helper to load in your Optimizely project's datafile. +- **Biome**: Fast formatter and linter for JavaScript +- **Miniflare**: Local development server that simulates Cloudflare Workers +- **Vitest**: Fast unit testing framework + +Available development commands: + +```bash +npm run dev # Start local development server +npm run format # Format code with Biome +npm run lint # Lint code with Biome +npm run test # Run tests with Vitest +npm run build # Build and validate (dry-run deploy) +``` ### Initialization -Sample code is included in `src/main.js` that shows examples of initializing and using the Optimizely JavaScript (Node) SDK interface for performing common functions such as creating user context, adding a notification listener, and making a decision based on the created user context. +Sample code is included in `src/index.js` that shows examples of initializing and using the Optimizely JavaScript (Node) SDK interface for performing common functions such as creating user context, adding a notification listener, and making decisions based on the created user context. -Additional platform-specific code is included in `src/optimizely_helper.js` which provide workarounds for otherwise common features of the Optimizely SDK. +Additional platform-specific code is included in `src/optimizely_helper.js` which provides workarounds for otherwise common features of the Optimizely SDK, including: -1. Update your Optimizely `sdkKey`, `flagKey` and `userId` in `src/index.js`. Your SDK keys can be found in the Optimizely application under **Settings**. +- **Datafile Caching**: Automatic fetching and caching of the Optimizely datafile using Cloudflare's cache API +- **Client Management**: Efficient client initialization and datafile updates +- **Event Dispatching**: Optional event dispatching to Optimizely's logging backend +1. **Configure your feature flags**: Update the `YOUR_FLAG_HERE` placeholder in `src/index.js` with your actual flag keys from the Optimizely dashboard. 2. Test and debug the worker locally. ``` - wrangler dev + npm run dev ``` ### Publishing @@ -59,7 +149,7 @@ Additional platform-specific code is included in `src/optimizely_helper.js` whic 3. Deploy the worker on Cloudflare. ``` - wrangler publish + wrangler deploy ``` 4. Optionally, tail the logs for debugging when accessing worker deployed on Cloudflare. @@ -71,15 +161,21 @@ Additional platform-specific code is included in `src/optimizely_helper.js` whic ### Caching with Cloudflare -This template uses Cloudflare cache API to provide performant caching for the [Optimizely Datafile](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/manage-config-datafile). +This template uses Cloudflare's cache API to provide performant caching for the [Optimizely Datafile](https://docs.developers.optimizely.com/feature-experimentation/docs/manage-config-datafile). The datafile is automatically fetched from Optimizely's CDN and cached for 5 minutes by default (configurable via the `OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS` environment variable), with Cloudflare edge caching providing additional performance benefits. + +**Cache Configuration:** +- **Default TTL**: 5 minutes (300 seconds) +- **Configurable via**: `OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS` environment variable +- **Example values**: `300` (5 minutes), `600` (10 minutes), `1800` (30 minutes) +- **Behavior**: If the datafile fetch fails, the cached datafile will continue to be used (stale-while-revalidate pattern) ### Identity Management -Out of the box, Optimizely's Feature Experimentation SDKs require a user-provided identifier to be passed in at runtime to drive experiment and feature flag decisions. This example generates a unique ID, stores it in a cookie and reuses it to make the decisions sticky. Alternatively, you can use an existing unique identifier available within your application and pass it in as the value for the `OPTIMIZELY_USER_ID` cookie. +Out of the box, Optimizely's Feature Experimentation SDKs require a user-provided identifier to be passed in at runtime to drive experiment and feature flag decisions. This example generates a unique ID using `crypto.randomUUID()`, stores it in a cookie, and reuses it to make the decisions sticky. Alternatively, you can use an existing unique identifier available within your application and pass it in as the value for the `OPTIMIZELY_USER_ID` cookie. ### Bucketing -For more information on how Optimizely Feature Experimentation SDKs assign users to feature flags and experiments, see [the documentation on how bucketing works](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/how-bucketing-works). +For more information on how Optimizely Feature Experimentation SDKs assign users to feature flags and experiments, see [the documentation on how bucketing works](https://docs.developers.optimizely.com/feature-experimentation/docs/how-bucketing-works-feature-experimentation). ### Cloudflare Workers @@ -88,7 +184,7 @@ For more information about Cloudflare Workers, you may visit the following resou - [Cloudflare Workers](https://workers.cloudflare.com/) - [Cloudflare Workers documentation](https://developers.cloudflare.com/workers/) - [Cloudflare Workers tutorials](https://developers.cloudflare.com/workers/tutorials) -- [Cloudflare Workers with Optimizely documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/cloudflare-workers) +- [Cloudflare Workers with Optimizely documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/cloudflare-workers) ## SDK Development @@ -130,4 +226,4 @@ Please see [CONTRIBUTING](CONTRIBUTING.md). - Fastly Compute@Edge - https://github.com/optimizely/fastly-compute-starter-kit -- Vercel Functions - https://github.com/optimizely/vercel-examples/tree/main/edge-functions/feature-flag-optimizely \ No newline at end of file +- Vercel Functions - https://github.com/optimizely/vercel-examples/tree/main/edge-functions/feature-flag-optimizely diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..c6b6c4c --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true + }, + "assist": { + "enabled": true + }, + "files": { + "includes": ["**", "!dist/**/*", "!node_modules/**/*"] + } +} diff --git a/package-lock.json b/package-lock.json index ab5c6df..40da0f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,102 +1,3222 @@ { - "name": "optimizely-cloudflare-worker-template", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@optimizely/js-sdk-datafile-manager": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-datafile-manager/-/js-sdk-datafile-manager-0.9.4.tgz", - "integrity": "sha512-963NZtcN2mxuLxeSNPZuTLozc0odX3G56DiNBlDGFECelYoLpvXmars9PwmSQ//oJR6EOincHTvvychnxwKLjQ==", - "requires": { - "@optimizely/js-sdk-logging": "^0.3.1", - "@optimizely/js-sdk-utils": "^0.4.0", - "decompress-response": "^4.2.1" - } - }, - "@optimizely/js-sdk-event-processor": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-event-processor/-/js-sdk-event-processor-0.9.5.tgz", - "integrity": "sha512-g5zqAjJuexxgbNvn7dacFkQXQxH3+OtjELfmSswvhxP9EHkyNR0ZdQF/kBxFxr335F2/RRPvAJ9tQBPkwaBg8g==", - "requires": { - "@optimizely/js-sdk-logging": "^0.3.1", - "@optimizely/js-sdk-utils": "^0.4.0" - } - }, - "@optimizely/js-sdk-logging": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.3.1.tgz", - "integrity": "sha512-K71Jf283FP0E4oXehcXTTM3gvgHZHr7FUrIsw//0mdJlotHJT4Nss4hE0CWPbBxO7LJAtwNnO+VIA/YOcO4vHg==", - "requires": { - "@optimizely/js-sdk-utils": "^0.4.0" - } - }, - "@optimizely/js-sdk-utils": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.4.0.tgz", - "integrity": "sha512-QG2oytnITW+VKTJK+l0RxjaS5VrA6W+AZMzpeg4LCB4Rn4BEKtF+EcW/5S1fBDLAviGq/0TLpkjM3DlFkJ9/Gw==", - "requires": { - "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "@optimizely/optimizely-sdk": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@optimizely/optimizely-sdk/-/optimizely-sdk-4.9.1.tgz", - "integrity": "sha512-BHwoXONZKOBI2DyXBc8gsYgPgGltCO42/11iFFc4oOnZFkR2UwO7PI2S7oeE2SN168ObTNhmEaJWgenIJuE00A==", - "requires": { - "@optimizely/js-sdk-datafile-manager": "^0.9.1", - "@optimizely/js-sdk-event-processor": "^0.9.2", - "@optimizely/js-sdk-logging": "^0.3.1", - "@optimizely/js-sdk-utils": "^0.4.0", - "json-schema": "^0.4.0", - "murmurhash": "0.0.2" - } - }, - "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" - }, - "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "requires": { - "mimic-response": "^2.0.0" - } - }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" - }, - "murmurhash": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-0.0.2.tgz", - "integrity": "sha1-bwe9ihEF5wnCb8iUIMtZMMJFhf4=" - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } - } -} + "name": "optimizely-cloudflare-worker-template", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "optimizely-cloudflare-worker-template", + "version": "2.0.0", + "license": "Apache-2.0", + "dependencies": { + "@optimizely/optimizely-sdk": "^6.1.0", + "cookie": "^1.0.2" + }, + "devDependencies": { + "@biomejs/biome": "^2.2.4", + "miniflare": "^4.20250917.0", + "vitest": "^3.2.4", + "vitest-environment-miniflare": "^2.14.4" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.4.tgz", + "integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==", + "dev": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.2.4", + "@biomejs/cli-darwin-x64": "2.2.4", + "@biomejs/cli-linux-arm64": "2.2.4", + "@biomejs/cli-linux-arm64-musl": "2.2.4", + "@biomejs/cli-linux-x64": "2.2.4", + "@biomejs/cli-linux-x64-musl": "2.2.4", + "@biomejs/cli-win32-arm64": "2.2.4", + "@biomejs/cli-win32-x64": "2.2.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz", + "integrity": "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz", + "integrity": "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz", + "integrity": "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz", + "integrity": "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz", + "integrity": "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz", + "integrity": "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz", + "integrity": "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz", + "integrity": "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250917.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250917.0.tgz", + "integrity": "sha512-0kL/kFnKUSycoo7b3PgM0nRyZ+1MGQAKaXtE6a2+SAeUkZ2FLnuFWmASi0s4rlWGsf/rlTw4AwXROePir9dUcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250917.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250917.0.tgz", + "integrity": "sha512-3/N1QmEJsC8Byxt1SGgVp5o0r+eKjuUEMbIL2yzLk/jrMdErPXy/DGf/tXZoACU68a/gMEbbT1itkYrm85iQHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250917.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250917.0.tgz", + "integrity": "sha512-E7sEow7CErbWY3olMmlbj6iss9r7Xb2uMyc+MKzYC9/J6yFlJd/dNHvjey9QIdxzbkC9qGe90a+KxQrjs+fspA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250917.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250917.0.tgz", + "integrity": "sha512-roOnRjxut2FUxo6HA9spbfs32naXAsnSQqsgku3iq6BYKv1QqGiFoY5bReK72N5uxmhxo7+RiTo8ZEkxA/vMIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250917.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250917.0.tgz", + "integrity": "sha512-gslh6Ou9+kshHjR1BJX47OsbPw3/cZCvGDompvaW/URCgr7aMzljbgmBb7p0uhwGy1qCXcIt31St6pd3IEcLng==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20250918.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250918.0.tgz", + "integrity": "sha512-mqTyfBPYUrUfHwnmLOnBTBrtEiuO45MIVxvbJ1blivIZC+0YMFskQnrcPn1txQM2S4LKnwmFv1XMgjt0qMma1Q==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@miniflare/cache": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/cache/-/cache-2.14.4.tgz", + "integrity": "sha512-ayzdjhcj+4mjydbNK7ZGDpIXNliDbQY4GPcY2KrYw0v1OSUdj5kZUkygD09fqoGRfAks0d91VelkyRsAXX8FQA==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/core": "2.14.4", + "@miniflare/shared": "2.14.4", + "http-cache-semantics": "^4.1.0", + "undici": "5.28.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/cache/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@miniflare/core": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/core/-/core-2.14.4.tgz", + "integrity": "sha512-FMmZcC1f54YpF4pDWPtdQPIO8NXfgUxCoR9uyrhxKJdZu7M6n8QKopPVNuaxR40jcsdxb7yKoQoFWnHfzJD9GQ==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@iarna/toml": "^2.2.5", + "@miniflare/queues": "2.14.4", + "@miniflare/shared": "2.14.4", + "@miniflare/watcher": "2.14.4", + "busboy": "^1.6.0", + "dotenv": "^10.0.0", + "kleur": "^4.1.4", + "set-cookie-parser": "^2.4.8", + "undici": "5.28.4", + "urlpattern-polyfill": "^4.0.3" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/core/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@miniflare/d1": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/d1/-/d1-2.14.4.tgz", + "integrity": "sha512-pMBVq9XWxTDdm+RRCkfXZP+bREjPg1JC8s8C0JTovA9OGmLQXqGTnFxIaS9vf1d8k3uSUGhDzPTzHr0/AUW1gA==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/core": "2.14.4", + "@miniflare/shared": "2.14.4" + }, + "engines": { + "node": ">=16.7" + } + }, + "node_modules/@miniflare/durable-objects": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/durable-objects/-/durable-objects-2.14.4.tgz", + "integrity": "sha512-+JrmHP6gHHrjxV8S3axVw5lGHLgqmAGdcO/1HJUPswAyJEd3Ah2YnKhpo+bNmV4RKJCtEq9A2hbtVjBTD2YzwA==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/core": "2.14.4", + "@miniflare/shared": "2.14.4", + "@miniflare/storage-memory": "2.14.4", + "undici": "5.28.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/durable-objects/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@miniflare/html-rewriter": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/html-rewriter/-/html-rewriter-2.14.4.tgz", + "integrity": "sha512-GB/vZn7oLbnhw+815SGF+HU5EZqSxbhIa3mu2L5MzZ2q5VOD5NHC833qG8c2GzDPhIaZ99ITY+ZJmbR4d+4aNQ==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/core": "2.14.4", + "@miniflare/shared": "2.14.4", + "html-rewriter-wasm": "^0.4.1", + "undici": "5.28.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/html-rewriter/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@miniflare/kv": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/kv/-/kv-2.14.4.tgz", + "integrity": "sha512-QlERH0Z+klwLg0xw+/gm2yC34Nnr/I0GcQ+ASYqXeIXBwjqOtMBa3YVQnocaD+BPy/6TUtSpOAShHsEj76R2uw==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/shared": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/queues": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/queues/-/queues-2.14.4.tgz", + "integrity": "sha512-aXQ5Ik8Iq1KGMBzGenmd6Js/jJgqyYvjom95/N9GptCGpiVWE5F0XqC1SL5rCwURbHN+aWY191o8XOFyY2nCUA==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/shared": "2.14.4" + }, + "engines": { + "node": ">=16.7" + } + }, + "node_modules/@miniflare/r2": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/r2/-/r2-2.14.4.tgz", + "integrity": "sha512-4ctiZWh7Ty7LB3brUjmbRiGMqwyDZgABYaczDtUidblo2DxX4JZPnJ/ZAyxMPNJif32kOJhcg6arC2hEthR9Sw==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/core": "2.14.4", + "@miniflare/shared": "2.14.4", + "undici": "5.28.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/r2/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@miniflare/runner-vm": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/runner-vm/-/runner-vm-2.14.4.tgz", + "integrity": "sha512-Nog0bB9SVhPbZAkTWfO4lpLAUsBXKEjlb4y+y66FJw77mPlmPlVdpjElCvmf8T3VN/pqh83kvELGM+/fucMf4g==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/shared": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/shared": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/shared/-/shared-2.14.4.tgz", + "integrity": "sha512-upl4RSB3hyCnITOFmRZjJj4A72GmkVrtfZTilkdq5Qe5TTlzsjVeDJp7AuNUM9bM8vswRo+N5jOiot6O4PVwwQ==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@types/better-sqlite3": "^7.6.0", + "kleur": "^4.1.4", + "npx-import": "^1.1.4", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/shared-test-environment": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/shared-test-environment/-/shared-test-environment-2.14.4.tgz", + "integrity": "sha512-FdU2/8wEd00vIu+MfofLiHcfZWz+uCbE2VTL85KpyYfBsNGAbgRtzFMpOXdoXLqQfRu6MBiRwWpb2FbMrBzi7g==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@cloudflare/workers-types": "^4.20221111.1", + "@miniflare/cache": "2.14.4", + "@miniflare/core": "2.14.4", + "@miniflare/d1": "2.14.4", + "@miniflare/durable-objects": "2.14.4", + "@miniflare/html-rewriter": "2.14.4", + "@miniflare/kv": "2.14.4", + "@miniflare/queues": "2.14.4", + "@miniflare/r2": "2.14.4", + "@miniflare/shared": "2.14.4", + "@miniflare/sites": "2.14.4", + "@miniflare/storage-memory": "2.14.4", + "@miniflare/web-sockets": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/shared/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@miniflare/sites": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/sites/-/sites-2.14.4.tgz", + "integrity": "sha512-O5npWopi+fw9W9Ki0gy99nuBbgDva/iXy8PDC4dAXDB/pz45nISDqldabk0rL2t4W2+lY6LXKzdOw+qJO1GQTA==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/kv": "2.14.4", + "@miniflare/shared": "2.14.4", + "@miniflare/storage-file": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/storage-file": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/storage-file/-/storage-file-2.14.4.tgz", + "integrity": "sha512-JxcmX0hXf4cB0cC9+s6ZsgYCq+rpyUKRPCGzaFwymWWplrO3EjPVxKCcMxG44jsdgsII6EZihYUN2J14wwCT7A==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/shared": "2.14.4", + "@miniflare/storage-memory": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/storage-memory": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/storage-memory/-/storage-memory-2.14.4.tgz", + "integrity": "sha512-9jB5BqNkMZ3SFjbPFeiVkLi1BuSahMhc/W1Y9H0W89qFDrrD+z7EgRgDtHTG1ZRyi9gIlNtt9qhkO1B6W2qb2A==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/shared": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/watcher": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/watcher/-/watcher-2.14.4.tgz", + "integrity": "sha512-PYn05ET2USfBAeXF6NZfWl0O32KVyE8ncQ/ngysrh3hoIV7l3qGGH7ubeFx+D8VWQ682qYhwGygUzQv2j1tGGg==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/shared": "2.14.4" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/web-sockets": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/@miniflare/web-sockets/-/web-sockets-2.14.4.tgz", + "integrity": "sha512-stTxvLdJ2IcGOs76AnvGYAzGvx8JvQPRxC5DW0P5zdAAnhL33noqb5LKdPt3P37BKp9FzBKZHuihQI9oVqwm0g==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/core": "2.14.4", + "@miniflare/shared": "2.14.4", + "undici": "5.28.4", + "ws": "^8.2.2" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@miniflare/web-sockets/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@optimizely/optimizely-sdk": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@optimizely/optimizely-sdk/-/optimizely-sdk-6.1.0.tgz", + "integrity": "sha512-p678Osy8QUiue7kdHiujeuKHHwNL82BPH2ubXeaFX8oJ4PASnSHzcVhpaCBI7GjuRPz3ZYVlZqL/Kylc3AO70Q==", + "dependencies": { + "decompress-response": "^7.0.0", + "json-schema": "^0.4.0", + "murmurhash": "^2.0.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": ">=1.0.0 <3.0.0", + "@react-native-community/netinfo": ">=5.0.0 <12.0.0", + "fast-text-encoding": "^1.0.6", + "react-native-get-random-values": "^1.11.0", + "ua-parser-js": "^1.0.38" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + }, + "@react-native-community/netinfo": { + "optional": true + }, + "fast-text-encoding": { + "optional": true + }, + "react-native-get-random-values": { + "optional": true + }, + "ua-parser-js": { + "optional": true + } + } + }, + "node_modules/@optimizely/optimizely-sdk/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", + "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", + "dev": true, + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz", + "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==", + "dev": true, + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", + "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", + "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", + "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", + "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", + "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", + "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", + "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", + "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", + "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", + "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", + "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", + "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", + "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", + "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", + "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", + "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", + "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", + "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", + "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", + "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", + "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", + "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", + "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz", + "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", + "dev": true + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz", + "integrity": "sha512-6IvPrADQyyPGLpMnUh6kfKiqy7SrbXbjoUuZ90WMBJKErzv2pCiwlGEXjRX9/54OnTq+XFVnkOnOMzclLI5aEA==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/html-rewriter-wasm": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/html-rewriter-wasm/-/html-rewriter-wasm-0.4.1.tgz", + "integrity": "sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true + }, + "node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/miniflare": { + "version": "4.20250917.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250917.0.tgz", + "integrity": "sha512-A7kYEc/Y6ohiiTji4W/qGJj3aJNc/9IMj/6wLy2phD/iMjcoY8t35654gR5mHbMx0AgUolDdr3HOsHB0cYBf+Q==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "7.14.0", + "workerd": "1.20250917.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/murmurhash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-2.0.1.tgz", + "integrity": "sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npx-import": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/npx-import/-/npx-import-1.1.4.tgz", + "integrity": "sha512-3ShymTWOgqGyNlh5lMJAejLuIv3W1K3fbI5Ewc6YErZU3Sp0PqsNs8UIU1O8z5+KVl/Du5ag56Gza9vdorGEoA==", + "dev": true, + "dependencies": { + "execa": "^6.1.0", + "parse-package-name": "^1.0.0", + "semver": "^7.3.7", + "validate-npm-package-name": "^4.0.0" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-package-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-package-name/-/parse-package-name-1.0.0.tgz", + "integrity": "sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", + "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.2", + "@rollup/rollup-android-arm64": "4.50.2", + "@rollup/rollup-darwin-arm64": "4.50.2", + "@rollup/rollup-darwin-x64": "4.50.2", + "@rollup/rollup-freebsd-arm64": "4.50.2", + "@rollup/rollup-freebsd-x64": "4.50.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", + "@rollup/rollup-linux-arm-musleabihf": "4.50.2", + "@rollup/rollup-linux-arm64-gnu": "4.50.2", + "@rollup/rollup-linux-arm64-musl": "4.50.2", + "@rollup/rollup-linux-loong64-gnu": "4.50.2", + "@rollup/rollup-linux-ppc64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-musl": "4.50.2", + "@rollup/rollup-linux-s390x-gnu": "4.50.2", + "@rollup/rollup-linux-x64-gnu": "4.50.2", + "@rollup/rollup-linux-x64-musl": "4.50.2", + "@rollup/rollup-openharmony-arm64": "4.50.2", + "@rollup/rollup-win32-arm64-msvc": "4.50.2", + "@rollup/rollup-win32-ia32-msvc": "4.50.2", + "@rollup/rollup-win32-x64-msvc": "4.50.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "optional": true + }, + "node_modules/undici": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true + }, + "node_modules/urlpattern-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-4.0.3.tgz", + "integrity": "sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ==", + "dev": true + }, + "node_modules/validate-npm-package-name": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", + "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/vite": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", + "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-environment-miniflare": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/vitest-environment-miniflare/-/vitest-environment-miniflare-2.14.4.tgz", + "integrity": "sha512-DzwQWdY42sVYR6aUndw9FdCtl/i0oh3NkbkQpw+xq5aYQw5eiJn5kwnKaKQEWaoBe8Cso71X2i1EJGvi1jZ2xw==", + "deprecated": "Miniflare v2 is no longer supported. Please upgrade to Miniflare v4", + "dev": true, + "dependencies": { + "@miniflare/queues": "2.14.4", + "@miniflare/runner-vm": "2.14.4", + "@miniflare/shared": "2.14.4", + "@miniflare/shared-test-environment": "2.14.4", + "undici": "5.28.4" + }, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "vitest": ">=0.23.0" + } + }, + "node_modules/vitest-environment-miniflare/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20250917.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250917.0.tgz", + "integrity": "sha512-0D+wWaccyYQb2Zx2DZDC77YDn9kOpkpGMCgyKgIHilghut5hBQ/adUIEseS4iuIZxBPeFSn6zFtICP0SxZ3z0g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250917.0", + "@cloudflare/workerd-darwin-arm64": "1.20250917.0", + "@cloudflare/workerd-linux-64": "1.20250917.0", + "@cloudflare/workerd-linux-arm64": "1.20250917.0", + "@cloudflare/workerd-windows-64": "1.20250917.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 116f7d3..3ea9ee4 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,28 @@ { - "private": true, - "name": "optimizely-cloudflare-worker-template", - "version": "0.1.0", - "description": "A template for kick starting a Cloudflare Workers project with Optimizely", - "main": "src/index.js", - "scripts": { - "format": "prettier --write '**/*.{js,css,json,md}'" - }, - "author": "Zeeshan Ashraf ", - "license": "ISC", - "devDependencies": { - "prettier": "^1.18.2" - }, - "dependencies": { - "@optimizely/optimizely-sdk": "^4.9.1", - "cookie": "^0.4.2", - "uuid": "^8.3.2" - } + "name": "optimizely-cloudflare-worker-template", + "description": "A template for kick starting a Cloudflare Workers project with Optimizely", + "version": "1.0.0", + "license": "Apache-2.0", + "type": "module", + "main": "src/index.js", + "private": true, + "scripts": { + "format": "biome format --write", + "lint": "biome check --write", + "test": "vitest run", + "precommit": "npm run lint && npm run format && npm run test", + "dev": "miniflare src/index.js --wrangler --env development", + "start": "miniflare src/index.js --wrangler", + "build": "npx wrangler deploy --dry-run --outdir dist" + }, + "dependencies": { + "@optimizely/optimizely-sdk": "^6.1.0", + "cookie": "^1.0.2" + }, + "devDependencies": { + "@biomejs/biome": "^2.2.4", + "miniflare": "^4.20250917.0", + "vitest": "^3.2.4", + "vitest-environment-miniflare": "^2.14.4" + } } diff --git a/src/index.js b/src/index.js index 63cf253..4320f96 100644 --- a/src/index.js +++ b/src/index.js @@ -1,119 +1,159 @@ /** - * Copyright 2021-2022 Optimizely and contributors + * Copyright 2021-2022, 2025 Optimizely * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -import { v4 } from "uuid"; import cookie from "cookie"; -import { - createInstance, - enums as OptimizelyEnums -} from "@optimizely/optimizely-sdk/dist/optimizely.lite.min.js"; -import { - getDatafile, - dispatchEvent, - } from "./optimizely_helper"; +import { getOptimizelyClient } from "./optimizely_helper"; -const CLOUDFLARE_CLIENT_ENGINE = "javascript-sdk/cloudflare"; +/** + * Cookie name used to store the Optimizely user ID for consistent user experience + * across requests from the same browser session. + * @type {string} + */ const OPTIMIZELY_USER_ID_COOKIE_NAME = "optimizely_user_id"; -addEventListener("fetch", event => { - event.respondWith(handleRequest(event)); -}); - -async function handleRequest(event) { - const cookies = cookie.parse(event.request.headers.get("Cookie") || ''); - - // Fetch user Id from the cookie if available to make sure that a returning user from same browser session always sees the same variation. - const userId = cookies[OPTIMIZELY_USER_ID_COOKIE_NAME] || v4(); - - // fetch datafile from optimizely CDN and cache it with cloudflare for the given number of seconds - const datafile = await getDatafile("YOUR_SDK_KEY_HERE", 600); - - const optimizelyClient = createInstance({ - datafile, - - // keep the LOG_LEVEL to ERROR in production. Setting LOG_LEVEL to INFO or DEBUG can adversely impact performance. - logLevel: OptimizelyEnums.LOG_LEVEL.ERROR, - - clientEngine: CLOUDFLARE_CLIENT_ENGINE - - /*** - * Optional event dispatcher. Please uncomment the following line if you want to dispatch an impression event to optimizely logx backend. - * When enabled, an event is dispatched asynchronously. It does not impact the response time for a particular worker but it will - * add to the total compute time of the worker and can impact cloudflare billing. - */ - - /* eventDispatcher: { - dispatchEvent: optimizelyEvent => { - // Tell cloudflare to wait for this promise to fullfill. - event.waitUntil(dispatchEvent(optimizelyEvent)); - } - }, */ - - /* Add other Optimizely SDK initialization options here if needed */ - }); - - const optimizelyUserContext = optimizelyClient.createUserContext( - userId, - { - /* YOUR_OPTIONAL_ATTRIBUTES_HERE */ - } - ); - - // --- Using Optimizely Config - const optimizelyConfig = optimizelyClient.getOptimizelyConfig(); - - // --- For a single flag --- // - const decision = optimizelyUserContext.decide("YOUR_FLAG_HERE"); - if (decision.enabled) { - console.log( - `The Flag "${ - decision.flagKey - }" was Enabled for the user "${decision.userContext.getUserId()}"` - ); - } else { - console.log( - `The Flag "${ - decision.flagKey - }" was Not Enabled for the user "${decision.userContext.getUserId()}"` - ); - } - - // --- For all flags --- // - const allDecisions = optimizelyUserContext.decideAll(); - Object.entries(allDecisions).forEach(([flagKey, decision]) => { - if (decision.enabled) { - console.log( - `The Flag "${ - decision.flagKey - }" was Enabled for the user "${decision.userContext.getUserId()}"` - ); - } else { - console.log( - `The Flag "${ - decision.flagKey - }" was Not Enabled for the user "${decision.userContext.getUserId()}"` - ); - } - }); +/** + * Cloudflare Worker export interface. + * Handles incoming HTTP requests using the fetch handler pattern. + * @see https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/ + */ +export default { + /** + * Main fetch handler for the Cloudflare Worker. + * @param {Request} request - The incoming HTTP request + * @param {Object} env - Environment bindings (secrets, KV namespaces, etc.) + * @param {ExecutionContext} ctx - Execution context for managing async operations + * @returns {Promise} HTTP response + */ + async fetch(request, env, ctx) { + return handleRequest(request, env, ctx); + }, +}; - let headers = new Headers(); - headers.set("Content-Type", "text/plain"); - headers.set("Set-Cookie", cookie.serialize(OPTIMIZELY_USER_ID_COOKIE_NAME, userId)); - return new Response( - "Welcome to the Optimizely Starter template. Check logs for decision results.", - { headers }, - ); +/** + * Handle incoming HTTP requests and perform Optimizely feature flag decisions. + * + * This function demonstrates: + * - Retrieving or generating a user ID from cookies + * - Creating an Optimizely user context + * - Making single and batch flag decisions + * - Setting cookies for user persistence + * + * @param {Request} request - The incoming HTTP request + * @param {Object} env - Environment bindings containing OPTIMIZELY_SDK_KEY and optional configuration + * @param {ExecutionContext} ctx - Cloudflare Worker execution context for managing async operations + * @returns {Promise} HTTP response with decision results + */ +async function handleRequest(request, env, ctx) { + const cookies = cookie.parse(request.headers.get("Cookie") || ""); + + // Fetch user Id from the cookie if available to make sure that a returning user from + // same browser session always sees the same variation. + const userId = cookies[OPTIMIZELY_USER_ID_COOKIE_NAME] || crypto.randomUUID(); + + // Get the cached Optimizely client (refreshes datafile if needed) + let optimizelyClient; + try { + optimizelyClient = await getOptimizelyClient(env, ctx); + } catch (error) { + console.error("Failed to initialize Optimizely client, continuing without feature flags:", error); + // Continue without Optimizely - return normal response + const headers = new Headers(); + headers.set("Content-Type", "text/plain"); + headers.set( + "Set-Cookie", + cookie.serialize(OPTIMIZELY_USER_ID_COOKIE_NAME, userId), + ); + return new Response( + "Welcome to the Optimizely Starter template. Feature flags unavailable.", + { headers }, + ); + } + + let optimizelyUserContext; + try { + optimizelyUserContext = optimizelyClient.createUserContext(userId, { + // Add optional user attributes here as key-value pairs for example + // location: "New York City", + // device: "mobile" + }); + } catch (error) { + console.error("Failed to create Optimizely user context, continuing without feature flags:", error); + // Continue without Optimizely + const headers = new Headers(); + headers.set("Content-Type", "text/plain"); + headers.set( + "Set-Cookie", + cookie.serialize(OPTIMIZELY_USER_ID_COOKIE_NAME, userId), + ); + return new Response( + "Welcome to the Optimizely Starter template. Feature flags unavailable.", + { headers }, + ); + } + + // Decide for a single flag + try { + const decision = optimizelyUserContext.decide("YOUR_FLAG_HERE"); + if (decision.enabled) { + console.info( + `The Flag "${ + decision.flagKey + }" was Enabled for the user "${decision.userContext.getUserId()}"`, + ); + } else { + console.info( + `The Flag "${ + decision.flagKey + }" was Not Enabled for the user "${decision.userContext.getUserId()}"`, + ); + } + } catch (error) { + console.error("Failed to decide for single flag, continuing:", error); + } + + // Decide for all flags + try { + const allDecisions = optimizelyUserContext.decideAll(); + Object.entries(allDecisions).forEach(([_flagKey, decision]) => { + if (decision.enabled) { + console.info( + `The Flag "${ + decision.flagKey + }" was Enabled for the user "${decision.userContext.getUserId()}"`, + ); + } else { + console.info( + `The Flag "${ + decision.flagKey + }" was Not Enabled for the user "${decision.userContext.getUserId()}"`, + ); + } + }); + } catch (error) { + console.error("Failed to decide for all flags, continuing:", error); + } + + const headers = new Headers(); + headers.set("Content-Type", "text/plain"); + headers.set( + "Set-Cookie", + cookie.serialize(OPTIMIZELY_USER_ID_COOKIE_NAME, userId), + ); + return new Response( + "Welcome to the Optimizely Starter template. Check logs for decision results.", + { headers }, + ); } diff --git a/src/optimizely_helper.js b/src/optimizely_helper.js index 7364c22..d8d189b 100644 --- a/src/optimizely_helper.js +++ b/src/optimizely_helper.js @@ -1,32 +1,187 @@ /** - * Copyright 2021-2022 Optimizely and contributors + * Copyright 2021-2022, 2025 Optimizely * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -export async function getDatafile(sdkKey, ttl) { - const datafileResponse = await fetch( - `https://cdn.optimizely.com/datafiles/${sdkKey}.json`, - { cf: { cacheTtl: ttl } } - ); - return await datafileResponse.text(); +import { + createEventDispatcher, + createForwardingEventProcessor, + createInstance, + createStaticProjectConfigManager, +} from "@optimizely/optimizely-sdk/universal"; +import { CloudflareRequestHandler } from "./request_handler"; + +/** + * Client engine identifier for Optimizely SDK telemetry. + * @type {string} + */ +const CLOUDFLARE_CLIENT_ENGINE = "javascript-sdk/cloudflare"; + +/** + * Default cache TTL for the Optimizely datafile in seconds. + * Can be overridden via the OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS environment variable. + * @type {number} + * @example + * // Set in wrangler.jsonc: + * // "OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS": "600" // 10 minutes + */ +const DEFAULT_DATAFILE_CACHE_TTL_SECONDS = 300; // 5 minutes + +/** + * Module-scope cache for the Optimizely datafile to avoid redundant CDN requests. + * @type {string|null} + */ +let cachedDatafile = null; + +/** + * Timestamp (milliseconds since epoch) of the last successful datafile fetch. + * @type {number} + */ +let lastDatafileUpdate = 0; + +/** + * Get the datafile cache TTL in seconds from environment variables. + * + * Reads OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS and returns it as an integer. + * Falls back to DEFAULT_DATAFILE_CACHE_TTL_SECONDS if not set or invalid. + * + * @param {Object} env - Environment variables object + * @returns {number} TTL in seconds + */ +function getDatafileCacheTTL(env) { + const envValue = env?.OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS; + + if (envValue === undefined || envValue === null) { + return DEFAULT_DATAFILE_CACHE_TTL_SECONDS; + } + + const parsedValue = Number.parseInt(envValue, 10); + if (Number.isNaN(parsedValue) || parsedValue < 0) { + console.warn( + `Invalid OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS value: "${envValue}". ` + + `Using default: ${DEFAULT_DATAFILE_CACHE_TTL_SECONDS} seconds.`, + ); + return DEFAULT_DATAFILE_CACHE_TTL_SECONDS; + } + + return parsedValue; +} + +/** + * Fetch the Optimizely datafile from the CDN. + * + * The datafile contains all project configuration including flags, experiments, + * audiences, and variations. This function makes a direct HTTP request to the + * Optimizely CDN without using the execution context. + * + * @param {string} sdkKey - The Optimizely SDK key for the project + * @returns {Promise} The datafile JSON as a string + * @throws {Error} If the HTTP request fails or returns an error status + * @see https://docs.developers.optimizely.com/feature-experimentation/docs/get-the-datafile + */ +export async function getDatafile(sdkKey) { + // Datafile fetching doesn't need context since it's not dispatching events + const requestHandler = new CloudflareRequestHandler(); + const url = `https://cdn.optimizely.com/datafiles/${sdkKey}.json`; + const { responsePromise } = requestHandler.makeRequest(url, {}, "GET"); + const response = await responsePromise; + return response.body; } -export function dispatchEvent({ url, params }) { - const eventRequest = new Request(url, { - method: "POST", - body: JSON.stringify(params) - }); +/** + * Get or create an Optimizely client instance with cached datafile management. + * + * This function implements smart caching: + * - Caches the datafile in module scope to avoid redundant CDN requests + * - Refreshes the datafile when the TTL expires + * - Falls back to stale datafile if refresh fails (resilience pattern) + * - Creates a new client instance per request with request-specific context + * + * The client is configured for Cloudflare Workers with: + * - Static project config manager (no polling) + * - Forwarding event processor for immediate event dispatch + * - Disposable mode for edge environment optimization + * + * @param {Object} env - Environment bindings containing OPTIMIZELY_SDK_KEY and optional cache configuration + * @param {string} env.OPTIMIZELY_SDK_KEY - Required: The Optimizely project SDK key + * @param {string} [env.OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS] - Optional: Cache TTL in seconds (default: 300) + * @param {ExecutionContext} ctx - Cloudflare Worker execution context for managing async event dispatch + * @returns {Promise} Configured Optimizely client instance + * @throws {Error} If OPTIMIZELY_SDK_KEY is missing or initial datafile fetch fails + * @see https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-the-javascript-sdk + */ +export async function getOptimizelyClient(env, ctx) { + const now = Date.now(); + + const sdkKey = env.OPTIMIZELY_SDK_KEY; + if (!sdkKey) { + throw new Error( + "OPTIMIZELY_SDK_KEY environment variable is required. " + + "Set it in wrangler.jsonc or use: wrangler secret put OPTIMIZELY_SDK_KEY", + ); + } + + // Get cache TTL from environment (defaults to 5 minutes) + const cacheTTLSeconds = getDatafileCacheTTL(env); + + // Check if we need to refresh the cached datafile + const isDatafileStale = now - lastDatafileUpdate > cacheTTLSeconds * 1000; + if (!cachedDatafile || isDatafileStale) { + try { + cachedDatafile = await getDatafile(sdkKey); + lastDatafileUpdate = now; + } catch (error) { + // If fetch fails and we have a cached datafile, continue with stale data + if (cachedDatafile) { + console.error( + "Failed to fetch fresh datafile, using cached version:", + error, + ); + } else { + // No cached datafile available - this is a critical error + console.error( + "Failed to fetch datafile and no cached version available:", + error, + ); + throw new Error( + `Unable to initialize Optimizely: Failed to fetch datafile for SDK key ${sdkKey}. ${error.message}`, + ); + } + } + } + + // Create a new client instance for each request with the request-specific context + // Use the same request handler instance for both event dispatching and any SDK requests + const contextualRequestHandler = new CloudflareRequestHandler(ctx); + + const projectConfigManager = createStaticProjectConfigManager({ + datafile: cachedDatafile, + }); + + const eventDispatcher = createEventDispatcher(contextualRequestHandler); + const eventProcessor = createForwardingEventProcessor({ + eventDispatcher, + }); + + // https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-the-javascript-sdk + const optimizelyClient = createInstance({ + projectConfigManager, + eventProcessor, + requestHandler: contextualRequestHandler, + clientEngine: CLOUDFLARE_CLIENT_ENGINE, + disposable: true, // Enable auto-disposal for edge environment + }); - return fetch(eventRequest); + return optimizelyClient; } diff --git a/src/request_handler.js b/src/request_handler.js new file mode 100644 index 0000000..68e5e08 --- /dev/null +++ b/src/request_handler.js @@ -0,0 +1,104 @@ +/** + * Copyright 2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * CloudflareRequestHandler implements a request handler using the Fetch API + * available in Cloudflare Workers. It supports making HTTP requests with + * customizable methods, headers, and bodies, and handles response parsing + * based on content type. + */ +export class CloudflareRequestHandler { + /** + * Create a new CloudflareRequestHandler instance. + * @param {Object} ctx - Optional Cloudflare Worker execution context with waitUntil method + */ + constructor(ctx) { + this.ctx = ctx; + } + + /** + * Make a fetch request inside Cloudflare Workers. + * @param {string} requestUrl - The request URL + * @param {Record} headers - Plain object of headers + * @param {string} method - HTTP method (will be normalized to uppercase) + * @param {any} data - Request body. Objects will be JSON.stringified unless already FormData/ArrayBuffer/Blob. + * @returns {{responsePromise: Promise<{statusCode:number,body:string,headers:Object}>, abort: ()=>void}} + */ + makeRequest(requestUrl, headers, method, data) { + const controller = new AbortController(); + method = (method || "GET").toUpperCase(); + headers = new Headers(headers || {}); + + const requestOptions = { + method, + headers, + signal: controller.signal, + }; + + if (data) { + requestOptions.body = data; + } + + const responsePromise = fetch(requestUrl, requestOptions) + .then(async (response) => { + const contentType = response.headers.get("content-type") || ""; + let body; + try { + if (contentType.includes("application/json")) { + body = await response.json(); + } else { + body = await response.text(); + } + } catch { + // If parsing fails, fallback to text + try { + body = await response.text(); + } catch { + body = ""; + } + } + + return { + statusCode: response.status, + body: typeof body === "string" ? body : JSON.stringify(body), + headers: + response.headers && typeof response.headers.entries === "function" + ? Object.fromEntries(response.headers.entries()) + : {}, + }; + }) + .catch((error) => { + if (error && error.name === "AbortError") { + // Preserve original error as cause where supported is not available in Workers, so set a message + const abortError = new Error("Request aborted"); + abortError.name = "AbortError"; + throw abortError; + } + throw error; + }); + + if (this.ctx && typeof this.ctx.waitUntil === "function") { + this.ctx.waitUntil(responsePromise); + } + + return { + responsePromise, + abort: () => { + controller.abort(); + }, + }; + } +} diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..6f3a242 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,297 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import workerExport from "../src/index.js"; + +// Mock the optimizely_helper module +vi.mock("../src/optimizely_helper.js", () => ({ + getOptimizelyClient: vi.fn(), +})); + +// Mock cookie module +vi.mock("cookie", () => ({ + default: { + parse: vi.fn(), + serialize: vi.fn(), + }, +})); + +describe("index.js - Cloudflare Worker", () => { + let mockOptimizelyClient; + let mockUserContext; + let mockDecision; + let getOptimizelyClient; + let cookie; + + beforeEach(async () => { + vi.clearAllMocks(); + + // Import mocked modules + const optimizelyHelper = await import("../src/optimizely_helper.js"); + const cookieModule = await import("cookie"); + + getOptimizelyClient = optimizelyHelper.getOptimizelyClient; + cookie = cookieModule.default; + + // Set up mock decision + mockDecision = { + enabled: true, + flagKey: "test-flag", + userContext: { + getUserId: vi.fn(() => "test-user-123"), + }, + }; + + // Set up mock user context + mockUserContext = { + decide: vi.fn(() => mockDecision), + decideAll: vi.fn(() => ({ + "test-flag": mockDecision, + "another-flag": { + enabled: false, + flagKey: "another-flag", + userContext: { + getUserId: vi.fn(() => "test-user-123"), + }, + }, + })), + }; + + // Set up mock Optimizely client + mockOptimizelyClient = { + createUserContext: vi.fn(() => mockUserContext), + }; + + getOptimizelyClient.mockResolvedValue(mockOptimizelyClient); + cookie.serialize.mockReturnValue( + "optimizely_user_id=test-user-123; Path=/", + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("default export", () => { + it("should have fetch method that delegates to handleRequest", async () => { + expect(workerExport).toHaveProperty("fetch"); + expect(typeof workerExport.fetch).toBe("function"); + + const mockRequest = new Request("https://example.com"); + const mockEnv = { OPTIMIZELY_SDK_KEY: "test-key" }; + const mockCtx = {}; + + cookie.parse.mockReturnValue({}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(response).toBeInstanceOf(Response); + expect(getOptimizelyClient).toHaveBeenCalledWith(mockEnv, mockCtx); + }); + }); + + describe("handleRequest", () => { + let mockRequest; + let mockEnv; + let mockCtx; + + beforeEach(() => { + mockEnv = { OPTIMIZELY_SDK_KEY: "test-key" }; + mockCtx = {}; + }); + + it("should handle request with no cookies and generate new user ID", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(cookie.parse).toHaveBeenCalledWith(""); + expect(mockOptimizelyClient.createUserContext).toHaveBeenCalledWith( + "test-uuid-123", + {}, + ); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("text/plain"); + }); + + it("should handle request with existing user ID cookie", async () => { + mockRequest = new Request("https://example.com", { + headers: { Cookie: "optimizely_user_id=existing-user-456" }, + }); + cookie.parse.mockReturnValue({ optimizely_user_id: "existing-user-456" }); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(cookie.parse).toHaveBeenCalledWith( + "optimizely_user_id=existing-user-456", + ); + expect(mockOptimizelyClient.createUserContext).toHaveBeenCalledWith( + "existing-user-456", + {}, + ); + expect(response.status).toBe(200); + }); + + it("should handle multiple cookies correctly", async () => { + mockRequest = new Request("https://example.com", { + headers: { + Cookie: + "other_cookie=value; optimizely_user_id=user-789; another=test", + }, + }); + cookie.parse.mockReturnValue({ + other_cookie: "value", + optimizely_user_id: "user-789", + another: "test", + }); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(mockOptimizelyClient.createUserContext).toHaveBeenCalledWith( + "user-789", + {}, + ); + expect(response.status).toBe(200); + }); + + it("should call decide for single flag and log results", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + + await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(mockUserContext.decide).toHaveBeenCalledWith("YOUR_FLAG_HERE"); + expect(global.console.info).toHaveBeenCalledWith( + 'The Flag "test-flag" was Enabled for the user "test-user-123"', + ); + }); + + it("should handle disabled flag decision", async () => { + mockDecision.enabled = false; + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + + await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(global.console.info).toHaveBeenCalledWith( + 'The Flag "test-flag" was Not Enabled for the user "test-user-123"', + ); + }); + + it("should call decideAll and log all flag results", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + + await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(mockUserContext.decideAll).toHaveBeenCalled(); + expect(global.console.info).toHaveBeenCalledWith( + 'The Flag "test-flag" was Enabled for the user "test-user-123"', + ); + expect(global.console.info).toHaveBeenCalledWith( + 'The Flag "another-flag" was Not Enabled for the user "test-user-123"', + ); + }); + + it("should set response headers correctly", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(response.headers.get("Content-Type")).toBe("text/plain"); + expect(cookie.serialize).toHaveBeenCalledWith( + "optimizely_user_id", + "test-uuid-123", + ); + expect(response.headers.get("Set-Cookie")).toBe( + "optimizely_user_id=test-user-123; Path=/", + ); + }); + + it("should return correct response body", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + const responseText = await response.text(); + + expect(responseText).toBe( + "Welcome to the Optimizely Starter template. Check logs for decision results.", + ); + }); + + it("should handle Optimizely client errors gracefully", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + getOptimizelyClient.mockRejectedValue(new Error("SDK Key missing")); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("text/plain"); + const text = await response.text(); + expect(text).toContain("Feature flags unavailable"); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to initialize Optimizely client, continuing without feature flags:", + expect.any(Error), + ); + + consoleErrorSpy.mockRestore(); + }); + + it("should handle decision errors gracefully", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + mockUserContext.decide.mockImplementation(() => { + throw new Error("Decision error"); + }); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + expect(response.status).toBe(200); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to decide for single flag, continuing:", + expect.any(Error), + ); + + consoleErrorSpy.mockRestore(); + }); + + it("should handle decideAll errors gracefully", async () => { + mockRequest = new Request("https://example.com"); + cookie.parse.mockReturnValue({}); + mockUserContext.decideAll.mockImplementation(() => { + throw new Error("DecideAll error"); + }); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const response = await workerExport.fetch(mockRequest, mockEnv, mockCtx); + expect(response.status).toBe(200); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to decide for all flags, continuing:", + expect.any(Error), + ); + + consoleErrorSpy.mockRestore(); + }); it("should preserve user ID when setting cookie for existing user", async () => { + mockRequest = new Request("https://example.com", { + headers: { Cookie: "optimizely_user_id=preserved-user-id" }, + }); + cookie.parse.mockReturnValue({ optimizely_user_id: "preserved-user-id" }); + + await workerExport.fetch(mockRequest, mockEnv, mockCtx); + + expect(cookie.serialize).toHaveBeenCalledWith( + "optimizely_user_id", + "preserved-user-id", + ); + expect(mockOptimizelyClient.createUserContext).toHaveBeenCalledWith( + "preserved-user-id", + {}, + ); + }); + }); +}); diff --git a/test/optimizely_helper.test.js b/test/optimizely_helper.test.js new file mode 100644 index 0000000..c54dc7e --- /dev/null +++ b/test/optimizely_helper.test.js @@ -0,0 +1,206 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@optimizely/optimizely-sdk/universal", () => ({ + createInstance: vi.fn(), + createStaticProjectConfigManager: vi.fn(), + createForwardingEventProcessor: vi.fn(), + createEventDispatcher: vi.fn(() => ({})), + LogLevel: { + Error: "ERROR", + }, +})); + +let getDatafile; +let getOptimizelyClient; + +describe("Optimizely Helper", () => { + beforeEach(async () => { + vi.clearAllMocks(); + vi.resetModules(); + + const mod = await import("../src/optimizely_helper.js"); + getDatafile = mod.getDatafile; + getOptimizelyClient = mod.getOptimizelyClient; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("getDatafile", () => { + it("should fetch datafile from Optimizely CDN with correct URL and TTL", async () => { + const mockResponse = { + status: 200, + ok: true, + headers: { + get: () => "application/json", + entries: () => [["content-type", "application/json"]], + }, + json: vi.fn().mockResolvedValue('{"version": "4", "experiments": []}'), + }; + global.fetch = vi.fn().mockResolvedValue(mockResponse); + + const sdkKey = "test-sdk-key"; + const ttl = 600; + + const result = await getDatafile(sdkKey, ttl); + + expect(global.fetch).toHaveBeenCalledWith( + `https://cdn.optimizely.com/datafiles/${sdkKey}.json`, + expect.objectContaining({ + method: "GET", + headers: expect.any(Headers), + signal: expect.any(AbortSignal), + }), + ); + expect(result).toBe('{"version": "4", "experiments": []}'); + }); + + it("should handle fetch errors", async () => { + global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + const sdkKey = "test-sdk-key"; + const ttl = 600; + + await expect(getDatafile(sdkKey, ttl)).rejects.toThrow("Network error"); + }); + }); + + describe("getOptimizelyClient", () => { + let mockCreateInstance; + let mockCreateStaticProjectConfigManager; + let mockCreateForwardingEventProcessor; + let mockClient; + let mockProjectConfigManager; + let mockEventProcessor; + + beforeEach(async () => { + const { + createInstance, + createStaticProjectConfigManager, + createForwardingEventProcessor, + } = await import("@optimizely/optimizely-sdk/universal"); + + mockCreateInstance = createInstance; + mockCreateStaticProjectConfigManager = createStaticProjectConfigManager; + mockCreateForwardingEventProcessor = createForwardingEventProcessor; + + mockClient = { + setDatafile: vi.fn(), + }; + mockProjectConfigManager = {}; + mockEventProcessor = {}; + + mockCreateInstance.mockReturnValue(mockClient); + mockCreateStaticProjectConfigManager.mockReturnValue( + mockProjectConfigManager, + ); + mockCreateForwardingEventProcessor.mockReturnValue(mockEventProcessor); + + // Mock getDatafile by mocking the CloudflareRequestHandler response + global.fetch = vi.fn().mockResolvedValue({ + status: 200, + ok: true, + headers: { + get: () => "application/json", + entries: () => [["content-type", "application/json"]], + }, + json: vi.fn().mockResolvedValue('{"version": "4"}'), + }); + }); + + it("should throw error when SDK key is missing", async () => { + const env = {}; + const ctx = {}; + + await expect(getOptimizelyClient(env, ctx)).rejects.toThrow( + "OPTIMIZELY_SDK_KEY environment variable is required", + ); + }); + + it("should create new client when called for the first time", async () => { + const env = { OPTIMIZELY_SDK_KEY: "test-key" }; + const ctx = {}; + + const client = await getOptimizelyClient(env, ctx); + + expect(global.fetch).toHaveBeenCalledWith( + "https://cdn.optimizely.com/datafiles/test-key.json", + expect.objectContaining({ + method: "GET", + headers: expect.any(Headers), + signal: expect.any(AbortSignal), + }), + ); + expect(mockCreateStaticProjectConfigManager).toHaveBeenCalledWith({ + datafile: '{"version": "4"}', + }); + expect(mockCreateForwardingEventProcessor).toHaveBeenCalledWith({ + eventDispatcher: expect.any(Object), + }); + expect(mockCreateInstance).toHaveBeenCalledWith({ + projectConfigManager: mockProjectConfigManager, + eventProcessor: mockEventProcessor, + requestHandler: expect.any(Object), + clientEngine: "javascript-sdk/cloudflare", + disposable: true, + }); + expect(client).toBe(mockClient); + }); + + it("should return cached client when called within TTL", async () => { + const env = { OPTIMIZELY_SDK_KEY: "test-key" }; + const ctx = {}; + + // First call + const client1 = await getOptimizelyClient(env, ctx); + + // Clear fetch mock to ensure it's not called again + vi.clearAllMocks(); + + // Second call within TTL + const client2 = await getOptimizelyClient(env, ctx); + + expect(global.fetch).not.toHaveBeenCalled(); + expect(client1).toBe(client2); + }); + + it("should refresh datafile when cache TTL expires", async () => { + const env = { OPTIMIZELY_SDK_KEY: "test-key" }; + const ctx = {}; + + // Mock Date.now to simulate time progression + const originalDateNow = Date.now; + let currentTime = 1000000000000; // Start time + Date.now = vi.fn(() => currentTime); + + try { + // First call - should create client + await getOptimizelyClient(env, ctx); + + // Advance time beyond TTL (300 seconds = 300,000 ms) + currentTime += 301000; + + // Mock new datafile response + global.fetch = vi.fn().mockResolvedValue({ + status: 200, + ok: true, + headers: { + get: () => "application/json", + entries: () => [["content-type", "application/json"]], + }, + json: vi.fn().mockResolvedValue('{"version": "5"}'), + }); + + // Second call after TTL expires - should create new client + const client2 = await getOptimizelyClient(env, ctx); + + expect(client2).toBe(mockClient); + expect(mockCreateStaticProjectConfigManager).toHaveBeenCalledTimes(2); + expect(mockCreateInstance).toHaveBeenCalledTimes(2); + } finally { + Date.now = originalDateNow; + } + }); + }); +}); diff --git a/test/request_handler.test.js b/test/request_handler.test.js new file mode 100644 index 0000000..a7aac61 --- /dev/null +++ b/test/request_handler.test.js @@ -0,0 +1,130 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { CloudflareRequestHandler } from "../src/request_handler"; + +describe("CloudflareRequestHandler", () => { + let originalFetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it("parses JSON response when content-type is application/json", async () => { + const mockResponse = { + status: 200, + ok: true, + headers: { + get: (k) => + k.toLowerCase() === "content-type" + ? "application/json; charset=utf-8" + : undefined, + entries: () => [["content-type", "application/json; charset=utf-8"]], + }, + json: async () => ({ hello: "world" }), + text: async () => JSON.stringify({ hello: "world" }), + }; + + global.fetch = vi.fn(() => Promise.resolve(mockResponse)); + + const rh = new CloudflareRequestHandler(); + const { responsePromise } = rh.makeRequest( + "https://example.test/api", + { Accept: "application/json" }, + "GET", + ); + + const res = await responsePromise; + expect(res.statusCode).toBe(200); + expect(res.body).toBe(JSON.stringify({ hello: "world" })); + expect(res.headers["content-type"]).toContain("application/json"); + }); + + it("parses text response when content-type is text/plain", async () => { + const mockResponse = { + status: 201, + ok: true, + headers: { + get: (k) => + k.toLowerCase() === "content-type" + ? "text/plain; charset=utf-8" + : undefined, + entries: () => [["content-type", "text/plain; charset=utf-8"]], + }, + json: async () => { + throw new Error("not json"); + }, + text: async () => "plain text body", + }; + + global.fetch = vi.fn(() => Promise.resolve(mockResponse)); + + const rh = new CloudflareRequestHandler(); + const { responsePromise } = rh.makeRequest( + "https://example.test/text", + {}, + "GET", + ); + + const res = await responsePromise; + expect(res.statusCode).toBe(201); + expect(res.body).toBe("plain text body"); + }); + + it("returns an aborted error when request is aborted", async () => { + // Create a fetch that returns a promise that rejects with an AbortError + const abortErr = new Error("The user aborted a request."); + abortErr.name = "AbortError"; + global.fetch = vi.fn(() => Promise.reject(abortErr)); + + const rh = new CloudflareRequestHandler(); + const { responsePromise, abort } = rh.makeRequest( + "https://example.test/abort", + {}, + "GET", + ); + + // call abort which will cause the fetch to reject with AbortError in our mock + abort(); + + await expect(responsePromise).rejects.toMatchObject({ name: "AbortError" }); + }); + + it("sends string body for POST and sets Content-Type when string provided", async () => { + let receivedInit; + global.fetch = vi.fn((url, init) => { + // reference url to avoid unused-parameter linter errors + void url; + receivedInit = init; + return Promise.resolve({ + status: 200, + ok: true, + headers: { get: () => "", entries: () => [] }, + json: async () => ({}), + text: async () => "", + }); + }); + + const rh = new CloudflareRequestHandler(); + const { responsePromise } = rh.makeRequest( + "https://example.test/post", + { "X-Test": "1" }, + "POST", + "raw body", + ); + + await responsePromise; + expect(receivedInit).toBeDefined(); + expect(receivedInit.method).toBe("POST"); + expect(receivedInit.body).toBe("raw body"); + // headers may be a Headers instance; check for Content-Type presence + const contentType = + receivedInit.headers.get("Content-Type") || + receivedInit.headers.get("content-type"); + // The implementation only sets Content-Type when body is JSON; since we passed string it may be null or undefined + expect(contentType === null || contentType === undefined).toBe(true); + }); +}); diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..fdc83e0 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,90 @@ +// Test setup file +import { vi } from "vitest"; + +// Mock fetch globally +global.fetch = vi.fn(); + +// Mock crypto.randomUUID for Node.js environment +if (!global.crypto) { + global.crypto = {}; +} +global.crypto.randomUUID = vi.fn(() => "test-uuid-123"); + +// Mock console methods +global.console = { + ...console, + info: vi.fn(), + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), +}; + +// Mock Request and Response for Node.js environment +if (typeof global.Request === "undefined") { + global.Request = class MockRequest { + constructor(url, options = {}) { + this.url = url; + this.method = options.method || "GET"; + this.headers = new Map(); + + if (options.headers) { + if (options.headers instanceof Map) { + this.headers = new Map(options.headers); + } else if (typeof options.headers === "object") { + Object.entries(options.headers).forEach(([key, value]) => { + this.headers.set(key.toLowerCase(), value); + }); + } + } + } + }; +} + +if (typeof global.Response === "undefined") { + global.Response = class MockResponse { + constructor(body, options = {}) { + this.body = body; + this.status = options.status || 200; + this.statusText = options.statusText || "OK"; + this.headers = new Map(); + + if (options.headers) { + if (options.headers instanceof Map) { + this.headers = new Map(options.headers); + } else if (typeof options.headers === "object") { + Object.entries(options.headers).forEach(([key, value]) => { + this.headers.set(key.toLowerCase(), value); + }); + } + } + } + + async text() { + return this.body; + } + + async json() { + return JSON.parse(this.body); + } + }; +} + +if (typeof global.Headers === "undefined") { + global.Headers = class MockHeaders extends Map { + get(name) { + return super.get(name.toLowerCase()); + } + + set(name, value) { + return super.set(name.toLowerCase(), value); + } + + has(name) { + return super.has(name.toLowerCase()); + } + + delete(name) { + return super.delete(name.toLowerCase()); + } + }; +} diff --git a/test/test-utils.js b/test/test-utils.js new file mode 100644 index 0000000..34dba1c --- /dev/null +++ b/test/test-utils.js @@ -0,0 +1,212 @@ +import { vi } from "vitest"; + +/** + * Creates a mock Request object for testing + * @param {string} url - The request URL + * @param {Object} options - Request options + * @returns {Request} Mock request object + */ +export function createMockRequest(url = "https://example.com", options = {}) { + return new Request(url, { + method: "GET", + ...options, + }); +} + +/** + * Creates a mock environment object for Cloudflare Workers + * @param {Object} overrides - Environment variable overrides + * @returns {Object} Mock environment object + */ +export function createMockEnv(overrides = {}) { + return { + OPTIMIZELY_SDK_KEY: "test-sdk-key", + ...overrides, + }; +} + +/** + * Creates a mock execution context for Cloudflare Workers + * @param {Object} overrides - Context overrides + * @returns {Object} Mock context object + */ +export function createMockCtx(overrides = {}) { + return { + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + ...overrides, + }; +} + +/** + * Creates a mock Optimizely client with configurable behavior + * @param {Object} options - Configuration options + * @returns {Object} Mock Optimizely client + */ +export function createMockOptimizelyClient(options = {}) { + const mockDecision = { + enabled: options.enabled ?? true, + flagKey: options.flagKey ?? "test-flag", + userContext: { + getUserId: vi.fn(() => options.userId ?? "test-user-123"), + }, + variationKey: options.variationKey ?? "control", + variables: options.variables ?? {}, + ruleKey: options.ruleKey ?? null, + reasons: options.reasons ?? [], + }; + + const mockUserContext = { + decide: vi.fn(() => mockDecision), + decideAll: vi.fn(() => ({ + [mockDecision.flagKey]: mockDecision, + })), + trackEvent: vi.fn(), + setForcedDecision: vi.fn(), + getForcedDecision: vi.fn(), + removeForcedDecision: vi.fn(), + removeAllForcedDecisions: vi.fn(), + getUserId: vi.fn(() => options.userId ?? "test-user-123"), + getAttributes: vi.fn(() => options.attributes ?? {}), + setAttributes: vi.fn(), + }; + + return { + createUserContext: vi.fn(() => mockUserContext), + setDatafile: vi.fn(), + getDatafile: vi.fn(() => '{"version": "4"}'), + isValid: vi.fn(() => true), + close: vi.fn(), + activate: vi.fn(), + getEnabledFeatures: vi.fn(() => []), + getFeatureVariable: vi.fn(), + getFeatureVariableBoolean: vi.fn(), + getFeatureVariableDouble: vi.fn(), + getFeatureVariableInteger: vi.fn(), + getFeatureVariableString: vi.fn(), + getAllFeatureVariables: vi.fn(() => ({})), + isFeatureEnabled: vi.fn(() => options.enabled ?? true), + track: vi.fn(), + // Expose mocks for testing + _mockUserContext: mockUserContext, + _mockDecision: mockDecision, + }; +} + +/** + * Creates a mock Response object with configurable properties + * @param {string} body - Response body + * @param {Object} options - Response options + * @returns {Response} Mock response object + */ +export function createMockResponse(body = "OK", options = {}) { + return new Response(body, { + status: 200, + statusText: "OK", + headers: new Headers(), + ...options, + }); +} + +/** + * Creates a mock fetch function that returns predefined responses + * @param {Array} responses - Array of responses to return in sequence + * @returns {Function} Mock fetch function + */ +export function createMockFetch(responses = []) { + const mockFetch = vi.fn(); + + responses.forEach((response) => { + mockFetch.mockResolvedValueOnce(response); + }); + + // Default response for any additional calls + if (responses.length === 0) { + mockFetch.mockResolvedValue(createMockResponse()); + } + + return mockFetch; +} + +/** + * Creates a mock cookie object with parse and serialize methods + * @param {Object} parsedCookies - Object to return from parse + * @param {string} serializedCookie - String to return from serialize + * @returns {Object} Mock cookie object + */ +export function createMockCookie( + parsedCookies = {}, + serializedCookie = "test=cookie", +) { + return { + parse: vi.fn(() => parsedCookies), + serialize: vi.fn(() => serializedCookie), + }; +} + +/** + * Sets up common test environment with mocked globals + */ +export function setupTestEnvironment() { + // Mock crypto.randomUUID + global.crypto = { + randomUUID: vi.fn(() => "test-uuid-123"), + }; + + // Mock console methods + global.console = { + ...console, + info: vi.fn(), + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + }; + + // Mock Date.now for time-based tests + const originalDateNow = Date.now; + const mockDateNow = vi.fn(() => 1000000000000); + Date.now = mockDateNow; + + return { + restoreGlobals: () => { + Date.now = originalDateNow; + }, + setCurrentTime: (time) => { + mockDateNow.mockReturnValue(time); + }, + }; +} + +/** + * Assertion helpers for testing responses + */ +export const responseAssertions = { + /** + * Asserts that a response has the expected properties + * @param {Response} response - Response to test + * @param {Object} expected - Expected properties + */ + assertResponse(response, expected = {}) { + if (expected.status) { + expect(response.status).toBe(expected.status); + } + if (expected.statusText) { + expect(response.statusText).toBe(expected.statusText); + } + if (expected.headers) { + Object.entries(expected.headers).forEach(([key, value]) => { + expect(response.headers.get(key)).toBe(value); + }); + } + }, + + /** + * Asserts that a response body matches expected text + * @param {Response} response - Response to test + * @param {string} expectedText - Expected body text + */ + async assertResponseText(response, expectedText) { + const text = await response.text(); + expect(text).toBe(expectedText); + }, +}; diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..adb8da5 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "miniflare", + environmentOptions: { + modules: true, + }, + threads: false, + setupFiles: ["./test/setup.js"], + }, +}); diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 41e77b7..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - target: "webworker", - entry: "./src/index.js" -}; diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..9e43614 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,16 @@ +{ + "name": "optimizely-cloudflare-template", + "main": "src/index.js", + "compatibility_date": "2025-09-18", + "compatibility_flags": ["nodejs_compat"], + "workers_dev": true, + "minify": true, + "upload_source_maps": true, + "account_id": "YOUR_ACCOUNT_ID", + "vars": { + // Replace with your Optimizely SDK Key or use Cloudflare Environment Variables + "OPTIMIZELY_SDK_KEY": "YOUR_SDK_KEY", + // Optional: Set the datafile cache TTL in seconds (default is 300 seconds = 5 minutes) + "OPTIMIZELY_DATAFILE_CACHE_TTL_SECONDS": "300" // 5 minutes + } +} diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index 3ca23d9..0000000 --- a/wrangler.toml +++ /dev/null @@ -1,8 +0,0 @@ -name = "optimizely-cloudflare-template" -type = "webpack" - -account_id = "" -workers_dev = true -route = "" -zone_id = "" -webpack_config = "webpack.config.js"