diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b9c57c..81ed1b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,64 +1,83 @@ -name: Package Release - -on: - push: - branches: - - latest-release - - beta - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - release-builder: - runs-on: ubuntu-latest - defaults: - run: - working-directory: packages/builder - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Important for semantic release to work correctly - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - registry-url: "https://registry.npmjs.org" - - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - version: "10.8.1" - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - name: Setup pnpm cache - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build package - run: pnpm build - - - name: Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: pnpm semantic-release - - # Add more jobs for additional packages as repo grows +name: Package Release + +on: + push: + branches: + - latest-release + - beta + + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write # Required for npm OIDC trusted publishing + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + release-builder: + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/builder + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Important for semantic release to work correctly + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: "10.8.1" + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: . + + - name: Build package + run: pnpm build + working-directory: . + + - name: Semantic Release + id: semantic + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: pnpm semantic-release + + - name: Publish to npm with OIDC + if: steps.semantic.outcome == 'success' + run: | + # Check if package.json version was updated (not 0.0.0-development) + VERSION=$(node -p "require('./package.json').version") + if [ "$VERSION" != "0.0.0-development" ]; then + echo "Publishing version $VERSION to npm with OIDC provenance..." + npm publish --provenance --access public + else + echo "No new version to publish" + fi + diff --git a/.gitignore b/.gitignore index 6f0a37f..635dc67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules dist coverage +sites *.tsbuildinfo diff --git a/.vscode/pushforge.code-workspace b/.vscode/pushforge.code-workspace index 6654f34..4f22de5 100644 --- a/.vscode/pushforge.code-workspace +++ b/.vscode/pushforge.code-workspace @@ -3,6 +3,9 @@ { "path": "../packages/builder", }, + { + "path": "../sites/test", + }, { "path": "..", }, diff --git a/README.md b/README.md index 9ab23d7..1e002c6 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,139 @@ -# PushForge πŸš€ -
-PushForge Logo +PushForge Logo + +# PushForge -**Modern, Cross-Platform Web Push Notifications** +**Web Push Notifications for the Modern Stack** +[![npm version](https://img.shields.io/npm/v/@pushforge/builder.svg)](https://www.npmjs.com/package/@pushforge/builder) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Node Version](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen)](https://nodejs.org/) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![TypeScript](https://img.shields.io/badge/TypeScript-first--class-blue.svg)](https://www.typescriptlang.org/) -
+Zero dependencies Β· Works everywhere Β· TypeScript-first -## What is PushForge? +[Documentation](packages/builder) Β· [npm](https://www.npmjs.com/package/@pushforge/builder) Β· [Report Bug](https://github.com/draphy/pushforge/issues) -PushForge is a comprehensive toolkit for implementing Web Push Notifications in modern web applications. It handles the complex parts of push notifications so you can focus on building great user experiences. +**[Try the Live Demo β†’](https://pushforge.draphy.org)** -**Zero dependencies. Cross-platform. TypeScript-first.** + -### Features +--- -- πŸ” Compliant VAPID authentication -- πŸ“¦ Streamlined payload encryption -- 🌐 Works everywhere: Node.js, Browsers, Deno, Bun, Cloudflare Workers -- 🧩 Modular architecture for flexible implementation -- πŸ› οΈ Built with TypeScript for robust type safety +## Live Demo -## Packages +See PushForge in action at **[pushforge.draphy.org](https://pushforge.draphy.org)** β€” a fully working test site powered by PushForge on Cloudflare Workers. -| Package | Description | Path | -| -------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- | -| [@pushforge/builder](packages/builder) | Core library for building push notification requests with proper VAPID encryption | [`packages/builder/`](packages/builder) | +- **Enable push notifications** on your device with a single toggle +- **Send a test notification** to all active devices β€” anyone visiting the page can send and receive +- **See it working across browsers** β€” Chrome, Firefox, Edge, Safari 16+, and more +- Subscriptions auto-expire after 5 minutes β€” no permanent data stored -_More packages coming soon!_ +The entire backend is a single Cloudflare Worker using `buildPushHTTPRequest()` from `@pushforge/builder` with zero additional dependencies. -## Quick Start +## The Problem -```bash -# Install the core package -npm install @pushforge/builder +Traditional web push libraries like `web-push` rely on Node.js-specific APIs that don't work in modern edge runtimes: -# Generate VAPID keys for push authentication -npx @pushforge/builder generate-vapid-keys +``` +❌ Cloudflare Workers - "crypto.createECDH is not a function" +❌ Vercel Edge - "https.request is not available" +❌ Convex - "Top-level await is not supported" ``` -Check out the complete documentation in each package's README for detailed usage examples. +## The Solution -## Project Structure +PushForge uses standard Web APIs that work everywhere: -``` -pushforge/ -β”œβ”€β”€ packages/ -β”‚ └── builder/ # Core push notification builder -β”‚ β”œβ”€β”€ lib/ # Source code -β”‚ β”œβ”€β”€ examples/ # Usage examples (coming soon...) -β”‚ └── README.md # Package documentation -└── README.md # This file -``` +```typescript +import { buildPushHTTPRequest } from "@pushforge/builder"; -## Requirements +const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK: VAPID_PRIVATE_KEY, + subscription: userSubscription, + message: { + payload: { title: "Hello!", body: "This works everywhere." }, + adminContact: "mailto:admin@example.com" + } +}); -- **Node.js**: v16.0.0 or higher (for WebCrypto API support) -- **NPM**, **Yarn**, or **pnpm** for package management +await fetch(endpoint, { method: "POST", headers, body }); +``` -## Development Setup +## Why PushForge? -1. Clone the repository: +| | PushForge | web-push | +|---|:---:|:---:| +| Dependencies | **0** | 5+ | +| Cloudflare Workers | βœ… | [❌](https://github.com/web-push-libs/web-push/issues/718) | +| Vercel Edge | βœ… | ❌ | +| Convex | βœ… | ❌ | +| Deno / Bun | βœ… | Limited | +| TypeScript | Native | @types | - ```bash - git clone https://github.com/draphy/pushforge.git - cd pushforge - ``` +## Quick Start -2. Install dependencies: +```bash +# Install +npm install @pushforge/builder - ```bash - pnpm install - ``` +# Generate VAPID keys +npx @pushforge/builder vapid +``` -3. Build packages: +**Frontend** - Subscribe users: - ```bash - pnpm build - ``` +```javascript +const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: VAPID_PUBLIC_KEY +}); +// Send subscription.toJSON() to your server +``` -4. Available Commands: +**Backend** - Send notifications: - ```bash - # Format and lint code - pnpm biome:format # Format code with Biome - pnpm biome:lint # Lint code with Biome - pnpm biome:check # Check code with Biome - pnpm biome:fix # Fix issues automatically with Biome +```typescript +import { buildPushHTTPRequest } from "@pushforge/builder"; - # Type checking - pnpm type:check # Run TypeScript type checking +const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK: process.env.VAPID_PRIVATE_KEY, + subscription, + message: { + payload: { title: "New Message", body: "You have a notification!" }, + adminContact: "mailto:admin@example.com" + } +}); - # Commit checks (run before committing) - pnpm commit:check # Run formatting, type checking and build - ``` +await fetch(endpoint, { method: "POST", headers, body }); +``` -## Contributing +See the [full documentation](packages/builder) for platform-specific examples (Cloudflare Workers, Vercel Edge, Convex, Deno, Bun). -Contributions are always welcome! We follow a structured workflow for contributions - see our [Contributing Guidelines](CONTRIBUTING.md) for details. +## Packages -Whether you want to: +| Package | Description | +|---------|-------------| +| [@pushforge/builder](packages/builder) | Core library for building push notification requests | -- πŸ› Report a bug -- πŸ’‘ Suggest new features -- πŸ§ͺ Improve tests -- πŸ“š Enhance documentation -- πŸ’» Submit a PR +## Requirements -We appreciate your help making PushForge better for everyone. +- **Node.js 20+** or any runtime with Web Crypto API +- Supported: Cloudflare Workers, Vercel Edge, Convex, Deno, Bun, modern browsers -## Reporting Issues +## Development -Found a bug or have a feature request? Please [open an issue](https://github.com/draphy/pushforge/issues/new) and provide as much detail as possible. +```bash +git clone https://github.com/draphy/pushforge.git +cd pushforge +pnpm install +pnpm build +``` -## Sponsorship +## Contributing -If you find PushForge valuable, consider [sponsoring the project](https://github.com/sponsors/draphy). Your sponsorship helps maintain and improve the library. +Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - ---- - -
- Built with ❀️ by David Raphi -
+MIT Β© [David Raphi](https://github.com/draphy) diff --git a/biome.json b/biome.json index 18e515d..cbd0fbf 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", "organizeImports": { "enabled": true }, @@ -15,7 +15,15 @@ "useImportType": "error" } }, - "ignore": ["dist", "node_modules", "build", "tmp", "dev-dist", ".vscode"] + "ignore": [ + "dist", + "node_modules", + "build", + "tmp", + "dev-dist", + ".vscode", + "sites" + ] }, "formatter": { "enabled": true, @@ -29,7 +37,8 @@ "build", "tmp", "dev-dist", - ".vscode" + ".vscode", + "sites" ] }, "javascript": { diff --git a/images/logo.webp b/images/logo.webp new file mode 100644 index 0000000..7744ead Binary files /dev/null and b/images/logo.webp differ diff --git a/images/pushforge_logo.png b/images/pushforge_logo.png deleted file mode 100644 index 9a40ac9..0000000 Binary files a/images/pushforge_logo.png and /dev/null differ diff --git a/package.json b/package.json index 272e842..3a166ad 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "email": "david@draphy.org" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", diff --git a/packages/builder/README.md b/packages/builder/README.md index c2fcf21..e5f554c 100644 --- a/packages/builder/README.md +++ b/packages/builder/README.md @@ -1,198 +1,360 @@ -# PushForge Builder +
-A robust, cross-platform Web Push notification library that handles VAPID authentication and payload encryption following the Web Push Protocol standard. +PushForge Logo -## Installation +# PushForge Builder -Choose your preferred package manager: +**A lightweight, dependency-free Web Push library built on the standard Web Crypto API.** -```bash -# NPM -npm install @pushforge/builder +[![npm version](https://img.shields.io/npm/v/@pushforge/builder.svg)](https://www.npmjs.com/package/@pushforge/builder) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/TypeScript-first--class-blue.svg)](https://www.typescriptlang.org/) -# Yarn -yarn add @pushforge/builder +Send push notifications from any JavaScript runtime Β· Zero dependencies -# pnpm -pnpm add @pushforge/builder -``` +[GitHub](https://github.com/draphy/pushforge) Β· [npm](https://www.npmjs.com/package/@pushforge/builder) Β· [Report Bug](https://github.com/draphy/pushforge/issues) -## Features +**[Try the Live Demo β†’](https://pushforge.draphy.org)** -- πŸ”‘ Compliant VAPID authentication -- πŸ”’ Web Push Protocol encryption -- 🌐 Cross-platform compatibility (Node.js 16+, Browsers, Deno, Bun, Cloudflare Workers) -- 🧩 TypeScript definitions included -- πŸ› οΈ Zero dependencies +
-## Getting Started +--- -### Step 1: Generate VAPID Keys +```bash +npm install @pushforge/builder +``` -PushForge includes a built-in CLI tool to generate VAPID keys for Web Push Authentication: +## Live Demo -```bash -# Generate VAPID keys using npx -npx @pushforge/builder generate-vapid-keys +Try PushForge in your browser at **[pushforge.draphy.org](https://pushforge.draphy.org)** β€” a live test site running on Cloudflare Workers. -# Using yarn -yarn dlx @pushforge/builder generate-vapid-keys +- Toggle push notifications on, send a test message, and see it arrive in real time +- Works across all supported browsers β€” Chrome, Firefox, Edge, Safari 16+ +- The backend is a single Cloudflare Worker using `buildPushHTTPRequest()` with zero additional dependencies +- Subscriptions auto-expire after 5 minutes β€” no permanent data stored -# Using pnpm -pnpm dlx @pushforge/builder generate-vapid-keys -``` +## Why PushForge? -This will output a public key and private key that you can use for VAPID authentication: +| | PushForge | web-push | +|---|:---:|:---:| +| Dependencies | **0** | 5+ (with nested deps) | +| Cloudflare Workers | Yes | [No](https://github.com/web-push-libs/web-push/issues/718) | +| Vercel Edge | Yes | No | +| Convex | Yes | No | +| Deno / Bun | Yes | Limited | +| TypeScript | First-class | @types package | -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ VAPID Keys Generated Successfully β”‚ -β”‚ β”‚ -β”‚ Public Key: β”‚ -β”‚ BDd0DtL3qQmnI7-JPwKMuGuFBC7VW9GjKP0qR-4C9Y9lJ2LLWR0pSI... β”‚ -β”‚ β”‚ -β”‚ Private Key (JWK): β”‚ -β”‚ { β”‚ -β”‚ "alg": "ES256", β”‚ -β”‚ "kty": "EC", β”‚ -β”‚ "crv": "P-256", β”‚ -β”‚ "x": "N3QO0vepCacjv4k_AoyYa4UELtVb0aMo_SpH7gL1j2U", β”‚ -β”‚ "y": "ZSdiy1kdKUiOGjuoVgMbp4HwmQDz0nhHxPJLbFYh1j8", β”‚ -β”‚ "d": "8M9F5JCaEsXdTU1OpD4ODq-o5qZQcDmCYS6EHrC1o8E" β”‚ -β”‚ } β”‚ -β”‚ β”‚ -β”‚ Store these keys securely. Never expose your private key. β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` +Traditional web push libraries rely on Node.js-specific APIs (`crypto.createECDH`, `https.request`) that don't work in modern edge runtimes. PushForge uses the standard [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), making it portable across all JavaScript environments. -**Requirements:** +## Quick Start -- Node.js 16.0.0 or later -- The command uses the WebCrypto API which is built-in to Node.js 16+ +### 1. Generate VAPID Keys -### Step 2: Set Up Push Notifications in Your Web Application +```bash +npx @pushforge/builder vapid +``` -To implement push notifications in your web application, you'll need to: +This outputs a public key (for your frontend) and a private key in JWK format (for your server). -1. Use the VAPID public key generated in Step 1 -2. Request notification permission from the user -3. Subscribe to push notifications using the Push API -4. Save the subscription information in your backend +### 2. Subscribe Users (Frontend) -When implementing your service worker, handle push events to display notifications when they arrive: +Use the VAPID public key to subscribe users to push notifications: -1. Listen for `push` events -2. Parse the notification data -3. Display the notification to the user -4. Handle notification click events +```javascript +// In your frontend application +const registration = await navigator.serviceWorker.ready; -Refer to the [Push API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) and [Notifications API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) for detailed implementation. +const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY' // From step 1 +}); -### Step 3: Send Push Notifications from Your Server +// Send this subscription to your server +// subscription.toJSON() returns: +// { +// endpoint: "https://fcm.googleapis.com/fcm/send/...", +// keys: { +// p256dh: "BNcRd...", +// auth: "tBHI..." +// } +// } +await fetch('/api/subscribe', { + method: 'POST', + body: JSON.stringify(subscription) +}); +``` -On your backend server, use the VAPID private key to send push notifications: +### 3. Send Notifications (Server) ```typescript import { buildPushHTTPRequest } from "@pushforge/builder"; -// Load the private key from your secure environment -// This should be the private key from your VAPID key generation -const privateJWK = process.env.VAPID_PRIVATE_KEY; +// Your VAPID private key (JWK format from step 1) +const privateJWK = { + kty: "EC", + crv: "P-256", + x: "...", + y: "...", + d: "..." +}; -// User subscription from browser push API +// The subscription object from the user's browser const subscription = { - endpoint: "https://fcm.googleapis.com/fcm/send/DEVICE_TOKEN", + endpoint: "https://fcm.googleapis.com/fcm/send/...", keys: { - p256dh: "USER_PUBLIC_KEY", - auth: "USER_AUTH_SECRET", - }, -}; - -// Create message with payload -const message = { - payload: { - title: "New Message", - body: "You have a new message!", - icon: "/images/icon.png", - }, - options: { - //Default value is 24 * 60 * 60 (24 hours). - //The VAPID JWT expiration claim (`exp`) must not exceed 24 hours from the time of the request. - ttl: 3600, // Time-to-live in seconds - urgency: "normal", // Options: "very-low", "low", "normal", "high" - topic: "updates", // Optional topic for replacing notifications - }, - adminContact: "mailto:admin@example.com", //The contact information of the administrator + p256dh: "BNcRd...", + auth: "tBHI..." + } }; -// Build the push notification request -const {endpoint, headers, body} = await buildPushHTTPRequest({ +// Build and send the notification +const { endpoint, headers, body } = await buildPushHTTPRequest({ privateJWK, - message, subscription, + message: { + payload: { + title: "New Message", + body: "You have a new notification!", + icon: "/icon.png" + }, + adminContact: "mailto:admin@example.com" + } }); -// Send the push notification const response = await fetch(endpoint, { method: "POST", headers, - body, + body }); if (response.status === 201) { - console.log("Push notification sent successfully"); -} else { - console.error("Failed to send push notification", await response.text()); + console.log("Notification sent"); } ``` -## Cross-Platform Support +## Understanding Push Subscriptions -PushForge works in all major JavaScript environments: +When a user subscribes to push notifications, the browser returns a `PushSubscription` object: -### Node.js 16+ +```javascript +{ + // The unique URL for this user's browser push service + endpoint: "https://fcm.googleapis.com/fcm/send/dAPT...", -```js -import { buildPushHTTPRequest } from "@pushforge/builder"; -// OR -const { buildPushHTTPRequest } = require("@pushforge/builder"); + keys: { + // Public key for encrypting messages (base64url) + p256dh: "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA...", + + // Authentication secret (base64url) + auth: "tBHItJI5svbpez7KI4CCXg==" + } +} +``` + +| Field | Description | +|-------|-------------| +| `endpoint` | The push service URL. Each browser vendor has their own (Google FCM, Mozilla autopush, Apple APNs). | +| `p256dh` | The user's public key for ECDH P-256 message encryption. | +| `auth` | A shared 16-byte authentication secret. | + +Store these securely on your server. You'll need them to send notifications to this user. + +## API Reference + +### `buildPushHTTPRequest(options)` + +Builds an HTTP request for sending a push notification. -// Use normally - Node.js 16+ has Web Crypto API built-in +```typescript +const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK, // Your VAPID private key (JWK object or JSON string) + subscription, // User's push subscription + message: { + payload, // Any JSON-serializable data + adminContact, // Contact email (mailto:...) or URL + options: { // Optional + ttl, // Time-to-live in seconds (default: 86400) + urgency, // "very-low" | "low" | "normal" | "high" + topic // Topic for notification coalescing + } + } +}); +``` + +**Returns:** `{ endpoint: string, headers: Headers, body: ArrayBuffer }` + +## Platform Examples + +### Cloudflare Workers + +```javascript +export default { + async fetch(request, env) { + const subscription = await request.json(); + + const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK: JSON.parse(env.VAPID_PRIVATE_KEY), + subscription, + message: { + payload: { title: "Hello from the Edge!" }, + adminContact: "mailto:admin@example.com" + } + }); + + return fetch(endpoint, { method: "POST", headers, body }); + } +}; ``` -### Browsers +### Vercel Edge Functions -```js +```typescript import { buildPushHTTPRequest } from "@pushforge/builder"; -// Use in a service worker for push notification handling +export const config = { runtime: "edge" }; + +export default async function handler(request: Request) { + const subscription = await request.json(); + + const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK: JSON.parse(process.env.VAPID_PRIVATE_KEY!), + subscription, + message: { + payload: { title: "Edge Notification" }, + adminContact: "mailto:admin@example.com" + } + }); + + await fetch(endpoint, { method: "POST", headers, body }); + return new Response("Sent", { status: 200 }); +} +``` + +### Convex + +```typescript +import { action } from "./_generated/server"; +import { buildPushHTTPRequest } from "@pushforge/builder"; +import { v } from "convex/values"; + +export const sendPush = action({ + args: { subscription: v.any(), title: v.string(), body: v.string() }, + handler: async (ctx, { subscription, title, body }) => { + const { endpoint, headers, body: reqBody } = await buildPushHTTPRequest({ + privateJWK: JSON.parse(process.env.VAPID_PRIVATE_KEY!), + subscription, + message: { + payload: { title, body }, + adminContact: "mailto:admin@example.com" + } + }); + + await fetch(endpoint, { method: "POST", headers, body: reqBody }); + } +}); ``` ### Deno -```js -// Import from npm CDN -import { buildPushHTTPRequest } from "https://cdn.jsdelivr.net/npm/@pushforge/builder/dist/lib/main.js"; +```typescript +import { buildPushHTTPRequest } from "npm:@pushforge/builder"; + +const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK: JSON.parse(Deno.env.get("VAPID_PRIVATE_KEY")!), + subscription, + message: { + payload: { title: "Hello from Deno!" }, + adminContact: "mailto:admin@example.com" + } +}); -// Run with --allow-net permissions +await fetch(endpoint, { method: "POST", headers, body }); ``` ### Bun -```js +```typescript import { buildPushHTTPRequest } from "@pushforge/builder"; -// Works natively in Bun with no special configuration +const { endpoint, headers, body } = await buildPushHTTPRequest({ + privateJWK: JSON.parse(Bun.env.VAPID_PRIVATE_KEY!), + subscription, + message: { + payload: { title: "Hello from Bun!" }, + adminContact: "mailto:admin@example.com" + } +}); + +await fetch(endpoint, { method: "POST", headers, body }); ``` -### Cloudflare Workers +## Service Worker Setup + +Handle incoming push notifications in your service worker: + +```javascript +// sw.js +self.addEventListener('push', (event) => { + const data = event.data?.json() ?? {}; + + event.waitUntil( + self.registration.showNotification(data.title, { + body: data.body, + icon: data.icon, + badge: data.badge, + data: data.url + }) + ); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + + if (event.notification.data) { + event.waitUntil(clients.openWindow(event.notification.data)); + } +}); +``` + +## Requirements + +**Node.js 20+** or any runtime with Web Crypto API support. + +| Environment | Status | +|-------------|--------| +| Node.js 20+ | Fully supported | +| Cloudflare Workers | Fully supported | +| Vercel Edge | Fully supported | +| Deno | Fully supported | +| Bun | Fully supported | +| Convex | Fully supported | +| Modern Browsers | Fully supported | + +
+Node.js 18 (requires polyfill) + +```javascript +import { webcrypto } from "node:crypto"; +globalThis.crypto = webcrypto; -```js import { buildPushHTTPRequest } from "@pushforge/builder"; ``` +Or: `node --experimental-global-webcrypto your-script.js` + +
+ +## Security + +PushForge validates all inputs before processing: + +- VAPID key structure (EC P-256 curve with required x, y, d parameters) +- Subscription endpoint (must be valid HTTPS URL) +- p256dh key format (65-byte uncompressed P-256 point) +- Auth secret length (exactly 16 bytes) +- Payload size (max 4KB per Web Push spec) +- TTL bounds (max 24 hours per VAPID spec) + ## License MIT diff --git a/packages/builder/lib/commandLine/keys.ts b/packages/builder/lib/commandLine/keys.ts index a7f216e..61489c2 100644 --- a/packages/builder/lib/commandLine/keys.ts +++ b/packages/builder/lib/commandLine/keys.ts @@ -1,47 +1,35 @@ #!/usr/bin/env node import { getPublicKeyFromJwk } from '../utils.js'; -let webcrypto: Crypto; -try { - const nodeCrypto = await import('node:crypto'); - webcrypto = nodeCrypto.webcrypto as Crypto; -} catch { - console.error('Error: This command requires Node.js environment.'); - console.error("Please ensure you're running Node.js 16.0.0 or later."); +if (!globalThis.crypto?.subtle) { + console.error('Error: Web Crypto API not available.'); + console.error('Please ensure you are running Node.js 20.0.0 or later.'); process.exit(1); } async function generateVapidKeys(): Promise { try { - console.log('Generating VAPID keys...'); + console.log('Generating VAPID keys...\n'); - const keypair = await webcrypto.subtle.generateKey( + const keypair = await globalThis.crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ); - const privateJWK = await webcrypto.subtle.exportKey( + const privateJWK = await globalThis.crypto.subtle.exportKey( 'jwk', keypair.privateKey, ); const privateJWKWithAlg = { alg: 'ES256', ...privateJWK }; const publicKey = getPublicKeyFromJwk(privateJWKWithAlg); - // Display in a nice formatted output - const resultText = ` -VAPID Keys Generated Successfully - -Public Key: -${publicKey} - -Private Key (JWK): -${JSON.stringify(privateJWKWithAlg, null, 2)} - -Store these keys securely. Never expose your private key. -`; - - console.log(resultText); + console.log('VAPID Keys Generated Successfully\n'); + console.log('Public Key:'); + console.log(publicKey); + console.log('\nPrivate Key (JWK):'); + console.log(JSON.stringify(privateJWKWithAlg, null, 2)); + console.log('\nStore these keys securely. Never expose your private key.'); } catch (error: unknown) { console.error('Error generating VAPID keys:'); if (error instanceof Error) { @@ -50,25 +38,43 @@ Store these keys securely. Never expose your private key. console.error('An unknown error occurred.'); } console.error( - '\nThis tool requires Node.js v16.0.0 or later with WebCrypto API support.', + '\nThis tool requires Node.js 20.0.0 or later with Web Crypto API support.', ); process.exit(1); } } -// Simple command parsing -const args = process.argv.slice(2); -const command = args[0]; - -if (command === 'generate-vapid-keys') { - generateVapidKeys(); -} else { +function showHelp(): void { console.log(` -PushForge CLI Tools +PushForge CLI -Usage: - npx @pushforge/builder generate-vapid-keys Generate VAPID key pair for Web Push Authentication +Usage: npx @pushforge/builder + +Commands: + vapid Generate VAPID key pair for Web Push authentication + help Show this help message + +Examples: + npx @pushforge/builder vapid + +Documentation: https://github.com/draphy/pushforge#readme +`); +} + +// Parse command +const args = process.argv.slice(2); +const command = args[0]?.toLowerCase(); -For more information, visit: https://github.com/draphy/pushforge - `); +switch (command) { + case 'vapid': + generateVapidKeys(); + break; + case 'help': + case '--help': + case '-h': + showHelp(); + break; + default: + showHelp(); + process.exit(command ? 1 : 0); } diff --git a/packages/builder/lib/crypto.ts b/packages/builder/lib/crypto.ts index 6d754c4..55a0caa 100644 --- a/packages/builder/lib/crypto.ts +++ b/packages/builder/lib/crypto.ts @@ -3,34 +3,25 @@ /** * A module that provides a cross-platform cryptographic interface. + * Uses globalThis.crypto which is available in: + * - Node.js 20+ (current LTS) + * - Browsers + * - Cloudflare Workers + * - Deno + * - Bun + * - Convex * * @module crypto */ -let isomorphicCrypto: Crypto; - -// Cloudflare Worker, Deno, Bun and Browser environments have crypto globally available -if (globalThis.crypto?.subtle) { - isomorphicCrypto = globalThis.crypto; -} -// Node.js requires importing the webcrypto module -else if (typeof process !== 'undefined' && process.versions?.node) { - try { - const { webcrypto } = await import('node:crypto'); - isomorphicCrypto = webcrypto as unknown as Crypto; - } catch { - throw new Error( - 'Crypto API not available in this Node.js environment. Please use Node.js 16+ which supports the Web Crypto API.', - ); - } -} -// Fallback error for unsupported environments -else { +if (!globalThis.crypto?.subtle) { throw new Error( - 'No Web Crypto API implementation available in this environment.', + 'Web Crypto API not available. Ensure you are using Node.js 20+ or a modern runtime with globalThis.crypto support.', ); } +const isomorphicCrypto: Crypto = globalThis.crypto; + /** * A cryptographic interface that provides methods for generating random values * and accessing subtle cryptographic operations. diff --git a/packages/builder/lib/payload.ts b/packages/builder/lib/payload.ts index cd4c201..453ca9d 100644 --- a/packages/builder/lib/payload.ts +++ b/packages/builder/lib/payload.ts @@ -40,6 +40,20 @@ const importClientKeys = async ( decodedKey = new Uint8Array(Buffer.from(base64Key, 'base64')); } + // Validate p256dh key format: must be 65 bytes (uncompressed P-256 point) + // Format: 0x04 (1 byte) + x coordinate (32 bytes) + y coordinate (32 bytes) + if (decodedKey.byteLength !== 65) { + throw new Error( + `Invalid p256dh key: expected 65 bytes but got ${decodedKey.byteLength} bytes`, + ); + } + + if (decodedKey[0] !== 0x04) { + throw new Error( + `Invalid p256dh key: expected uncompressed point format (0x04 prefix) but got 0x${decodedKey[0].toString(16).padStart(2, '0')}`, + ); + } + const p256 = await crypto.subtle.importKey( 'jwk', { @@ -95,7 +109,7 @@ const derivePseudoRandomKey = async ( const createContext = async ( clientPublicKey: CryptoKey, localPublicKey: CryptoKey, -): Promise => { +): Promise> => { const [clientKeyBytes, localKeyBytes] = await Promise.all([ crypto.subtle.exportKey('raw', clientPublicKey), crypto.subtle.exportKey('raw', localPublicKey), @@ -120,8 +134,8 @@ const createContext = async ( */ const deriveNonce = async ( pseudoRandomKey: CryptoKey, - salt: Uint8Array, - context: Uint8Array, + salt: Uint8Array, + context: Uint8Array, ): Promise => { const nonceInfo = concatTypedArrays([ new TextEncoder().encode('Content-Encoding: nonce\0'), @@ -145,8 +159,8 @@ const deriveNonce = async ( */ const deriveContentEncryptionKey = async ( pseudoRandomKey: CryptoKey, - salt: Uint8Array, - context: Uint8Array, + salt: Uint8Array, + context: Uint8Array, ): Promise => { const info = concatTypedArrays([ new TextEncoder().encode('Content-Encoding: aesgcm\0'), @@ -162,6 +176,19 @@ const deriveContentEncryptionKey = async ( return crypto.subtle.importKey('raw', bits, 'AES-GCM', false, ['encrypt']); }; +/** + * Maximum payload size after accounting for encryption overhead. + * Web push payloads have an overall max size of 4KB (4096 bytes). + * With the required overhead (16 bytes auth tag + 2 bytes padding length), + * the actual max payload size is 4078 bytes. + */ +const MAX_PAYLOAD_SIZE = 4078; + +/** + * Minimum required size for padding length prefix (2 bytes). + */ +const PADDING_LENGTH_PREFIX_SIZE = 2; + /** * Pads the payload to ensure it fits within the maximum allowed size for web push notifications. * @@ -169,20 +196,32 @@ const deriveContentEncryptionKey = async ( * required overhead for encryption, the actual max payload size is 4078 bytes. * * @param {Uint8Array} payload - The original payload to be padded. - * @returns {Uint8Array} The padded payload, including length information. + * @returns {Uint8Array} The padded payload, including length information. + * @throws {Error} Throws an error if the payload exceeds the maximum allowed size. */ -const padPayload = (payload: Uint8Array): Uint8Array => { - const MAX_PAYLOAD_SIZE = 4078; // Maximum payload size after encryption overhead - - let paddingSize = Math.round(Math.random() * 100); // Random padding size - const payloadSizeWithPadding = payload.byteLength + 2 + paddingSize; +const padPayload = (payload: Uint8Array): Uint8Array => { + const maxPayloadContentSize = MAX_PAYLOAD_SIZE - PADDING_LENGTH_PREFIX_SIZE; - if (payloadSizeWithPadding > MAX_PAYLOAD_SIZE) { - // Adjust padding size if the total exceeds the maximum allowed size - paddingSize -= payloadSizeWithPadding - MAX_PAYLOAD_SIZE; + if (payload.byteLength > maxPayloadContentSize) { + throw new Error( + `Payload too large. Maximum size is ${maxPayloadContentSize} bytes, but received ${payload.byteLength} bytes`, + ); } - const paddingArray = new ArrayBuffer(2 + paddingSize); + // Calculate available space for padding + const availableSpace = + MAX_PAYLOAD_SIZE - PADDING_LENGTH_PREFIX_SIZE - payload.byteLength; + + // Generate random padding size, clamped to available space + const maxRandomPadding = Math.min(100, availableSpace); + const paddingSize = + maxRandomPadding > 0 + ? Math.floor(Math.random() * (maxRandomPadding + 1)) + : 0; + + const paddingArray = new ArrayBuffer( + PADDING_LENGTH_PREFIX_SIZE + paddingSize, + ); new DataView(paddingArray).setUint16(0, paddingSize); // Store the length of the padding // Return the new payload with padding added @@ -200,7 +239,7 @@ const padPayload = (payload: Uint8Array): Uint8Array => { */ export const encryptPayload = async ( localKeys: CryptoKeyPair, - salt: Uint8Array, + salt: Uint8Array, payload: string, target: PushSubscription, ): Promise => { diff --git a/packages/builder/lib/request.ts b/packages/builder/lib/request.ts index 3f0bfd2..b415de7 100644 --- a/packages/builder/lib/request.ts +++ b/packages/builder/lib/request.ts @@ -3,6 +3,61 @@ import { encryptPayload } from './payload.js'; import type { BuilderOptions, PushOptions } from './types.js'; import { vapidHeaders } from './vapid.js'; +/** + * Validates that a JWK has the required properties for ECDSA P-256. + * + * @param {JsonWebKey} jwk - The JSON Web Key to validate. + * @throws {Error} Throws if the JWK is missing required properties or has invalid values. + */ +const validatePrivateJWK = (jwk: JsonWebKey): void => { + if (jwk.kty !== 'EC') { + throw new Error( + `Invalid JWK: 'kty' must be 'EC', received '${jwk.kty ?? 'undefined'}'`, + ); + } + + if (jwk.crv !== 'P-256') { + throw new Error( + `Invalid JWK: 'crv' must be 'P-256', received '${jwk.crv ?? 'undefined'}'`, + ); + } + + if (!jwk.x || typeof jwk.x !== 'string') { + throw new Error("Invalid JWK: missing or invalid 'x' coordinate"); + } + + if (!jwk.y || typeof jwk.y !== 'string') { + throw new Error("Invalid JWK: missing or invalid 'y' coordinate"); + } + + if (!jwk.d || typeof jwk.d !== 'string') { + throw new Error("Invalid JWK: missing or invalid 'd' (private key)"); + } +}; + +/** + * Validates that the subscription endpoint is a valid HTTPS URL. + * + * @param {string} endpoint - The push subscription endpoint URL. + * @throws {Error} Throws if the endpoint is not a valid HTTPS URL. + */ +const validateEndpoint = (endpoint: string): void => { + let url: URL; + try { + url = new URL(endpoint); + } catch { + throw new Error( + `Invalid subscription endpoint: '${endpoint}' is not a valid URL`, + ); + } + + if (url.protocol !== 'https:') { + throw new Error( + `Invalid subscription endpoint: push endpoints must use HTTPS, received '${url.protocol}'`, + ); + } +}; + /** * Builds an HTTP request body and headers for sending a push notification. * @@ -67,8 +122,18 @@ export async function buildPushHTTPRequest({ headers: Record | Headers; }> { // Parse the private JWK if it's a string - const jwk: JsonWebKey = - typeof privateJWK === 'string' ? JSON.parse(privateJWK) : privateJWK; + let jwk: JsonWebKey; + try { + jwk = typeof privateJWK === 'string' ? JSON.parse(privateJWK) : privateJWK; + } catch { + throw new Error('Invalid privateJWK: failed to parse JSON string'); + } + + // Validate the JWK structure + validatePrivateJWK(jwk); + + // Validate the subscription endpoint + validateEndpoint(subscription.endpoint); const MAX_TTL = 24 * 60 * 60; diff --git a/packages/builder/lib/utils.ts b/packages/builder/lib/utils.ts index de1fdf9..4acea10 100644 --- a/packages/builder/lib/utils.ts +++ b/packages/builder/lib/utils.ts @@ -104,9 +104,11 @@ export const getPublicKeyFromJwk = (jwk: JsonWebKey): string => * Concatenates multiple Uint8Array instances into a single Uint8Array. * * @param {Uint8Array[]} arrays - An array of Uint8Array instances to concatenate. - * @returns {Uint8Array} A new Uint8Array containing all the concatenated data. + * @returns {Uint8Array} A new Uint8Array containing all the concatenated data. */ -export const concatTypedArrays = (arrays: Uint8Array[]): Uint8Array => { +export const concatTypedArrays = ( + arrays: readonly Uint8Array[], +): Uint8Array => { const length = arrays.reduce( (accumulator, current) => accumulator + current.byteLength, 0, diff --git a/packages/builder/package.json b/packages/builder/package.json index dcc5146..fe55871 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/draphy/pushforge/issues" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "repository": { "type": "git", @@ -40,7 +40,7 @@ "semantic-release": "semantic-release" }, "bin": { - "generate-vapid-keys": "./dist/lib/commandLine/keys.js" + "pushforge": "./dist/lib/commandLine/keys.js" }, "devDependencies": { "@semantic-release/commit-analyzer": "^11.1.0", diff --git a/packages/builder/release.config.cjs b/packages/builder/release.config.cjs index 636213b..a4e73ab 100644 --- a/packages/builder/release.config.cjs +++ b/packages/builder/release.config.cjs @@ -3,7 +3,12 @@ module.exports = { plugins: [ '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', - '@semantic-release/npm', + [ + '@semantic-release/npm', + { + npmPublish: false, + }, + ], [ '@semantic-release/github', { diff --git a/packages/builder/test/unit.test.ts b/packages/builder/test/unit.test.ts new file mode 100644 index 0000000..33b89b5 --- /dev/null +++ b/packages/builder/test/unit.test.ts @@ -0,0 +1,630 @@ +import { describe, expect, test } from 'vitest'; +import { buildPushHTTPRequest } from '../lib/main.js'; +import type { + BuilderOptions, + PushMessage, + PushSubscription, +} from '../lib/types.js'; +import { subscriptions } from './fixtures/fixtures.js'; +import { vapidKeys } from './fixtures/vapid.js'; + +/** + * Valid test data for reuse across tests + */ +const validSubscription: PushSubscription = subscriptions.chrome; + +const validMessage: PushMessage = { + payload: { title: 'Test', body: 'Test message' }, + adminContact: 'mailto:test@example.com', + options: { ttl: 3600 }, +}; + +const validPrivateJWK = vapidKeys.privateJWK; + +/** + * Helper to create BuilderOptions with overrides + */ +const createOptions = ( + overrides: Partial = {}, +): BuilderOptions => ({ + privateJWK: validPrivateJWK, + subscription: validSubscription, + message: validMessage, + ...overrides, +}); + +// ============================================================================ +// JWK Validation Tests +// ============================================================================ +describe('JWK Validation', () => { + test('accepts valid JWK as string', async () => { + const result = await buildPushHTTPRequest(createOptions()); + expect(result.endpoint).toBe(validSubscription.endpoint); + expect(result.body).toBeInstanceOf(ArrayBuffer); + expect(result.headers).toBeDefined(); + }); + + test('accepts valid JWK as object', async () => { + const jwkObject = JSON.parse(validPrivateJWK); + const result = await buildPushHTTPRequest( + createOptions({ privateJWK: jwkObject }), + ); + expect(result.endpoint).toBe(validSubscription.endpoint); + }); + + test('rejects invalid JSON string', async () => { + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: 'not valid json' })), + ).rejects.toThrow('Invalid privateJWK: failed to parse JSON string'); + }); + + test('rejects JWK with wrong kty', async () => { + const invalidJWK = { ...JSON.parse(validPrivateJWK), kty: 'RSA' }; + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: invalidJWK })), + ).rejects.toThrow("Invalid JWK: 'kty' must be 'EC', received 'RSA'"); + }); + + test('rejects JWK with wrong curve', async () => { + const invalidJWK = { ...JSON.parse(validPrivateJWK), crv: 'P-384' }; + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: invalidJWK })), + ).rejects.toThrow("Invalid JWK: 'crv' must be 'P-256', received 'P-384'"); + }); + + test('rejects JWK missing x coordinate', async () => { + const invalidJWK = JSON.parse(validPrivateJWK); + invalidJWK.x = undefined; + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: invalidJWK })), + ).rejects.toThrow("Invalid JWK: missing or invalid 'x' coordinate"); + }); + + test('rejects JWK missing y coordinate', async () => { + const invalidJWK = JSON.parse(validPrivateJWK); + invalidJWK.y = undefined; + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: invalidJWK })), + ).rejects.toThrow("Invalid JWK: missing or invalid 'y' coordinate"); + }); + + test('rejects JWK missing d (private key)', async () => { + const invalidJWK = JSON.parse(validPrivateJWK); + invalidJWK.d = undefined; + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: invalidJWK })), + ).rejects.toThrow("Invalid JWK: missing or invalid 'd' (private key)"); + }); + + test('rejects JWK with missing kty', async () => { + const invalidJWK = JSON.parse(validPrivateJWK); + invalidJWK.kty = undefined; + await expect( + buildPushHTTPRequest(createOptions({ privateJWK: invalidJWK })), + ).rejects.toThrow("Invalid JWK: 'kty' must be 'EC', received 'undefined'"); + }); +}); + +// ============================================================================ +// Endpoint Validation Tests +// ============================================================================ +describe('Endpoint Validation', () => { + test('accepts valid HTTPS endpoint', async () => { + const result = await buildPushHTTPRequest(createOptions()); + expect(result.endpoint).toBe(validSubscription.endpoint); + }); + + test('rejects HTTP endpoint', async () => { + const httpSubscription: PushSubscription = { + ...validSubscription, + endpoint: 'http://example.com/push', + }; + await expect( + buildPushHTTPRequest(createOptions({ subscription: httpSubscription })), + ).rejects.toThrow("push endpoints must use HTTPS, received 'http:'"); + }); + + test('rejects invalid URL', async () => { + const invalidSubscription: PushSubscription = { + ...validSubscription, + endpoint: 'not-a-valid-url', + }; + await expect( + buildPushHTTPRequest( + createOptions({ subscription: invalidSubscription }), + ), + ).rejects.toThrow('is not a valid URL'); + }); + + test('rejects empty endpoint', async () => { + const emptySubscription: PushSubscription = { + ...validSubscription, + endpoint: '', + }; + await expect( + buildPushHTTPRequest(createOptions({ subscription: emptySubscription })), + ).rejects.toThrow('is not a valid URL'); + }); +}); + +// ============================================================================ +// Subscription Keys Validation Tests +// ============================================================================ +describe('Subscription Keys Validation', () => { + test('accepts valid p256dh and auth keys', async () => { + const result = await buildPushHTTPRequest(createOptions()); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('rejects invalid p256dh key (wrong length)', async () => { + const invalidSubscription: PushSubscription = { + ...validSubscription, + keys: { + ...validSubscription.keys, + p256dh: 'AAAA', // Too short - will decode to ~3 bytes instead of 65 + }, + }; + await expect( + buildPushHTTPRequest( + createOptions({ subscription: invalidSubscription }), + ), + ).rejects.toThrow('Invalid p256dh key: expected 65 bytes'); + }); + + test('rejects invalid auth key (wrong length)', async () => { + const invalidSubscription: PushSubscription = { + ...validSubscription, + keys: { + ...validSubscription.keys, + auth: 'AAAA', // Too short - will decode to ~3 bytes instead of 16 + }, + }; + await expect( + buildPushHTTPRequest( + createOptions({ subscription: invalidSubscription }), + ), + ).rejects.toThrow('Incorrect auth length, expected 16 bytes'); + }); + + test('rejects empty p256dh key', async () => { + const invalidSubscription: PushSubscription = { + ...validSubscription, + keys: { + ...validSubscription.keys, + p256dh: '', + }, + }; + await expect( + buildPushHTTPRequest( + createOptions({ subscription: invalidSubscription }), + ), + ).rejects.toThrow(); + }); + + test('rejects empty auth key', async () => { + const invalidSubscription: PushSubscription = { + ...validSubscription, + keys: { + ...validSubscription.keys, + auth: '', + }, + }; + await expect( + buildPushHTTPRequest( + createOptions({ subscription: invalidSubscription }), + ), + ).rejects.toThrow('Invalid input'); + }); +}); + +// ============================================================================ +// TTL Validation Tests +// ============================================================================ +describe('TTL Validation', () => { + test('accepts TTL within 24 hours', async () => { + const message: PushMessage = { + ...validMessage, + options: { ttl: 3600 }, // 1 hour + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.headers).toBeDefined(); + }); + + test('accepts TTL of exactly 24 hours', async () => { + const message: PushMessage = { + ...validMessage, + options: { ttl: 24 * 60 * 60 }, // 24 hours + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.headers).toBeDefined(); + }); + + test('rejects TTL exceeding 24 hours', async () => { + const message: PushMessage = { + ...validMessage, + options: { ttl: 24 * 60 * 60 + 1 }, // 24 hours + 1 second + }; + await expect( + buildPushHTTPRequest(createOptions({ message })), + ).rejects.toThrow('TTL must be less than 24 hours'); + }); + + test('uses default TTL when not specified', async () => { + const message: PushMessage = { + payload: { title: 'Test' }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + // Default TTL should be 24 hours = 86400 seconds + expect(getHeaderValue('TTL')).toBe('86400'); + }); + + test('uses default TTL when TTL is 0', async () => { + const message: PushMessage = { + ...validMessage, + options: { ttl: 0 }, + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + expect(getHeaderValue('TTL')).toBe('86400'); + }); +}); + +// ============================================================================ +// Payload Tests +// ============================================================================ +describe('Payload Handling', () => { + test('encrypts simple payload', async () => { + const result = await buildPushHTTPRequest(createOptions()); + expect(result.body).toBeInstanceOf(ArrayBuffer); + expect(result.body.byteLength).toBeGreaterThan(0); + }); + + test('encrypts empty object payload', async () => { + const message: PushMessage = { + payload: {}, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('encrypts payload with unicode characters', async () => { + const message: PushMessage = { + payload: { title: 'δ½ ε₯½δΈ–η•Œ', body: 'πŸŽ‰πŸš€βœ¨' }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('encrypts payload with special characters', async () => { + const message: PushMessage = { + payload: { + title: 'Test ', + body: 'Line1\nLine2\tTabbed', + }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('encrypts large payload (under limit)', async () => { + // Create a payload close to but under the 4076 byte limit + const largeData = 'x'.repeat(3000); + const message: PushMessage = { + payload: { data: largeData }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('rejects payload exceeding size limit', async () => { + // Create a payload that exceeds the 4076 byte limit + const hugeData = 'x'.repeat(5000); + const message: PushMessage = { + payload: { data: hugeData }, + adminContact: 'mailto:test@example.com', + }; + await expect( + buildPushHTTPRequest(createOptions({ message })), + ).rejects.toThrow('Payload too large'); + }); +}); + +// ============================================================================ +// Headers Tests +// ============================================================================ +describe('Headers Construction', () => { + test('includes required headers', async () => { + const result = await buildPushHTTPRequest(createOptions()); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + expect(getHeaderValue('Authorization')).toMatch(/^vapid t=.+, k=.+$/); + expect(getHeaderValue('Content-Type')).toBe('application/octet-stream'); + expect(getHeaderValue('Content-Encoding')).toBe('aesgcm'); + expect(getHeaderValue('TTL')).toBeDefined(); + expect(getHeaderValue('Encryption')).toMatch(/^salt=/); + expect(getHeaderValue('Crypto-Key')).toMatch(/^dh=/); + }); + + test('includes optional Topic header when specified', async () => { + const message: PushMessage = { + ...validMessage, + options: { ...validMessage.options, topic: 'test-topic' }, + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + expect(getHeaderValue('Topic')).toBe('test-topic'); + }); + + test('includes optional Urgency header when specified', async () => { + const message: PushMessage = { + ...validMessage, + options: { ...validMessage.options, urgency: 'high' }, + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + expect(getHeaderValue('Urgency')).toBe('high'); + }); + + test('omits Topic header when not specified', async () => { + const message: PushMessage = { + payload: validMessage.payload, + adminContact: validMessage.adminContact, + options: { ttl: 3600 }, // No topic + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + expect(getHeaderValue('Topic')).toBeNull(); + }); + + test('omits Urgency header when not specified', async () => { + const message: PushMessage = { + payload: validMessage.payload, + adminContact: validMessage.adminContact, + options: { ttl: 3600 }, // No urgency + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + expect(getHeaderValue('Urgency')).toBeNull(); + }); +}); + +// ============================================================================ +// VAPID Authorization Tests +// ============================================================================ +describe('VAPID Authorization', () => { + test('generates valid VAPID authorization header', async () => { + const result = await buildPushHTTPRequest(createOptions()); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + const authHeader = getHeaderValue('Authorization'); + expect(authHeader).toMatch( + /^vapid t=[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+, k=[A-Za-z0-9_-]+$/, + ); + }); + + test('JWT contains correct audience', async () => { + const result = await buildPushHTTPRequest(createOptions()); + + const getHeaderValue = (name: string): string | null => { + if (result.headers instanceof Headers) { + return result.headers.get(name); + } + return result.headers[name] || null; + }; + + const authHeader = getHeaderValue('Authorization'); + expect(authHeader).not.toBeNull(); + + const tokenMatch = authHeader?.match( + /t=([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/, + ); + expect(tokenMatch).not.toBeNull(); + + const [, payload] = tokenMatch?.[1].split('.') ?? []; + const decodedPayload = JSON.parse( + Buffer.from( + payload.replace(/-/g, '+').replace(/_/g, '/'), + 'base64', + ).toString(), + ); + + expect(decodedPayload.aud).toBe('https://fcm.googleapis.com'); + expect(decodedPayload.sub).toBe(validMessage.adminContact); + expect(decodedPayload.exp).toBeGreaterThan(Math.floor(Date.now() / 1000)); + }); +}); + +// ============================================================================ +// Multiple Subscriptions Tests +// ============================================================================ +describe('Multiple Subscription Types', () => { + test('handles Chrome/FCM subscription', async () => { + const result = await buildPushHTTPRequest( + createOptions({ + subscription: subscriptions.chrome, + }), + ); + expect(result.endpoint).toContain('fcm.googleapis.com'); + }); + + test('handles Edge/WNS subscription', async () => { + const result = await buildPushHTTPRequest( + createOptions({ + subscription: subscriptions.edge, + }), + ); + expect(result.endpoint).toContain('notify.windows.com'); + }); +}); + +// ============================================================================ +// Consistency Tests +// ============================================================================ +describe('Output Consistency', () => { + test('produces different encrypted bodies for same input (due to random salt)', async () => { + const options = createOptions(); + const result1 = await buildPushHTTPRequest(options); + const result2 = await buildPushHTTPRequest(options); + + // Bodies should be different due to random salt and padding + const body1 = new Uint8Array(result1.body); + const body2 = new Uint8Array(result2.body); + + // At least some bytes should differ + let hasDifference = false; + for (let i = 0; i < Math.min(body1.length, body2.length); i++) { + if (body1[i] !== body2[i]) { + hasDifference = true; + break; + } + } + expect(hasDifference).toBe(true); + }); + + test('produces consistent endpoint', async () => { + const options = createOptions(); + const result1 = await buildPushHTTPRequest(options); + const result2 = await buildPushHTTPRequest(options); + + expect(result1.endpoint).toBe(result2.endpoint); + }); +}); + +// ============================================================================ +// Edge Cases Tests +// ============================================================================ +describe('Edge Cases', () => { + test('handles null values in payload', async () => { + const message: PushMessage = { + payload: { title: 'Test', nullValue: null }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('handles nested objects in payload', async () => { + const message: PushMessage = { + payload: { + title: 'Test', + data: { + nested: { + deeply: { + value: 'test', + }, + }, + }, + }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('handles arrays in payload', async () => { + const message: PushMessage = { + payload: { + title: 'Test', + items: [1, 2, 3, 'four', { five: 5 }], + }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('handles boolean values in payload', async () => { + const message: PushMessage = { + payload: { + isTrue: true, + isFalse: false, + }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('handles numeric values in payload', async () => { + const message: PushMessage = { + payload: { + integer: 42, + float: 3.14, + negative: -100, + zero: 0, + }, + adminContact: 'mailto:test@example.com', + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); + + test('handles very long adminContact', async () => { + const message: PushMessage = { + payload: { title: 'Test' }, + adminContact: `mailto:${'a'.repeat(200)}@example.com`, + }; + const result = await buildPushHTTPRequest(createOptions({ message })); + expect(result.body).toBeInstanceOf(ArrayBuffer); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c58b92f..7c5922f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,22 @@ importers: specifier: ^3.1.2 version: 3.1.2(@types/node@22.14.1) + sites/test: + dependencies: + '@pushforge/builder': + specifier: workspace:* + version: link:../../packages/builder + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250124.0 + version: 4.20260128.0 + typescript: + specifier: ^5.7.3 + version: 5.8.3 + wrangler: + specifier: ^4.61.0 + version: 4.61.0(@cloudflare/workers-types@4.20260128.0) + packages: '@babel/code-frame@7.26.2': @@ -102,163 +118,516 @@ packages: cpu: [x64] os: [win32] + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.11.0': + resolution: {integrity: sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20260115.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260124.0': + resolution: {integrity: sha512-VuqscLhiiVIf7t/dcfkjtT0LKJH+a06KUFwFTHgdTcqyLbFZ44u1SLpOONu5fyva4A9MdaKh9a+Z/tBC1d76nw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260124.0': + resolution: {integrity: sha512-PfnjoFooPgRKFUIZcEP9irnn5Y7OgXinjM+IMlKTdEyLWjMblLsbsqAgydf75+ii0715xAeUlWQjZrWdyOZjMw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260124.0': + resolution: {integrity: sha512-KSkZl4kwcWeFXI7qsaLlMnKwjgdZwI0OEARjyZpiHCxJCqAqla9XxQKNDscL2Z3qUflIo30i+uteGbFrhzuVGQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260124.0': + resolution: {integrity: sha512-61xjSUNk745EVV4vXZP0KGyLCatcmamfBB+dcdQ8kDr6PrNU4IJ1kuQFSJdjybyDhJRm4TpGVywq+9hREuF7xA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260124.0': + resolution: {integrity: sha512-j9O11pwQQV6Vi3peNrJoyIas3SrZHlPj0Ah+z1hDW9o1v35euVBQJw/PuzjPOXxTFUlGQoMJdfzPsO9xP86g7A==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260128.0': + resolution: {integrity: sha512-oid8qPnF4K5Wmgf66bUUrGycwL8BOCGm9ptQOoQNR/jhY5TmDObLtPjJm+BmDklkpAkaM1FnqKY9lo+FNo78AA==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@esbuild/aix-ppc64@0.25.2': resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.2': resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.2': resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.2': resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.2': resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.2': resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.2': resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.2': resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.2': resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.2': resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.2': resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.2': resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.2': resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.2': resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.2': resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.2': resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.2': resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.2': resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.2': resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.2': resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.2': resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.2': resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.2': resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.2': resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.2': resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -385,6 +754,15 @@ packages: resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} engines: {node: '>=12'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rollup/rollup-android-arm-eabi@4.40.0': resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} cpu: [arm] @@ -544,6 +922,10 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -552,6 +934,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -644,6 +1029,9 @@ packages: before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -762,6 +1150,10 @@ packages: resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} engines: {node: '>=12'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -802,6 +1194,10 @@ packages: deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -834,6 +1230,9 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -842,6 +1241,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1129,6 +1533,10 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1206,6 +1614,11 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + miniflare@4.20260124.0: + resolution: {integrity: sha512-Co8onUh+POwOuLty4myQg+Nzg9/xZ5eAJc1oqYBzRovHd/XIpb5WAnRVaubcfAQJ85awWtF3yXUHCDx6cIaN3w==} + engines: {node: '>=18.0.0'} + hasBin: true + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1404,6 +1817,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1524,6 +1940,15 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1621,6 +2046,10 @@ packages: resolution: {integrity: sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==} engines: {node: '>=18'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -1692,6 +2121,9 @@ packages: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} engines: {node: '>= 0.4'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} @@ -1717,6 +2149,13 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} @@ -1839,6 +2278,21 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerd@1.20260124.0: + resolution: {integrity: sha512-JN6voV/fUQK342a39Rl+20YVmtIXZVbpxc7V/m809lUnlTGPy4aa5MI7PMoc+9qExgAEOw9cojvN5zOfqmMWLg==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.61.0: + resolution: {integrity: sha512-Kb8NMe1B/HM7/ds3hU+fcV1U7T996vRKJ0UU/qqgNUMwdemTRA+sSaH3mQvQslIBbprHHU81s0huA6fDIcwiaQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260124.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1846,6 +2300,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -1874,6 +2340,12 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + snapshots: '@babel/code-frame@7.26.2': @@ -1919,86 +2391,301 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.11.0(unenv@2.0.0-rc.24)(workerd@1.20260124.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260124.0 + + '@cloudflare/workerd-darwin-64@1.20260124.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260124.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20260124.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260124.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20260124.0': + optional: true + + '@cloudflare/workers-types@4.20260128.0': {} + '@colors/colors@1.5.0': optional: true + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.2': optional: true + '@esbuild/aix-ppc64@0.27.0': + optional: true + '@esbuild/android-arm64@0.25.2': optional: true + '@esbuild/android-arm64@0.27.0': + optional: true + '@esbuild/android-arm@0.25.2': optional: true + '@esbuild/android-arm@0.27.0': + optional: true + '@esbuild/android-x64@0.25.2': optional: true + '@esbuild/android-x64@0.27.0': + optional: true + '@esbuild/darwin-arm64@0.25.2': optional: true + '@esbuild/darwin-arm64@0.27.0': + optional: true + '@esbuild/darwin-x64@0.25.2': optional: true + '@esbuild/darwin-x64@0.27.0': + optional: true + '@esbuild/freebsd-arm64@0.25.2': optional: true + '@esbuild/freebsd-arm64@0.27.0': + optional: true + '@esbuild/freebsd-x64@0.25.2': optional: true + '@esbuild/freebsd-x64@0.27.0': + optional: true + '@esbuild/linux-arm64@0.25.2': optional: true + '@esbuild/linux-arm64@0.27.0': + optional: true + '@esbuild/linux-arm@0.25.2': optional: true + '@esbuild/linux-arm@0.27.0': + optional: true + '@esbuild/linux-ia32@0.25.2': optional: true + '@esbuild/linux-ia32@0.27.0': + optional: true + '@esbuild/linux-loong64@0.25.2': optional: true + '@esbuild/linux-loong64@0.27.0': + optional: true + '@esbuild/linux-mips64el@0.25.2': optional: true + '@esbuild/linux-mips64el@0.27.0': + optional: true + '@esbuild/linux-ppc64@0.25.2': optional: true + '@esbuild/linux-ppc64@0.27.0': + optional: true + '@esbuild/linux-riscv64@0.25.2': optional: true + '@esbuild/linux-riscv64@0.27.0': + optional: true + '@esbuild/linux-s390x@0.25.2': optional: true + '@esbuild/linux-s390x@0.27.0': + optional: true + '@esbuild/linux-x64@0.25.2': optional: true + '@esbuild/linux-x64@0.27.0': + optional: true + '@esbuild/netbsd-arm64@0.25.2': optional: true + '@esbuild/netbsd-arm64@0.27.0': + optional: true + '@esbuild/netbsd-x64@0.25.2': optional: true + '@esbuild/netbsd-x64@0.27.0': + optional: true + '@esbuild/openbsd-arm64@0.25.2': optional: true + '@esbuild/openbsd-arm64@0.27.0': + optional: true + '@esbuild/openbsd-x64@0.25.2': optional: true + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + '@esbuild/sunos-x64@0.25.2': optional: true + '@esbuild/sunos-x64@0.27.0': + optional: true + '@esbuild/win32-arm64@0.25.2': optional: true + '@esbuild/win32-arm64@0.27.0': + optional: true + '@esbuild/win32-ia32@0.25.2': optional: true + '@esbuild/win32-ia32@0.27.0': + optional: true + '@esbuild/win32-x64@0.25.2': optional: true + '@esbuild/win32-x64@0.27.0': + optional: true + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2148,6 +2835,18 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@rollup/rollup-android-arm-eabi@4.40.0': optional: true @@ -2351,10 +3050,14 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@sindresorhus/is@7.2.0': {} + '@sindresorhus/merge-streams@2.3.0': {} '@sindresorhus/merge-streams@4.0.0': {} + '@speed-highlight/core@1.2.14': {} + '@types/estree@1.0.7': {} '@types/node@22.14.1': @@ -2445,6 +3148,8 @@ snapshots: before-after-hook@3.0.2: {} + blake3-wasm@2.1.5: {} + bottleneck@2.19.5: {} braces@3.0.3: @@ -2574,6 +3279,8 @@ snapshots: convert-hrtime@5.0.0: {} + cookie@1.1.1: {} + core-util-is@1.0.3: {} cosmiconfig@9.0.0(typescript@5.8.3): @@ -2605,6 +3312,8 @@ snapshots: deprecation@2.3.1: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -2634,6 +3343,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.6.0: {} esbuild@0.25.2: @@ -2664,6 +3375,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.2 '@esbuild/win32-x64': 0.25.2 + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -2946,6 +3686,8 @@ snapshots: jsonparse@1.3.1: {} + kleur@4.1.5: {} + lines-and-columns@1.2.4: {} load-json-file@4.0.0: @@ -3010,6 +3752,18 @@ snapshots: mimic-fn@4.0.0: {} + miniflare@4.20260124.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260124.0 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimist@1.2.8: {} ms@2.1.3: {} @@ -3122,6 +3876,8 @@ snapshots: path-key@4.0.0: {} + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} path-type@6.0.0: {} @@ -3283,6 +4039,39 @@ snapshots: semver@7.7.1: {} + semver@7.7.3: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3367,6 +4156,8 @@ snapshots: function-timeout: 1.0.2 time-span: 5.1.0 + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -3431,6 +4222,9 @@ snapshots: traverse@0.6.8: {} + tslib@2.8.1: + optional: true + type-fest@1.4.0: {} type-fest@2.19.0: {} @@ -3444,6 +4238,12 @@ snapshots: undici-types@6.21.0: {} + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicode-emoji-modifier-base@1.0.0: {} unicorn-magic@0.1.0: {} @@ -3552,6 +4352,31 @@ snapshots: wordwrap@1.0.0: {} + workerd@1.20260124.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260124.0 + '@cloudflare/workerd-darwin-arm64': 1.20260124.0 + '@cloudflare/workerd-linux-64': 1.20260124.0 + '@cloudflare/workerd-linux-arm64': 1.20260124.0 + '@cloudflare/workerd-windows-64': 1.20260124.0 + + wrangler@4.61.0(@cloudflare/workers-types@4.20260128.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.11.0(unenv@2.0.0-rc.24)(workerd@1.20260124.0) + blake3-wasm: 2.1.5 + esbuild: 0.27.0 + miniflare: 4.20260124.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260124.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20260128.0 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -3560,6 +4385,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -3589,3 +4416,16 @@ snapshots: yargs-parser: 21.1.1 yoctocolors@2.1.1: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5cc9d8b..446c998 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - - 'packages/*' \ No newline at end of file + - 'packages/*' + - 'sites/*' \ No newline at end of file